Finished client assigning

This commit is contained in:
Donald Zou
2025-07-18 18:49:19 +08:00
parent 1839645360
commit c8348f7be8
9 changed files with 1638 additions and 1555 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,8 @@
<script setup async> <script setup async>
import LocaleText from "@/components/text/localeText.vue"; import LocaleText from "@/components/text/localeText.vue";
import {onMounted, ref} from "vue";
import {GetLocale} from "@/utilities/locale.js";
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import SearchClients from "@/components/configurationComponents/peerAssignModalComponents/searchClients.vue"; import SearchClients from "@/components/configurationComponents/peerAssignModalComponents/searchClients.vue";
import AssignedClients from "@/components/configurationComponents/peerAssignModalComponents/assignedClients.vue"; import AssignedClients from "@/components/configurationComponents/peerAssignModalComponents/assignedClients.vue";
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
const props = defineProps({ const props = defineProps({
selectedPeer: Object selectedPeer: Object
@@ -12,44 +10,17 @@ const props = defineProps({
const emits = defineEmits([ const emits = defineEmits([
'close' 'close'
]) ])
const assignments = ref([]) const assignmentStore = DashboardClientAssignmentStore()
const clients = ref([])
await fetchGet('/api/clients/allClients', {},(res) => {
clients.value = res.data;
console.log(clients.value)
})
const getAssignedClients = async () => { if (assignmentStore.clients.length > 0){
await fetchGet('/api/clients/assignedClients', { assignmentStore.getClients()
ConfigurationName: props.selectedPeer.configuration.Name, }else{
Peer: props.selectedPeer.id await assignmentStore.getClients()
}, (res) => {
assignments.value = res.data
})
} }
await getAssignedClients() await assignmentStore.getAssignedClients(props.selectedPeer.configuration.Name, props.selectedPeer.id)
const assignClient = async (clientID) => { const assignClient = async (clientID) => {
await fetchPost('/api/clients/assignClient', { await assignmentStore.assignClient(props.selectedPeer.configuration.Name, props.selectedPeer.id, clientID)
ConfigurationName: props.selectedPeer.configuration.Name,
Peer: props.selectedPeer.id,
ClientID: clientID
}, async (res) => {
if (res.status){
await getAssignedClients()
}
})
}
const unassignClient = async (assignmentID) => {
await fetchPost('/api/clients/unassignClient', {
AssignmentID: assignmentID
}, async (res) => {
if (res.status){
await getAssignedClients()
}
})
} }
</script> </script>
@@ -66,12 +37,11 @@ const unassignClient = async (assignmentID) => {
</div> </div>
<div class="card-body px-4 pb-4 d-flex gap-2 flex-column"> <div class="card-body px-4 pb-4 d-flex gap-2 flex-column">
<AssignedClients <AssignedClients
@unassign="args => unassignClient(args)" :configuration-name="props.selectedPeer.configuration.Name"
:assignments="assignments"></AssignedClients> :peer="props.selectedPeer.id"
></AssignedClients>
<SearchClients <SearchClients
:assignments="assignments" @assign="args => assignClient(args)"></SearchClients>
@assign="args => assignClient(args)"
:clients="clients"></SearchClients>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,11 @@
<script setup> <script setup>
import LocaleText from "@/components/text/localeText.vue"; import LocaleText from "@/components/text/localeText.vue";
const props = defineProps(['assignments']) import {ref} from "vue";
import Assignment from "@/components/configurationComponents/peerAssignModalComponents/assignment.vue";
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
const emits = defineEmits(['unassign']) const emits = defineEmits(['unassign'])
const props = defineProps(['configurationName', 'peer'])
const assignmentStore = DashboardClientAssignmentStore()
</script> </script>
<template> <template>
@@ -10,26 +14,11 @@ const emits = defineEmits(['unassign'])
<LocaleText t="Assigned Clients"></LocaleText> <LocaleText t="Assigned Clients"></LocaleText>
</h6> </h6>
<TransitionGroup name="list" tag="div" class="position-relative"> <TransitionGroup name="list" tag="div" class="position-relative">
<div class="bg-body-secondary rounded-3 text-start p-2 d-flex mb-2 assignment" <Assignment :assignment="a" :key="a.AssignmentID"
:key="a.AssignmentID" @unassign="assignmentStore.unassignClient(configurationName, peer, a.AssignmentID)"
v-for="a in assignments"> v-for="a in assignmentStore.assignments"></Assignment>
<div class="d-flex flex-column">
<small>
{{ a.Client.Email }}
</small>
<small class="text-muted">
{{ a.Client.Name ? a.Client.Name + ' | ' : '' }}{{ a.Client.ClientGroup ? a.Client.ClientGroup : 'Local' }}
</small>
</div>
<button
@click="emits('unassign', a.AssignmentID)"
aria-label="Delete Assignment"
class="btn bg-danger-subtle text-danger-emphasis ms-auto">
<i class="bi bi-trash-fill"></i>
</button>
</div>
</TransitionGroup> </TransitionGroup>
<div class="text-center" v-if="assignments.length === 0"> <div class="text-center" v-if="assignmentStore.assignments.length === 0">
<small class="text-muted"> <small class="text-muted">
<LocaleText t="No client assigned to this peer yet"></LocaleText> <LocaleText t="No client assigned to this peer yet"></LocaleText>
</small> </small>

View File

@@ -0,0 +1,64 @@
<script setup>
import {ref} from "vue";
import LocaleText from "@/components/text/localeText.vue";
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
const props = defineProps(['assignment'])
const emits = defineEmits(['unassign'])
const confirmDelete = ref(false)
const assignmentStore = DashboardClientAssignmentStore()
</script>
<template>
<div class="bg-body-secondary rounded-3 text-start p-2 mb-2 assignment">
<div class="d-flex" v-if="!confirmDelete">
<div class="d-flex flex-column">
<small>
{{ assignment.Client.Email }}
</small>
<small class="text-muted">
{{ assignment.Client.Name ? assignment.Client.Name + ' | ' : '' }}{{ assignment.Client.ClientGroup ? assignment.Client.ClientGroup : 'Local' }}
</small>
</div>
<button
v-if="!confirmDelete"
@click="confirmDelete = !confirmDelete"
:class="{disabled: assignmentStore.unassigning}"
aria-label="Delete Assignment"
class="btn bg-danger-subtle text-danger-emphasis ms-auto">
<i class="bi bi-trash-fill"></i>
</button>
</div>
<div class="d-flex gap-2" v-else>
<div class="d-flex flex-column">
<small>
<LocaleText t="Are you sure to delete assignment for"></LocaleText>
</small>
<small class="text-muted">
<LocaleText :t="assignment.Client.Email + ' in group ' + (assignment.Client.ClientGroup ? assignment.Client.ClientGroup : 'Local') + '?'"></LocaleText>
</small>
</div>
<button
@click="emits('unassign')"
aria-label="Delete Assignment"
:class="{disabled: assignmentStore.unassigning}"
class="btn bg-danger-subtle text-danger-emphasis ms-auto">
<span class="spinner-border spinner-border-sm" v-if="assignmentStore.unassigning"></span>
<i class="bi bi-check-lg" v-else></i>
</button>
<button
:class="{disabled: assignmentStore.unassigning}"
@click="confirmDelete = !confirmDelete"
aria-label="Cancel Delete Assignment"
class="btn bg-secondary-subtle text-secondary-emphasis ">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -4,22 +4,25 @@ import LocaleText from "@/components/text/localeText.vue";
import {computed, reactive, ref} from "vue"; import {computed, reactive, ref} from "vue";
import SearchClientsGroup from "@/components/configurationComponents/peerAssignModalComponents/searchClientsGroup.vue"; import SearchClientsGroup from "@/components/configurationComponents/peerAssignModalComponents/searchClientsGroup.vue";
import {fetchPost} from "@/utilities/fetch.js"; import {fetchPost} from "@/utilities/fetch.js";
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
const props = defineProps(['clients', 'newAssignClients', 'assignments']) const props = defineProps(['clients', 'newAssignClients', 'assignments'])
const assignmentStore = DashboardClientAssignmentStore()
const selectedGroup = ref("") const selectedGroup = ref("")
const searchString = ref("") const searchString = ref("")
const getSelectedGroup = computed(() => { const getSelectedGroup = computed(() => {
if (selectedGroup.value){ if (selectedGroup.value){
return { return {
[selectedGroup.value] : props.clients[selectedGroup.value] [selectedGroup.value] : assignmentStore.clients[selectedGroup.value]
} }
} }
return props.clients return assignmentStore.clients
}) })
const groupCount = reactive({}) const groupCount = reactive({})
Object.keys(props.clients).forEach( Object.keys(assignmentStore.clients).forEach(
x => groupCount[x] = props.clients[x].length x => groupCount[x] = assignmentStore.clients[x].length
) )
const emits = defineEmits(['assign']) const emits = defineEmits(['assign'])
@@ -52,7 +55,7 @@ const emits = defineEmits(['assign'])
@click="selectedGroup = groupName" @click="selectedGroup = groupName"
:class="{'active': selectedGroup === groupName}" :class="{'active': selectedGroup === groupName}"
class="btn bg-primary-subtle text-primary-emphasis btn-sm me-2 rounded-3" class="btn bg-primary-subtle text-primary-emphasis btn-sm me-2 rounded-3"
v-for="(_, groupName) in clients"> v-for="(_, groupName) in assignmentStore.clients">
{{ groupName }} {{ groupName }}
<span class="ms-1 badge" :class="[ groupCount[groupName] > 0 ? 'bg-primary' : 'bg-secondary' ]"> <span class="ms-1 badge" :class="[ groupCount[groupName] > 0 ? 'bg-primary' : 'bg-secondary' ]">
{{ groupCount[groupName] }} {{ groupCount[groupName] }}
@@ -62,11 +65,11 @@ const emits = defineEmits(['assign'])
</div> </div>
<div class="p-3 border rounded-3 d-flex flex-column gap-2 overflow-y-scroll" style="height: 400px"> <div class="p-3 border rounded-3 d-flex flex-column gap-2 overflow-y-scroll" style="height: 400px">
<SearchClientsGroup <SearchClientsGroup
:assignments="assignments"
@assign="(args) => emits('assign', args)" @assign="(args) => emits('assign', args)"
@count="(args) => groupCount[groupName] = args" @count="(args) => groupCount[groupName] = args"
:searchString="searchString" :searchString="searchString"
:group="group" :groupName="groupName" v-for="(group, groupName) in getSelectedGroup"></SearchClientsGroup> :group="group" :groupName="groupName"
v-for="(group, groupName) in getSelectedGroup"></SearchClientsGroup>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,14 +1,15 @@
<script setup> <script setup>
import {computed} from "vue"; import {computed} from "vue";
import LocaleText from "@/components/text/localeText.vue"; import LocaleText from "@/components/text/localeText.vue";
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
const props = defineProps(['group', 'groupName', 'searchString', 'assignments']) const props = defineProps(['group', 'groupName', 'searchString'])
const emits = defineEmits(['count', 'assign']) const emits = defineEmits(['count', 'assign'])
const assignmentStore = DashboardClientAssignmentStore()
const filterGroup = computed(() => { const filterGroup = computed(() => {
let g = props.group.filter(x => let g = props.group.filter(x =>
!props.assignments.map(a => a.Client.ClientID).includes(x.ClientID)) !assignmentStore.assignments.map(a => a.Client.ClientID).includes(x.ClientID))
if (props.searchString){ if (props.searchString){
let v = g.filter( let v = g.filter(
x => (x.Name && x.Name.includes(props.searchString)) || (x.Email && x.Email.includes(props.searchString)) x => (x.Name && x.Name.includes(props.searchString)) || (x.Email && x.Email.includes(props.searchString))
@@ -27,13 +28,22 @@ const filterGroup = computed(() => {
<small>{{groupName}}</small> <small>{{groupName}}</small>
</h6> </h6>
<div v-if="filterGroup.length > 0" class="d-flex flex-column gap-2"> <div v-if="filterGroup.length > 0" class="d-flex flex-column gap-2">
<div class="bg-body-secondary rounded-3 text-start p-2 d-flex p-1" role="button" <div class="bg-body-secondary rounded-3 text-start p-2 d-flex"
@click="emits('assign', client.ClientID)"
v-for="client in filterGroup"> v-for="client in filterGroup">
<div class="d-flex flex-column">
<small class="mb-0"> <small class="mb-0">
{{ client.Email }} {{ client.Email }}
</small> </small>
<small class="text-muted ms-auto">{{ client.Name }}</small> <small class="text-muted">{{ client.Name ? client.Name : 'No Name' }}</small>
</div>
<button
@click="emits('assign', client.ClientID)"
:class="{disabled: assignmentStore.assigning}"
class="btn bg-success-subtle text-success-emphasis ms-auto">
<span class="spinner-border spinner-border-sm" v-if="assignmentStore.assigning === client.ClientID"></span>
<i class="bi bi-plus-circle-fill" v-else></i>
</button>
</div> </div>
</div> </div>
<div v-else> <div v-else>

View File

@@ -0,0 +1,70 @@
import {defineStore} from "pinia";
import {ref} from "vue";
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
export const DashboardClientAssignmentStore =
defineStore('DashboardClientAssignmentStore', () => {
const assignments = ref([])
const clients = ref({})
const unassigning = ref(false)
const assigning = ref("")
const getClients = async () => {
await fetchGet('/api/clients/allClients', {},(res) => {
clients.value = res.data;
})
}
const getClientById = (ClientID) => {
return Object.values(clients.value).flat().find(x => x.ClientID === ClientID)
}
const getAssignedClients = async (ConfigurationName, Peer) => {
await fetchGet('/api/clients/assignedClients', {
ConfigurationName: ConfigurationName,
Peer: Peer
}, (res) => {
assignments.value = res.data
})
}
const assignClient = async (ConfigurationName, Peer, ClientID) => {
assigning.value = ClientID;
await fetchPost('/api/clients/assignClient', {
ConfigurationName: ConfigurationName,
Peer: Peer,
ClientID: ClientID
}, async (res) => {
if (res.status){
await getAssignedClients(ConfigurationName, Peer)
assigning.value = "";
}else{
assigning.value = "";
}
})
}
const unassignClient = async (ConfigurationName, Peer, AssignmentID) => {
unassigning.value = true;
await fetchPost('/api/clients/unassignClient', {
AssignmentID: AssignmentID
}, async (res) => {
if (res.status){
await getAssignedClients(ConfigurationName, Peer)
}
unassigning.value = false;
})
}
return {
assignments,
getAssignedClients,
getClients,
clients,
unassignClient,
assignClient,
getClientById,
unassigning,
assigning
}
})

View File

@@ -48,12 +48,12 @@ window.dayjs = dayjs
</script> </script>
<template> <template>
<div class="card rounded-3 border shadow"> <div class="card rounded-3 border-0 shadow">
<div class="card-header border-0 align-items-center d-flex p-3 flex-column flex-sm-row gap-2"> <div class="card-header rounded-top-3 border-0 align-items-center d-flex p-3 flex-column flex-sm-row gap-2">
<small class="fw-bold"> <small class="fw-bold">
{{ props.config.name }} {{ props.config.name }}
</small> </small>
<span class="badge rounded-3 shadow ms-sm-auto" <span class="badge rounded-3 ms-sm-auto"
:class="[props.config.protocol === 'wg' ? 'wireguardBg' : 'amneziawgBg' ]" :class="[props.config.protocol === 'wg' ? 'wireguardBg' : 'amneziawgBg' ]"
v-if="props.config.protocol === 'wg'"> v-if="props.config.protocol === 'wg'">
{{ props.config.protocol === 'wg' ? 'WireGuard': 'AmneziaWG' }} {{ props.config.protocol === 'wg' ? 'WireGuard': 'AmneziaWG' }}

View File

@@ -72,9 +72,11 @@ const signOut = async () => {
<small>No configuration available</small> <small>No configuration available</small>
</div> </div>
</div> </div>
<div v-else class="d-flex py-4"> <div v-else class="d-flex p-3">
<div class="bg-body rounded-3 d-flex" style="width: 100%; height: 211px;">
<div class="spinner-border m-auto"></div> <div class="spinner-border m-auto"></div>
</div> </div>
</div>
</Transition> </Transition>
</div> </div>
</template> </template>