From 2936e15a5d37d0c34bb388b97400a4c36ca6b599 Mon Sep 17 00:00:00 2001 From: Dmytro Bondar Date: Thu, 19 Sep 2024 17:21:36 +0200 Subject: [PATCH 1/3] Added peers sorting for Interface and Profile views --- frontend/src/assets/base.css | 16 +++++++++--- frontend/src/helpers/utils.js | 3 +++ frontend/src/stores/peers.js | 23 ++++++++++++++++- frontend/src/stores/profile.js | 23 ++++++++++++++++- frontend/src/views/InterfaceView.vue | 38 ++++++++++++++++++++++++---- frontend/src/views/ProfileView.vue | 30 +++++++++++++++++++--- 6 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 frontend/src/helpers/utils.js diff --git a/frontend/src/assets/base.css b/frontend/src/assets/base.css index 0d813989..13a3f7e6 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 00000000..6d3e11fc --- /dev/null +++ b/frontend/src/helpers/utils.js @@ -0,0 +1,3 @@ +export function ipToLong(ip) { + return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0); +} diff --git a/frontend/src/stores/peers.js b/frontend/src/stores/peers.js index df681168..9b843dd4 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 { ipToLong } 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,26 @@ 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 ? ipToLong(aValue[0]) : 0; + bValue = bValue.length > 0 ? ipToLong(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; + } + 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 f795d226..5f931461 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 { ipToLong } 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,26 @@ 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 ? ipToLong(aValue[0]) : 0; + bValue = bValue.length > 0 ? ipToLong(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; + } + 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 6e6070eb..57f6b6f4 100644 --- a/frontend/src/views/InterfaceView.vue +++ b/frontend/src/views/InterfaceView.vue @@ -21,6 +21,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 +328,25 @@ onMounted(async () => { - {{ $t('interfaces.table-heading.name') }} - {{ $t('interfaces.table-heading.user') }} - {{ $t('interfaces.table-heading.ip') }} - {{ $t('interfaces.table-heading.endpoint') }} - {{ $t('interfaces.table-heading.status') }} + + {{ $t("interfaces.table-heading.name") }} + + + + {{ $t("interfaces.table-heading.user") }} + + + + {{ $t("interfaces.table-heading.ip") }} + + + + {{ $t("interfaces.table-heading.endpoint") }} + + + {{ $t("interfaces.table-heading.status") }} + + diff --git a/frontend/src/views/ProfileView.vue b/frontend/src/views/ProfileView.vue index 4fe063c8..18bca99e 100644 --- a/frontend/src/views/ProfileView.vue +++ b/frontend/src/views/ProfileView.vue @@ -12,10 +12,25 @@ const profile = profileStore() const viewedPeerId = ref("") const editPeerId = 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 + } + profile.sortKey = sortKey.value; + profile.sortOrder = sortOrder.value; +} + onMounted(async () => { await profile.LoadUser() await profile.LoadPeers() await profile.LoadStats() + await profile.calculatePages(); // Forces to show initial page number }) @@ -58,9 +73,18 @@ onMounted(async () => { value=""> - {{ $t('profile.table-heading.name') }} - {{ $t('profile.table-heading.ip') }} - {{ $t('profile.table-heading.stats') }} + + {{ $t("profile.table-heading.name") }} + + + + {{ $t("profile.table-heading.ip") }} + + + + {{ $t("profile.table-heading.stats") }} + + {{ $t('profile.table-heading.interface') }} From 1ac1b8c6fc96ae4b5db97704298961f38be9b14e Mon Sep 17 00:00:00 2001 From: Dmytro Bondar Date: Sun, 22 Sep 2024 22:26:14 +0200 Subject: [PATCH 2/3] Use ip-address package to sort with IPv6 addresses only --- frontend/package-lock.json | 26 ++++++++++++++++++++++++++ frontend/package.json | 1 + frontend/src/helpers/utils.js | 14 ++++++++++++-- frontend/src/stores/peers.js | 6 +++--- frontend/src/stores/profile.js | 6 +++--- 5 files changed, 45 insertions(+), 8 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0b1cbf15..3f87220a 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 5e89f46c..74aab2b5 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/helpers/utils.js b/frontend/src/helpers/utils.js index 6d3e11fc..f09c7633 100644 --- a/frontend/src/helpers/utils.js +++ b/frontend/src/helpers/utils.js @@ -1,3 +1,13 @@ -export function ipToLong(ip) { - return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0); +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() } diff --git a/frontend/src/stores/peers.js b/frontend/src/stores/peers.js index 9b843dd4..62415f83 100644 --- a/frontend/src/stores/peers.js +++ b/frontend/src/stores/peers.js @@ -4,7 +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 { ipToLong } from '@/helpers/utils'; +import { ipToBigInt } from '@/helpers/utils'; const baseUrl = `/peer` @@ -47,8 +47,8 @@ export const peerStore = defineStore({ let aValue = a[state.sortKey]; let bValue = b[state.sortKey]; if (state.sortKey === 'Addresses') { - aValue = aValue.length > 0 ? ipToLong(aValue[0]) : 0; - bValue = bValue.length > 0 ? ipToLong(bValue[0]) : 0; + 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; diff --git a/frontend/src/stores/profile.js b/frontend/src/stores/profile.js index 5f931461..40a30e14 100644 --- a/frontend/src/stores/profile.js +++ b/frontend/src/stores/profile.js @@ -4,7 +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 { ipToLong } from '@/helpers/utils'; +import { ipToBigInt } from '@/helpers/utils'; const baseUrl = `/user` @@ -43,8 +43,8 @@ export const profileStore = defineStore({ let aValue = a[state.sortKey]; let bValue = b[state.sortKey]; if (state.sortKey === 'Addresses') { - aValue = aValue.length > 0 ? ipToLong(aValue[0]) : 0; - bValue = bValue.length > 0 ? ipToLong(bValue[0]) : 0; + 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; From 58ce0dadb70ba5e0a65b0cb3a55a8ede2edf04fb Mon Sep 17 00:00:00 2001 From: Dmytro Bondar Date: Mon, 23 Sep 2024 11:52:49 +0200 Subject: [PATCH 3/3] Add RX/TX column and fix add-peer button title --- frontend/src/helpers/utils.js | 7 +++++++ frontend/src/stores/peers.js | 4 ++++ frontend/src/stores/profile.js | 4 ++++ frontend/src/views/InterfaceView.vue | 7 +++++++ frontend/src/views/ProfileView.vue | 9 ++++++++- 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/frontend/src/helpers/utils.js b/frontend/src/helpers/utils.js index f09c7633..30d3a63f 100644 --- a/frontend/src/helpers/utils.js +++ b/frontend/src/helpers/utils.js @@ -11,3 +11,10 @@ export function ipToBigInt(ip) { 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 62415f83..b308467b 100644 --- a/frontend/src/stores/peers.js +++ b/frontend/src/stores/peers.js @@ -54,6 +54,10 @@ export const peerStore = defineStore({ 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; diff --git a/frontend/src/stores/profile.js b/frontend/src/stores/profile.js index 40a30e14..1f16f56d 100644 --- a/frontend/src/stores/profile.js +++ b/frontend/src/stores/profile.js @@ -50,6 +50,10 @@ export const profileStore = defineStore({ 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; diff --git a/frontend/src/views/InterfaceView.vue b/frontend/src/views/InterfaceView.vue index 57f6b6f4..596f495a 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() @@ -347,6 +348,9 @@ onMounted(async () => { {{ $t("interfaces.table-heading.status") }} + RX/TX + + @@ -373,6 +377,9 @@ onMounted(async () => { + + {{ humanFileSize(peers.Statistics(peer.Identifier).BytesReceived) }} / {{ humanFileSize(peers.Statistics(peer.Identifier).BytesTransmitted) }} + diff --git a/frontend/src/views/ProfileView.vue b/frontend/src/views/ProfileView.vue index 18bca99e..0516fd41 100644 --- a/frontend/src/views/ProfileView.vue +++ b/frontend/src/views/ProfileView.vue @@ -5,6 +5,7 @@ import { onMounted, ref } from "vue"; import { profileStore } from "@/stores/profile"; import PeerEditModal from "@/components/PeerEditModal.vue"; import { settingsStore } from "@/stores/settings"; +import { humanFileSize } from "@/helpers/utils"; const settings = settingsStore() const profile = profileStore() @@ -56,7 +57,7 @@ onMounted(async () => {
@@ -85,6 +86,9 @@ onMounted(async () => { {{ $t("profile.table-heading.stats") }} + RX/TX + + {{ $t('profile.table-heading.interface') }} @@ -114,6 +118,9 @@ onMounted(async () => { + + {{ humanFileSize(profile.Statistics(peer.Identifier).BytesReceived) }} / {{ humanFileSize(profile.Statistics(peer.Identifier).BytesTransmitted) }} + {{ peer.InterfaceIdentifier }}