Migrated configuration sorting to Pinia store

Fixed #841
This commit is contained in:
Donald Zou
2025-08-12 17:22:40 +08:00
parent b81d4667b2
commit 93baa505c7
10 changed files with 137 additions and 55 deletions

View File

@@ -25,6 +25,7 @@
"npm": "^10.5.0",
"ol": "^10.2.1",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0",
"qrcode": "^1.5.3",
"qrcodejs": "^1.0.0",
"simple-code-editor": "^2.0.9",
@@ -3030,6 +3031,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-pick-omit": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/deep-pick-omit/-/deep-pick-omit-1.2.1.tgz",
"integrity": "sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==",
"license": "MIT"
},
"node_modules/defaults": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz",
@@ -3051,6 +3058,12 @@
"node": ">=10"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -3060,6 +3073,12 @@
"node": ">=0.4.0"
}
},
"node_modules/destr": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz",
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -7564,6 +7583,33 @@
}
}
},
"node_modules/pinia-plugin-persistedstate": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.5.0.tgz",
"integrity": "sha512-QTkP1xJVyCdr2I2p3AKUZM84/e+IS+HktRxKGAIuDzkyaKKV48mQcYkJFVVDuvTxlI5j6X3oZObpqoVB8JnWpw==",
"license": "MIT",
"dependencies": {
"deep-pick-omit": "^1.2.1",
"defu": "^6.1.4",
"destr": "^2.0.5"
},
"peerDependencies": {
"@nuxt/kit": ">=3.0.0",
"@pinia/nuxt": ">=0.10.0",
"pinia": ">=3.0.0"
},
"peerDependenciesMeta": {
"@nuxt/kit": {
"optional": true
},
"@pinia/nuxt": {
"optional": true
},
"pinia": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/@vue/devtools-api": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.7.tgz",

View File

@@ -29,6 +29,7 @@
"npm": "^10.5.0",
"ol": "^10.2.1",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0",
"qrcode": "^1.5.3",
"qrcodejs": "^1.0.0",
"simple-code-editor": "^2.0.9",

View File

@@ -41,6 +41,7 @@ export default {
<template>
<div class="card shadow-sm rounded-3 peerCard"
:id="'peer_'+Peer.id"
:class="{'border-warning': Peer.restricted}">
<div>
<div v-if="!Peer.restricted" class="card-header bg-transparent d-flex align-items-center gap-2 border-0">

View File

@@ -189,6 +189,17 @@ const searchPeers = computed(() => {
return 0;
}).slice(0, showPeersCount.value)
})
watch(() => route.query.id, (newValue) => {
if (newValue){
wireguardConfigurationStore.searchString = newValue
}else{
wireguardConfigurationStore.searchString = undefined
}
}, {
immediate: true
})
</script>
<template>

View File

@@ -15,7 +15,6 @@ export default {
return {store, wireguardConfigurationStore}
},
props: {
configuration: Object
},
data(){
@@ -43,18 +42,6 @@ export default {
}
},
methods: {
debounce(){
if (!this.searchStringTimeout){
this.searchStringTimeout = setTimeout(() => {
this.wireguardConfigurationStore.searchString = this.searchString;
}, 300)
}else{
clearTimeout(this.searchStringTimeout)
this.searchStringTimeout = setTimeout(() => {
this.wireguardConfigurationStore.searchString = this.searchString;
}, 300)
}
},
updateSort(sort){
fetchPost("/api/updateDashboardConfigurationItem", {
section: "Server",

View File

@@ -3,13 +3,14 @@ import {GetLocale} from "@/utilities/locale.js";
import {computed, onMounted, ref, useTemplateRef} from "vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import LocaleText from "@/components/text/localeText.vue";
import {useRoute, useRouter} from "vue-router";
const searchBarPlaceholder = computed(() => {
return GetLocale("Search Peers...")
})
let searchStringTimeout = undefined
const searchString = ref("")
const wireguardConfigurationStore = WireguardConfigurationsStore()
const searchString = ref(wireguardConfigurationStore.searchString)
const debounce = () => {
if (!searchStringTimeout){
@@ -27,10 +28,18 @@ const debounce = () => {
const emits = defineEmits(['close'])
const input = useTemplateRef('searchBar')
const route = useRoute()
const router = useRouter()
if (route.query.peer){
searchString.value = route.query.peer
router.replace({ query: null })
}
onMounted(() => {
input.value.focus();
})
</script>
<template>

View File

@@ -29,17 +29,17 @@ export default {
}
},
async mounted() {
if (!window.localStorage.getItem('ConfigurationListSort')){
window.localStorage.setItem('ConfigurationListSort', JSON.stringify(this.currentSort))
}else{
this.currentSort = JSON.parse(window.localStorage.getItem('ConfigurationListSort'))
}
if (!window.localStorage.getItem('ConfigurationListDisplay')){
window.localStorage.setItem('ConfigurationListDisplay', this.currentDisplay)
}else{
this.currentDisplay = window.localStorage.getItem('ConfigurationListDisplay')
}
// if (!window.localStorage.getItem('ConfigurationListSort')){
// window.localStorage.setItem('ConfigurationListSort', JSON.stringify(this.currentSort))
// }else{
// this.currentSort = JSON.parse(window.localStorage.getItem('ConfigurationListSort'))
// }
//
// if (!window.localStorage.getItem('ConfigurationListDisplay')){
// window.localStorage.setItem('ConfigurationListDisplay', this.currentDisplay)
// }else{
// this.currentDisplay = window.localStorage.getItem('ConfigurationListDisplay')
// }
await this.wireguardConfigurationsStore.getConfigurations();
@@ -54,17 +54,8 @@ export default {
},
computed: {
configurations(){
return [...this.wireguardConfigurationsStore.Configurations]
return this.wireguardConfigurationsStore.sortConfigurations
.filter(x => x.Name.toLowerCase().includes(this.searchKey) || x.PublicKey.includes(this.searchKey) || !this.searchKey)
.sort((a, b) => {
if (this.currentSort.order === 'desc') {
return this.dotNotation(a, this.currentSort.key) < this.dotNotation(b, this.currentSort.key) ?
1 : this.dotNotation(a, this.currentSort.key) > this.dotNotation(b, this.currentSort.key) ? -1 : 0;
} else {
return this.dotNotation(a, this.currentSort.key) > this.dotNotation(b, this.currentSort.key) ?
1 : this.dotNotation(a, this.currentSort.key) < this.dotNotation(b, this.currentSort.key) ? -1 : 0;
}
})
}
},
methods: {
@@ -76,18 +67,16 @@ export default {
return result
},
updateSort(key){
if (this.currentSort.key === key){
if (this.currentSort.order === 'asc') this.currentSort.order = 'desc'
else this.currentSort.order = 'asc'
if (this.wireguardConfigurationsStore.CurrentSort.key === key){
this.wireguardConfigurationsStore.CurrentSort.order =
this.wireguardConfigurationsStore.CurrentSort.order === 'asc' ? 'desc' : 'asc'
}else{
this.currentSort.key = key
this.wireguardConfigurationsStore.CurrentSort.key = key
}
window.localStorage.setItem('ConfigurationListSort', JSON.stringify(this.currentSort))
},
updateDisplay(key){
if (this.currentDisplay !== key){
this.currentDisplay = key
window.localStorage.setItem('ConfigurationListDisplay', this.currentDisplay)
if (this.wireguardConfigurationsStore.CurrentDisplay !== key){
this.wireguardConfigurationsStore.CurrentDisplay = key
}
}
}
@@ -121,12 +110,12 @@ export default {
<div class="d-flex ms-auto ms-lg-0">
<a role="button"
@click="updateSort(sv)"
:class="{'bg-primary-subtle text-primary-emphasis': this.currentSort.key === sv}"
class="px-2 py-1 rounded-3" v-for="(s, sv) in this.sort">
:class="{'bg-primary-subtle text-primary-emphasis': this.wireguardConfigurationsStore.CurrentSort.key === sv}"
class="px-2 py-1 rounded-3" v-for="(s, sv) in this.wireguardConfigurationsStore.SortOptions">
<small>
<i class="bi me-2"
:class="[this.currentSort.order === 'asc' ? 'bi-sort-up' : 'bi-sort-down']"
v-if="this.currentSort.key === sv"></i>{{s}}
:class="[this.wireguardConfigurationsStore.CurrentSort.order === 'asc' ? 'bi-sort-up' : 'bi-sort-down']"
v-if="this.wireguardConfigurationsStore.CurrentSort.key === sv"></i>{{s}}
</small>
</a>
</div>
@@ -139,7 +128,7 @@ export default {
<a role="button"
@click="updateDisplay(x.name)"
v-for="x in [{name: 'List', key: 'list'}, {name: 'Grid', key: 'grid'}]"
:class="{'bg-primary-subtle text-primary-emphasis': this.currentDisplay === x.name}"
:class="{'bg-primary-subtle text-primary-emphasis': this.wireguardConfigurationsStore.CurrentDisplay === x.name}"
class="px-2 py-1 rounded-3">
<small>
<i class="bi me-2" :class="'bi-' + x.key"></i> <LocaleText :t="x.name"></LocaleText>
@@ -167,7 +156,7 @@ export default {
<LocaleText t="You don't have any WireGuard configurations yet. Please check the configuration folder or change it in Settings. By default the folder is /etc/wireguard."></LocaleText>
</p>
<ConfigurationCard
:display="this.currentDisplay"
:display="this.wireguardConfigurationsStore.CurrentDisplay"
v-for="(c, index) in configurations"
:delay="index*0.03 + 's'"
v-else-if="this.configurationLoaded"

View File

@@ -74,14 +74,14 @@ export default {
</RouterLink></li>
<li class="nav-item">
<RouterLink class="nav-link rounded-3" to="/settings"
exact-active-class="active">
active-class="active">
<i class="bi bi-gear me-2"></i>
<LocaleText t="Settings"></LocaleText>
</RouterLink>
</li>
<li class="nav-item">
<RouterLink class="nav-link rounded-3" to="/clients"
exact-active-class="active">
active-class="active">
<i class="bi bi-people me-2"></i>
<LocaleText t="Clients"></LocaleText>
</RouterLink>
@@ -98,7 +98,7 @@ export default {
<LocaleText t="WireGuard Configurations"></LocaleText>
</h6>
<ul class="nav flex-column px-2 gap-1">
<li class="nav-item" v-for="c in this.wireguardConfigurationsStore.Configurations">
<li class="nav-item" v-for="c in this.wireguardConfigurationsStore.sortConfigurations">
<RouterLink :to="'/configuration/'+c.Name + '/peers'" class="nav-link nav-conf-link rounded-3"
active-class="active"
>

View File

@@ -9,6 +9,7 @@ import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router/router.js'
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
let Locale;
await fetch("/api/locale")
.then(res => res.json())
@@ -19,11 +20,12 @@ await fetch("/api/locale")
const app = createApp(App)
app.use(router)
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate)
pinia.use(({ store }) => {
store.$router = markRaw(router)
})
app.use(pinia)
const store = DashboardConfigurationStore()
store.Locale = Locale;

View File

@@ -5,9 +5,19 @@ import {GetLocale} from "@/utilities/locale.js";
export const WireguardConfigurationsStore = defineStore('WireguardConfigurationsStore', {
state: () => ({
Configurations: undefined,
Configurations: [],
searchString: "",
ConfigurationListInterval: undefined,
SortOptions: {
Name: GetLocale("Name"),
Status: GetLocale("Status"),
'DataUsage.Total': GetLocale("Total Usage")
},
CurrentSort: {
key: "Name",
order: "asc"
},
CurrentDisplay: "List",
PeerScheduleJobs: {
dropdowns: {
Field: [
@@ -66,6 +76,19 @@ export const WireguardConfigurationsStore = defineStore('WireguardConfigurations
}
}
}),
getters: {
sortConfigurations(){
return [...this.Configurations].sort((a, b) => {
if (this.CurrentSort.order === 'desc') {
return this.dotNotation(a, this.CurrentSort.key) < this.dotNotation(b, this.CurrentSort.key) ?
1 : this.dotNotation(a, this.CurrentSort.key) > this.dotNotation(b, this.CurrentSort.key) ? -1 : 0;
} else {
return this.dotNotation(a, this.CurrentSort.key) > this.dotNotation(b, this.CurrentSort.key) ?
1 : this.dotNotation(a, this.CurrentSort.key) < this.dotNotation(b, this.CurrentSort.key) ? -1 : 0;
}
})
},
},
actions: {
async getConfigurations(){
await fetchGet("/api/getWireguardConfigurations", {}, (res) => {
@@ -73,6 +96,14 @@ export const WireguardConfigurationsStore = defineStore('WireguardConfigurations
// this.Configurations = []
});
},
dotNotation(object, dotNotation){
let result = dotNotation.split('.').reduce((o, key) => o && o[key], object)
if (typeof result === "string"){
return result.toLowerCase()
}
return result
},
regexCheckIP(ip){
let regex = /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/;
return regex.test(ip)
@@ -84,5 +115,10 @@ export const WireguardConfigurationsStore = defineStore('WireguardConfigurations
const reg = /^[A-Za-z0-9+/]{43}=?=?$/;
return reg.test(key)
}
},
persist: {
pick: [
"CurrentSort", "CurrentDisplay"
]
}
});