diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0b1cbf1..3f87220 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "bootstrap": "^5.3.2", "bootswatch": "^5.3.2", "flag-icons": "^7.1.0", + "ip-address": "^9.0.5", "is-cidr": "^5.0.3", "is-ip": "^5.0.1", "pinia": "^2.1.7", @@ -914,6 +915,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/ip-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", @@ -962,6 +976,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -1117,6 +1137,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/super-regex": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5e89f46..74aab2b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "bootstrap": "^5.3.2", "bootswatch": "^5.3.2", "flag-icons": "^7.1.0", + "ip-address": "^9.0.5", "is-cidr": "^5.0.3", "is-ip": "^5.0.1", "pinia": "^2.1.7", diff --git a/frontend/src/assets/base.css b/frontend/src/assets/base.css index 0d81398..13a3f7e 100644 --- a/frontend/src/assets/base.css +++ b/frontend/src/assets/base.css @@ -1,9 +1,17 @@ a.disabled { - pointer-events: none; - cursor: default; - color: #888888; + pointer-events: none; + cursor: default; + color: #888888; } .text-wrap { - overflow-break: anywhere; + overflow-break: anywhere; +} + +.asc::after { + content: " ↑"; +} + +.desc::after { + content: " ↓"; } diff --git a/frontend/src/helpers/utils.js b/frontend/src/helpers/utils.js new file mode 100644 index 0000000..30d3a63 --- /dev/null +++ b/frontend/src/helpers/utils.js @@ -0,0 +1,20 @@ +import { Address4, Address6 } from "ip-address" + +export function ipToBigInt(ip) { + // Check if it's an IPv4 address + if (ip.includes(".")) { + const addr = new Address4(ip) + return addr.bigInteger() + } + + // Otherwise, assume it's an IPv6 address + const addr = new Address6(ip) + return addr.bigInteger() +} + +export function humanFileSize(size) { + const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] + if (size === 0) return "0B" + const i = parseInt(Math.floor(Math.log(size) / Math.log(1024))) + return Math.round(size / Math.pow(1024, i), 2) + sizes[i] +} diff --git a/frontend/src/stores/peers.js b/frontend/src/stores/peers.js index df68116..b308467 100644 --- a/frontend/src/stores/peers.js +++ b/frontend/src/stores/peers.js @@ -4,6 +4,7 @@ import {notify} from "@kyvg/vue3-notification"; import {interfaceStore} from "./interfaces"; import {freshPeer, freshStats} from '@/helpers/models'; import { base64_url_encode } from '@/helpers/encoding'; +import { ipToBigInt } from '@/helpers/utils'; const baseUrl = `/peer` @@ -21,6 +22,8 @@ export const peerStore = defineStore({ pageOffset: 0, pages: [], fetching: false, + sortKey: 'IsConnected', // Default sort key + sortOrder: -1, // 1 for ascending, -1 for descending }), getters: { Find: (state) => { @@ -39,8 +42,30 @@ export const peerStore = defineStore({ return p.DisplayName.includes(state.filter) || p.Identifier.includes(state.filter) }) }, + Sorted: (state) => { + return state.Filtered.slice().sort((a, b) => { + let aValue = a[state.sortKey]; + let bValue = b[state.sortKey]; + if (state.sortKey === 'Addresses') { + aValue = aValue.length > 0 ? ipToBigInt(aValue[0]) : 0; + bValue = bValue.length > 0 ? ipToBigInt(bValue[0]) : 0; + } + if (state.sortKey === 'IsConnected') { + aValue = state.statsEnabled && state.stats[a.Identifier]?.IsConnected ? 1 : 0; + bValue = state.statsEnabled && state.stats[b.Identifier]?.IsConnected ? 1 : 0; + } + if (state.sortKey === 'Traffic') { + aValue = state.statsEnabled ? (state.stats[a.Identifier].BytesReceived + state.stats[a.Identifier].BytesTransmitted) : 0; + bValue = state.statsEnabled ? (state.stats[b.Identifier].BytesReceived + state.stats[b.Identifier].BytesTransmitted) : 0; + } + let result = 0; + if (aValue > bValue) result = 1; + if (aValue < bValue) result = -1; + return state.sortOrder === 1 ? result : -result; + }); + }, FilteredAndPaged: (state) => { - return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize) + return state.Sorted.slice(state.pageOffset, state.pageOffset + state.pageSize); }, ConfigQrUrl: (state) => { return (id) => state.peers.find((p) => p.Identifier === id) ? apiWrapper.url(`${baseUrl}/config-qr/${base64_url_encode(id)}`) : '' diff --git a/frontend/src/stores/profile.js b/frontend/src/stores/profile.js index f795d22..1f16f56 100644 --- a/frontend/src/stores/profile.js +++ b/frontend/src/stores/profile.js @@ -4,6 +4,7 @@ import {notify} from "@kyvg/vue3-notification"; import {authStore} from "@/stores/auth"; import { base64_url_encode } from '@/helpers/encoding'; import {freshStats} from "@/helpers/models"; +import { ipToBigInt } from '@/helpers/utils'; const baseUrl = `/user` @@ -19,6 +20,8 @@ export const profileStore = defineStore({ pageOffset: 0, pages: [], fetching: false, + sortKey: 'IsConnected', // Default sort key + sortOrder: -1, // 1 for ascending, -1 for descending }), getters: { FindPeers: (state) => { @@ -35,8 +38,30 @@ export const profileStore = defineStore({ return p.DisplayName.includes(state.filter) || p.Identifier.includes(state.filter) }) }, + Sorted: (state) => { + return state.FilteredPeers.slice().sort((a, b) => { + let aValue = a[state.sortKey]; + let bValue = b[state.sortKey]; + if (state.sortKey === 'Addresses') { + aValue = aValue.length > 0 ? ipToBigInt(aValue[0]) : 0; + bValue = bValue.length > 0 ? ipToBigInt(bValue[0]) : 0; + } + if (state.sortKey === 'IsConnected') { + aValue = state.statsEnabled && state.stats[a.Identifier]?.IsConnected ? 1 : 0; + bValue = state.statsEnabled && state.stats[b.Identifier]?.IsConnected ? 1 : 0; + } + if (state.sortKey === 'Traffic') { + aValue = state.statsEnabled ? (state.stats[a.Identifier].BytesReceived + state.stats[a.Identifier].BytesTransmitted) : 0; + bValue = state.statsEnabled ? (state.stats[b.Identifier].BytesReceived + state.stats[b.Identifier].BytesTransmitted) : 0; + } + let result = 0; + if (aValue > bValue) result = 1; + if (aValue < bValue) result = -1; + return state.sortOrder === 1 ? result : -result; + }); + }, FilteredAndPagedPeers: (state) => { - return state.FilteredPeers.slice(state.pageOffset, state.pageOffset + state.pageSize) + return state.Sorted.slice(state.pageOffset, state.pageOffset + state.pageSize); }, isFetching: (state) => state.fetching, hasNextPage: (state) => state.pageOffset < (state.FilteredPeerCount - state.pageSize), diff --git a/frontend/src/views/InterfaceView.vue b/frontend/src/views/InterfaceView.vue index cfee9c9..69b7953 100644 --- a/frontend/src/views/InterfaceView.vue +++ b/frontend/src/views/InterfaceView.vue @@ -10,6 +10,7 @@ import {peerStore} from "@/stores/peers"; import {interfaceStore} from "@/stores/interfaces"; import {notify} from "@kyvg/vue3-notification"; import {settingsStore} from "@/stores/settings"; +import {humanFileSize} from '@/helpers/utils'; const settings = settingsStore() const interfaces = interfaceStore() @@ -21,6 +22,20 @@ const multiCreatePeerId = ref("") const editInterfaceId = ref("") const viewedInterfaceId = ref("") +const sortKey = ref(""); +const sortOrder = ref(1); + +function sortBy(key) { + if (sortKey.value === key) { + sortOrder.value = sortOrder.value * -1; // Toggle sort order + } else { + sortKey.value = key; + sortOrder.value = 1; // Default to ascending + } + peers.sortKey = sortKey.value; + peers.sortOrder = sortOrder.value; +} + function calculateInterfaceName(id, name) { let result = id if (name) { @@ -314,11 +329,28 @@ onMounted(async () => {