Peer groups are done... ish?

This commit is contained in:
Donald Zou
2025-08-19 00:40:01 +08:00
parent ba85879151
commit b8792200c6
59 changed files with 432 additions and 180 deletions

View File

@@ -15,7 +15,7 @@ export default {
PeerTagBadge, LocaleText, PeerSettingsDropdown
},
props: {
Peer: Object
Peer: Object, ConfigurationInfo: Object
},
data(){
return {
@@ -106,7 +106,9 @@ export default {
<div class="d-flex align-items-center gap-1"
:class="{'ms-auto': dashboardStore.Configuration.Server.dashboard_peer_list_display === 'list'}"
>
<PeerTagBadge BackgroundColor="#ff3838" GroupName="IDK" Icon="bi-pencil"></PeerTagBadge>
<PeerTagBadge :BackgroundColor="group.BackgroundColor" :GroupName="group.GroupName" :Icon="'bi-' + group.Icon"
v-for="group in Object.values(ConfigurationInfo.Info.PeerGroups).filter(x => x.Peers.includes(Peer.id))"
></PeerTagBadge>
<div class="ms-auto px-2 rounded-3 subMenuBtn"
:class="{active: this.subMenuOpened}"
>
@@ -124,6 +126,7 @@ export default {
@share="this.$emit('share')"
@assign="this.$emit('assign')"
:Peer="Peer"
:ConfigurationInfo="ConfigurationInfo"
v-if="this.subMenuOpened"
ref="target"
></PeerSettingsDropdown>
@@ -138,16 +141,7 @@ export default {
<style scoped>
.slide-fade-leave-active, .slide-fade-enter-active{
transition: all 0.2s cubic-bezier(0.82, 0.58, 0.17, 1.3);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(20px);
opacity: 0;
filter: blur(3px);
}
.subMenuBtn.active{
background-color: #ffffff20;

View File

@@ -13,13 +13,6 @@ import PeerListModals from "@/components/configurationComponents/peerListCompone
import PeerIntersectionObserver from "@/components/configurationComponents/peerIntersectionObserver.vue";
import ConfigurationDescription from "@/components/configurationComponents/configurationDescription.vue";
// import PeerSearchBar from "@/components/configurationComponents/peerSearchBar.vue"
// import PeerJobsAllModal from "@/components/configurationComponents/peerJobsAllModal.vue"
// import PeerJobsLogsModal from "@/components/configurationComponents/peerJobsLogsModal.vue"
// import EditConfigurationModal from "@/components/configurationComponents/editConfiguration.vue"
// import SelectPeersModal from "@/components/configurationComponents/selectPeers.vue"
// import PeerAddModal from "@/components/configurationComponents/peerAddModal.vue"
// Async Components
const PeerSearchBar = defineAsyncComponent(() => import("@/components/configurationComponents/peerSearchBar.vue"))
const PeerJobsAllModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerJobsAllModal.vue"))
@@ -377,6 +370,7 @@ watch(() => route.query.id, (newValue) => {
:key="peer.id"
v-for="peer in searchPeers">
<Peer :Peer="peer"
:ConfigurationInfo="configurationInfo"
@share="configurationModals.peerShare.modalOpen = true; configurationModalSelectedPeer = peer"
@refresh="fetchPeerList()"
@jobs="configurationModals.peerScheduleJobs.modalOpen = true; configurationModalSelectedPeer = peer"

View File

@@ -39,7 +39,8 @@ export default {
searchString: "",
searchStringTimeout: undefined,
showDisplaySettings: false,
showMoreSettings: false
showMoreSettings: false,
tagManager: false
}
},
methods: {
@@ -159,11 +160,19 @@ export default {
</div>
<div class="position-relative">
<button
@click="tagManager = !tagManager"
class="btn btn-sm w-100 text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle position-relative">
<i class="bi me-2 bi-hash"></i>
<LocaleText t="Tags"></LocaleText>
</button>
<PeerTag :configuration="configuration"></PeerTag>
<Transition name="slide-fade">
<PeerTag
@update="args => configuration.Info.PeerGroups = args"
@close="this.tagManager = false"
:configuration="configuration" v-if="this.tagManager"></PeerTag>
</Transition>
</div>
<button class="btn btn-sm text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle ms-lg-auto"

View File

@@ -4,16 +4,18 @@ import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.
import LocaleText from "@/components/text/localeText.vue";
import PeerSettingsDropdownTool
from "@/components/configurationComponents/peerSettingsDropdownComponents/peerSettingsDropdownTool.vue";
import PeerTagSelectDropdown
from "@/components/configurationComponents/peerSettingsDropdownComponents/peerTagSelectDropdown.vue";
export default {
name: "peerSettingsDropdown",
components: {PeerSettingsDropdownTool, LocaleText},
components: {PeerTagSelectDropdown, PeerSettingsDropdownTool, LocaleText},
setup(){
const dashboardStore = DashboardConfigurationStore()
return {dashboardStore}
},
props: {
Peer: Object
Peer: Object, ConfigurationInfo: Object
},
data(){
return{
@@ -154,6 +156,18 @@ export default {
<i class="me-auto bi bi-diagram-2"></i> <LocaleText t="Assign Peer"></LocaleText>
</a>
</li>
<li class="dropdown dropstart">
<a class="dropdown-item d-flex " role="button"
data-bs-auto-close="outside"
data-bs-toggle="dropdown"
>
<i class="me-auto bi bi-diagram-2"></i> <LocaleText t="Tag Peer"></LocaleText>
</a>
<PeerTagSelectDropdown
@update="this.$emit('refresh')"
:Peer="Peer" :ConfigurationInfo="ConfigurationInfo"></PeerTagSelectDropdown>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item d-flex text-warning"

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import PeerTagBadge from "@/components/configurationComponents/peerTagBadge.vue";
import {reactive, watch} from "vue";
const props = defineProps(['Peer', 'ConfigurationInfo'])
const groups = reactive({...props.ConfigurationInfo.Info.PeerGroups})
import { fetchPost } from "@/utilities/fetch.js"
const emits = defineEmits(['update'])
watch(() => groups, (newVal) => {
fetchPost("/api/updateWireguardConfigurationInfo", {
Name: props.ConfigurationInfo.Name,
Key: "PeerGroups",
Value: newVal
}, (res) => {
if (res.status){
emits('update', groups)
}
})
}, {
deep: true
})
const togglePeer = (groupId, peerId) => {
if (groups[groupId].Peers.includes(peerId)){
groups[groupId].Peers = groups[groupId].Peers.filter(x => x !== peerId)
}else{
groups[groupId].Peers.push(peerId)
}
}
</script>
<template>
<ul class="dropdown-menu">
<li v-for="(group, groupId) in groups" >
<a role="button"
@click="togglePeer(groupId, Peer.id)"
class="dropdown-item d-flex align-items-center">
<i class="bi bi-check-circle-fill" v-if="group.Peers.includes(Peer.id)"></i>
<i class="bi bi-circle" v-else></i>
<PeerTagBadge
class="ms-auto"
:BackgroundColor="group.BackgroundColor" :GroupName="group.GroupName" :Icon="'bi-' + group.Icon"></PeerTagBadge>
</a>
</li>
</ul>
</template>
<style scoped>
</style>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import {reactive, ref} from "vue";
import {reactive, ref, watch} from "vue";
import LocaleText from "@/components/text/localeText.vue";
import bootstrapIcons from "bootstrap-icons/font/bootstrap-icons.json"
const predefinedColors = {
"blue-100": "#cfe2ff",
@@ -105,17 +106,21 @@ const predefinedColors = {
"white": "#fff",
"black": "#000",
}
const props = defineProps(['configuration'])
const groups = reactive({...props.configuration.Info.PeerGroups})
import { v4 } from "uuid"
import PeerTagSetting from "@/components/configurationComponents/peerTagComponents/peerTagSetting.vue";
import PeerTagIconPicker from "@/components/configurationComponents/peerTagComponents/peerTagIconPicker.vue";
import PeerTagColorPicker from "@/components/configurationComponents/peerTagComponents/peerTagColorPicker.vue";
import { fetchPost } from "@/utilities/fetch.js"
const addGroup = () => {
groups[v4().toString()] = {
GroupName: "",
Description: "",
BackgroundColor: randomColor(),
Icon: "",
Icon: randomIcon(),
Peers: []
}
}
@@ -125,22 +130,70 @@ const randomColor = () => {
const n = Math.floor(Math.random() * keys.length) + 1
return predefinedColors[keys[n]]
}
const randomIcon = () => {
const keys = Object.keys(bootstrapIcons)
const n = Math.floor(Math.random() * keys.length) + 1
return keys[n]
}
const iconPickerOpen = ref(false)
const colorPickerOpen = ref(false)
const selectedKey = ref("")
const emits = defineEmits(['close', 'update'])
watch(() => groups, (newVal) => {
fetchPost("/api/updateWireguardConfigurationInfo", {
Name: props.configuration.Name,
Key: "PeerGroups",
Value: newVal
}, (res) => {
if (res.status){
emits('update', groups)
}
})
}, {
deep: true
})
</script>
<template>
<div class="card shadow" id="peerTag">
<div class="card-body" >
<small v-if="Object.keys(groups).length === 0">
<LocaleText t="No tag"></LocaleText>
</small>
<div class="d-flex flex-column gap-2" v-else>
<PeerTagSetting v-for="group in groups" :group="group">
</PeerTagSetting>
</div>
<div class="card-body p-2" >
<Transition name="zoom" mode="out-in">
<div v-if="!iconPickerOpen && !colorPickerOpen">
<div v-if="Object.keys(groups).length === 0" class="text-center text-muted">
<small><LocaleText t="No tag"></LocaleText></small>
</div>
<div class="d-flex flex-column gap-2" v-else>
<PeerTagSetting v-for="(group, key) in groups"
@delete="delete groups[key]"
@colorPickerOpen="colorPickerOpen = true; selectedKey = key"
@iconPickerOpen="iconPickerOpen = true; selectedKey = key"
:key="key"
:group="group"></PeerTagSetting>
</div>
</div>
<PeerTagIconPicker
v-else-if="iconPickerOpen"
@close="iconPickerOpen = false"
:group="groups[selectedKey]"></PeerTagIconPicker>
<PeerTagColorPicker :colors="predefinedColors"
@close="colorPickerOpen = false"
:group="groups[selectedKey]"
v-else-if="colorPickerOpen"></PeerTagColorPicker>
</Transition>
</div>
<div class="card-footer btn" @click="addGroup()">
<small><i class="bi bi-plus-lg me-2"></i><LocaleText t="Tag"></LocaleText></small>
<div class="card-footer p-2 d-flex gap-2" >
<button
@click="emits('close')"
class="btn btn-sm bg-secondary-subtle text-secondary-emphasis border-secondary-subtle rounded-3">
<small><LocaleText t="Close"></LocaleText></small>
</button>
<button
@click="addGroup"
class="btn btn-sm bg-primary-subtle text-primary-emphasis border-primary-subtle rounded-3 ms-auto">
<small><i class="bi bi-plus-lg me-2"></i><LocaleText t="Tag"></LocaleText></small>
</button>
</div>
</div>
</template>

View File

@@ -1,28 +1,18 @@
<script setup lang="ts">
import { fromString } from 'css-color-converter';
import {computed} from "vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"
const props = defineProps(["BackgroundColor", "GroupName", "Icon"])
const color = computed(() => {
if (props.BackgroundColor){
const cssColor = fromString(props.BackgroundColor)
if (cssColor) {
const rgb = cssColor.toRgbaArray()
return +((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 255000).toFixed(2)
}
}
return 0
})
const store = WireguardConfigurationsStore();
</script>
<template>
<span
class="badge rounded-3 shadow"
:style="{'background-color': BackgroundColor, 'color': color > 0.69 ? '#000':'#fff' }"
>
<i class="bi me-1" :class="Icon" v-if="Icon"></i>#{{ GroupName }}
<h6 class="mb-0">
<span
class="badge rounded-3 shadow"
:style="{'background-color': BackgroundColor, 'color': store.colorText(BackgroundColor) }"
>
<i class="bi" :class="[Icon, GroupName ? 'me-2': '']" v-if="Icon"></i>{{ GroupName }}
</span>
</h6>
</template>
<style scoped>

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import {onMounted, ref} from "vue";
const props = defineProps(['colors', 'group'])
const emits = defineEmits(['close', 'select', ''])
const searchString = ref("")
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"
const store = WireguardConfigurationsStore();
onMounted(() => {
let ele = document.querySelector(".icon-grid div.active")
if (ele){
ele.parentElement.scrollTop =
document.querySelector(".icon-grid div.active").offsetTop - 60
}
})
</script>
<template>
<div class="w-100 bg-body top-0 border rounded-2">
<div class="p-2 d-flex align-items-center gap-2 border-bottom">
<label>
<i class="bi bi-search"></i>
</label>
<input v-model="searchString"
placeholder="Search Icon"
class="form-control form-control-sm rounded-2">
</div>
<div class="p-2 d-grid icon-grid"
style="grid-template-columns: repeat(auto-fit, minmax(30px, 30px)); gap: 3px; max-height: 300px; overflow-y: scroll">
<div class="rounded-1 border icon d-flex"
:class="{active: group.BackgroundColor === color}"
style="cursor: pointer"
:aria-label="name"
:style="{'background-color': color}"
:key="color"
@click="group.BackgroundColor = color"
v-for="(color, name) in colors">
<i
:style="{color: store.colorText(color)}"
class="bi bi-check-circle m-auto" v-if="group.BackgroundColor === color"></i>
</div>
</div>
<div class="p-2 border-top d-flex gap-2">
<button class="btn btn-sm btn-success rounded-2 ms-auto" @click="emits('close')">
Done
</button>
</div>
</div>
</template>
<style scoped>
.icon{
flex: 1;
aspect-ratio: 1 / 1;
}
</style>

View File

@@ -0,0 +1,67 @@
<script setup lang="ts">
import {computed, onMounted, ref} from "vue";
const props = defineProps(['group'])
import bootstrapIcons from "bootstrap-icons/font/bootstrap-icons.json"
const emits = defineEmits(['close', 'select'])
onMounted(() => {
let ele = document.querySelector(".icon-grid div.active")
if (ele){
ele.parentElement.scrollTop =
document.querySelector(".icon-grid div.active").offsetTop - 60
}
})
const searchString = ref("")
const searchIcon = computed(() => {
if (searchString.value){
return [...Object.keys(bootstrapIcons).filter(
x => x.includes(searchString.value.toLowerCase())
)]
}
return Object.keys(bootstrapIcons)
})
</script>
<template>
<div class="w-100 bg-body top-0 border rounded-2">
<div class="p-2 d-flex align-items-center gap-2 border-bottom">
<label>
<i class="bi bi-search"></i>
</label>
<input v-model="searchString"
placeholder="Search Icon"
class="form-control form-control-sm rounded-2">
</div>
<div class="p-2 d-grid icon-grid"
style="grid-template-columns: repeat(auto-fit, minmax(30px, 30px)); gap: 3px; max-height: 300px; overflow-y: scroll">
<div class="rounded-1 border icon d-flex"
:class="{'text-bg-success active' : group.Icon === iconName}"
style="cursor: pointer"
:key="iconName"
@click="group.Icon = iconName"
v-for="iconName in searchIcon">
<i class="bi m-auto" :class="'bi-' + iconName"></i>
</div>
</div>
<div class="p-2 border-top d-flex gap-2">
<button
@click="group.Icon = ''"
class="btn btn-sm btn-secondary rounded-2 ms-auto">
Remove Icon
</button>
<button class="btn btn-sm btn-success rounded-2" @click="emits('close')">
Done
</button>
</div>
</div>
</template>
<style scoped>
.icon{
flex: 1;
min-width: 30px;
max-width: 30px;
width: 30px;
aspect-ratio: 1 / 1;
}
</style>

View File

@@ -1,45 +1,43 @@
<script setup lang="ts">
import {computed} from "vue";
import { fromString } from 'css-color-converter';
import LocaleText from "@/components/text/localeText.vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"
import {ref} from "vue";
const store = WireguardConfigurationsStore();
const props = defineProps(['group'])
const emits = defineEmits(['delete', 'iconPickerOpen', 'colorPickerOpen'])
const color = computed(() => {
if (props.group.BackgroundColor){
const cssColor = fromString(props.group.BackgroundColor)
if (cssColor) {
const rgb = cssColor.toRgbaArray()
return +((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 255000).toFixed(2) > 0.69 ? "#000":"#fff"
}
}
return "#ffffff"
})
console.log(color)
const groupName = ref(props.group.GroupName)
</script>
<template>
<div :style="{'background-color': group.BackgroundColor }"
class="badge rounded-3 d-flex align-items-center overflow-scroll">
<div class="border rounded-3 p-2">
<div
aria-label="Pick icon button"
style="height: 30px;"
class="d-flex align-items-center border rounded-2 p-2 btn btn-sm">
<i class="bi bi-pencil-fill" :style="{color: color}"></i>
</div>
<div contenteditable="true" class="flex-grow-1 text-start d-flex align-items-center rounded-2"
:style="{color: color}"
style="height: 30px">
Tag Name
</div>
<div style="height: 30px;"
aria-label="Pick color button"
class="d-flex align-items-center border-0 rounded-2 p-2 btn btn-sm">
<i class="bi bi-palette-fill" :style="{color: color}"></i>
</div>
<div style="height: 30px;"
aria-label="Pick color button"
class="d-flex align-items-center border-0 rounded-2 p-2 btn btn-sm">
<i class="bi bi-trash-fill" :style="{color: color}"></i>
class="rounded-3 align-items-center overflow-scroll d-flex gap-2 position-relative">
<div
@click="emits('iconPickerOpen')"
aria-label="Pick icon button"
class="d-flex align-items-center p-2 btn btn-sm border rounded-2">
<i class="bi" :class="'bi-' + group.Icon" :aria-label="group.Icon" v-if="group.Icon"></i>
<span style="white-space: nowrap" v-else>
<LocaleText t="No Icon"></LocaleText>
</span>
</div>
<div
aria-label="Pick color button"
@click="emits('colorPickerOpen')"
:style="{'background-color': group.BackgroundColor, 'color': store.colorText(group.BackgroundColor)}"
class="d-flex align-items-center p-2 btn btn-sm border rounded-2">
<i class="bi bi-eyedropper" ></i>
</div>
<input
v-model="groupName"
@change="group.GroupName = groupName"
class="form-control form-control-sm p-2 rounded-2 w-100">
<div
aria-label="Pick color button" @click="emits('delete')"
class="rounded-2 border p-2 btn btn-sm btn-outline-danger">
<i class="bi bi-trash-fill" ></i>
</div>
</div>
</div>
</template>

View File

@@ -1289,4 +1289,15 @@ samp{
}
.agent-message-leave-active {
position: absolute;
}
.slide-fade-leave-active, .slide-fade-enter-active{
transition: all 0.2s cubic-bezier(0.82, 0.58, 0.17, 1.3);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(20px);
opacity: 0;
filter: blur(3px);
}

View File

@@ -2,6 +2,7 @@ import {defineStore} from "pinia";
import {fetchGet} from "@/utilities/fetch.js";
import isCidr from "is-cidr";
import {GetLocale} from "@/utilities/locale.js";
import {fromString} from "css-color-converter";
export const WireguardConfigurationsStore = defineStore('WireguardConfigurationsStore', {
state: () => ({
@@ -99,7 +100,16 @@ export const WireguardConfigurationsStore = defineStore('WireguardConfigurations
this.ConfigurationLoaded = true
});
},
colorText(color){
if (color){
const cssColor = fromString(color)
if (cssColor) {
const rgb = cssColor.toRgbaArray()
return +((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 255000).toFixed(2) > 0.5 ? "#000":"#fff"
}
}
return "#ffffff"
},
dotNotation(object, dotNotation){
let result = dotNotation.split('.').reduce((o, key) => o && o[key], object)
if (typeof result === "string"){