From 4075f72a44c45b5a84b8c36ae6468db3f0ece4f6 Mon Sep 17 00:00:00 2001 From: Andrea Leardini Date: Thu, 8 Jan 2026 10:21:22 +0100 Subject: [PATCH 01/35] update pinia-colada and pinia-colada devtools --- frontend/package-lock.json | 88 ++++++-------------------------------- frontend/package.json | 4 +- 2 files changed, 14 insertions(+), 78 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a3cf78f8..a896fc34 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,7 +16,7 @@ "@logto/vue": "^3.0.8", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/vue-components": "^3.4.0", - "@pinia/colada": "^0.17.6", + "@pinia/colada": "^0.21.0", "@tailwindcss/vite": "^4.1.10", "@vueuse/core": "^13.4.0", "axios": "^1.11.0", @@ -30,7 +30,7 @@ "vue-router": "^4.5.0" }, "devDependencies": { - "@pinia/colada-devtools": "^0.1.5", + "@pinia/colada-devtools": "^0.4.1", "@tsconfig/node22": "^22.0.1", "@types/lodash": "^4.17.18", "@types/node": "^22.14.0", @@ -122,7 +122,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -701,7 +700,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -725,7 +723,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1345,7 +1342,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", "license": "MIT", - "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" }, @@ -1804,13 +1800,10 @@ } }, "node_modules/@pinia/colada": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@pinia/colada/-/colada-0.17.6.tgz", - "integrity": "sha512-odayx9xVMUgC8ZMU/hwqODoboHnSWigp7VsbKGHKrNl9yHnljmxgJMwm1vtF/KIBSd2vUBigmN3maFqcC48/Rg==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@pinia/colada/-/colada-0.21.0.tgz", + "integrity": "sha512-Y7c4gRsZcZCOyxKFbvSadC55sHKJJqKjD3KC1wiOBFl9ubqBlzqOPZl7fj0LkoqbsZYGqAdA0FtNfe9cvgQOXA==", "license": "MIT", - "dependencies": { - "@vue/devtools-api": "^8.0.2" - }, "funding": { "url": "https://github.com/sponsors/posva" }, @@ -1820,9 +1813,9 @@ } }, "node_modules/@pinia/colada-devtools": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@pinia/colada-devtools/-/colada-devtools-0.1.6.tgz", - "integrity": "sha512-wRW/GxP8SiahC5TRVulQe+5NuIQ7DGtgsO4Xsf9tP2HSTTRD8ac+7pn9vbKxovPdXrgAyAo9PWzk1b+y5MYEUQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@pinia/colada-devtools/-/colada-devtools-0.4.1.tgz", + "integrity": "sha512-BmurCHPmgfaq3fZEoKim/g0vrVpM5psAdYsh/kvpmwiARKRlNGR1GPj1Uhm+EE1fpCMfzuQDwRqffb+zydooRw==", "dev": true, "license": "MIT", "funding": { @@ -2613,7 +2606,6 @@ "integrity": "sha512-UJdblFqXymSBhmZf96BnbisoFIr8ooiiBRMolQgg77Ea+VM37jXw76C2LQr9n8wm9+i/OvlUlW6xSvqwzwqznw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2670,7 +2662,6 @@ "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", @@ -3198,15 +3189,6 @@ "he": "^1.2.0" } }, - "node_modules/@vue/devtools-api": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.2.tgz", - "integrity": "sha512-RdwsaYoSTumwZ7XOt5yIPP1/T4O0bTs+c5XaEjmUB6f9x+FvDSL9AekxW1vuhK1lmA9TfewpXVt2r5LIax3LHw==", - "license": "MIT", - "dependencies": { - "@vue/devtools-kit": "^8.0.2" - } - }, "node_modules/@vue/devtools-core": { "version": "7.7.7", "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.7.7.tgz", @@ -3277,30 +3259,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@vue/devtools-kit": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.2.tgz", - "integrity": "sha512-yjZKdEmhJzQqbOh4KFBfTOQjDPMrjjBNCnHBvnTGJX+YLAqoUtY2J+cg7BE+EA8KUv8LprECq04ts75wCoIGWA==", - "license": "MIT", - "dependencies": { - "@vue/devtools-shared": "^8.0.2", - "birpc": "^2.5.0", - "hookable": "^5.5.3", - "mitt": "^3.0.1", - "perfect-debounce": "^2.0.0", - "speakingurl": "^14.0.1", - "superjson": "^2.2.2" - } - }, - "node_modules/@vue/devtools-shared": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.2.tgz", - "integrity": "sha512-mLU0QVdy5Lp40PMGSixDw/Kbd6v5dkQXltd2r+mdVQV7iUog2NlZuLxFZApFZ/mObUBDhoCpf0T3zF2FWWdeHw==", - "license": "MIT", - "dependencies": { - "rfdc": "^1.4.1" - } - }, "node_modules/@vue/eslint-config-prettier": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.2.0.tgz", @@ -3480,7 +3438,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3642,9 +3599,9 @@ } }, "node_modules/birpc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.5.0.tgz", - "integrity": "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -3700,7 +3657,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.2", "caniuse-lite": "^1.0.30001741", @@ -3996,7 +3952,6 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -4320,7 +4275,6 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4382,7 +4336,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -4430,7 +4383,6 @@ "integrity": "sha512-A5dRYc3eQ5i2rJFBW8J6F69ur/H7YfYg+5SCg6v829FU0BhM4fUTrRVR2d4MdZgzw0ioJEk6otYHEAnoGFqO4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", @@ -6420,12 +6372,6 @@ "node": ">= 14.16" } }, - "node_modules/perfect-debounce": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", - "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6463,7 +6409,6 @@ "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz", "integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==", "license": "MIT", - "peer": true, "dependencies": { "@vue/devtools-api": "^7.7.2" }, @@ -6577,7 +6522,6 @@ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6799,7 +6743,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -7217,8 +7160,7 @@ "version": "4.1.13", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.2.3", @@ -7317,7 +7259,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7477,7 +7418,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7632,7 +7572,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -7875,7 +7814,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7889,7 +7827,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -7982,7 +7919,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz", "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.21", "@vue/compiler-sfc": "3.5.21", diff --git a/frontend/package.json b/frontend/package.json index 65c0171e..d2fb321b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,7 +26,7 @@ "@logto/vue": "^3.0.8", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/vue-components": "^3.4.0", - "@pinia/colada": "^0.17.6", + "@pinia/colada": "^0.21.0", "@tailwindcss/vite": "^4.1.10", "@vueuse/core": "^13.4.0", "axios": "^1.11.0", @@ -40,7 +40,7 @@ "vue-router": "^4.5.0" }, "devDependencies": { - "@pinia/colada-devtools": "^0.1.5", + "@pinia/colada-devtools": "^0.4.1", "@tsconfig/node22": "^22.0.1", "@types/lodash": "^4.17.18", "@types/node": "^22.14.0", From e53d852c5d28e56fc6490c14f1f796d05c0b914d Mon Sep 17 00:00:00 2001 From: Andrea Leardini Date: Thu, 8 Jan 2026 10:22:09 +0100 Subject: [PATCH 02/35] show pinia-colada devtools on production environment --- frontend/src/App.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 98c43e10..5df5fe8b 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -55,7 +55,8 @@ onMounted(() => { - + + From 9cc776ed14f55788cd4c411db04ee0100bbc444c Mon Sep 17 00:00:00 2001 From: Andrea Leardini Date: Thu, 8 Jan 2026 12:02:57 +0100 Subject: [PATCH 03/35] fix style of system secret --- frontend/src/components/systems/CreateOrEditSystemDrawer.vue | 4 +--- frontend/src/components/systems/SecretRegeneratedModal.vue | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/systems/CreateOrEditSystemDrawer.vue b/frontend/src/components/systems/CreateOrEditSystemDrawer.vue index a314ac7d..e31b8d7c 100644 --- a/frontend/src/components/systems/CreateOrEditSystemDrawer.vue +++ b/frontend/src/components/systems/CreateOrEditSystemDrawer.vue @@ -395,9 +395,7 @@ function copySecretAndCloseDrawer() {
{{ secret }}
-
- ************************************************************************* -
+
************************
{{ newSecret }} -
- ************************************************************************* -
+
************************
Date: Thu, 8 Jan 2026 12:03:28 +0100 Subject: [PATCH 04/35] fix system notes --- .../src/components/systems/SystemInfoCard.vue | 52 +++++-------------- .../components/systems/SystemNotesModal.vue | 3 +- frontend/src/i18n/en/translation.json | 3 +- 3 files changed, 16 insertions(+), 42 deletions(-) diff --git a/frontend/src/components/systems/SystemInfoCard.vue b/frontend/src/components/systems/SystemInfoCard.vue index a83ff3c9..c054c922 100644 --- a/frontend/src/components/systems/SystemInfoCard.vue +++ b/frontend/src/components/systems/SystemInfoCard.vue @@ -17,39 +17,11 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { getOrganizationIcon } from '@/lib/organizations' import DataItem from '../DataItem.vue' import ClickToCopy from '../ClickToCopy.vue' -import { computed, ref } from 'vue' +import { ref } from 'vue' import SystemNotesModal from './SystemNotesModal.vue' -const NOTES_MAX_LENGTH = 32 - const { state: systemDetail } = useSystemDetail() const isNotesModalShown = ref(false) - -const notesLengthExceeded = computed(() => { - if (!systemDetail.value.data?.notes) { - return false - } - const notes = systemDetail.value.data.notes - if (notes.length > NOTES_MAX_LENGTH || notes.includes('\n')) { - return true - } - return false -}) - -// truncate notes if they exceed a certain length or the contain new lines -const truncatedNotes = computed(() => { - if (!systemDetail.value.data?.notes) { - return '' - } - const notes = systemDetail.value.data.notes - if (notes.length > NOTES_MAX_LENGTH) { - return notes.slice(0, NOTES_MAX_LENGTH) + '...' - } - if (notes.includes('\n')) { - return notes.split('\n')[0] + '...' - } - return notes -}) - - - - + + diff --git a/frontend/src/components/systems/SystemNotesModal.vue b/frontend/src/components/systems/SystemNotesModal.vue index 1d9432b8..4b5bbebe 100644 --- a/frontend/src/components/systems/SystemNotesModal.vue +++ b/frontend/src/components/systems/SystemNotesModal.vue @@ -5,6 +5,7 @@ diff --git a/frontend/src/components/systems/SystemsTable.vue b/frontend/src/components/systems/SystemsTable.vue index 1663e63b..16d109e5 100644 --- a/frontend/src/components/systems/SystemsTable.vue +++ b/frontend/src/components/systems/SystemsTable.vue @@ -47,6 +47,7 @@ import { savePageSizeToStorage } from '@/lib/tablePageSize' import { canManageSystems } from '@/lib/permissions' import { useSystems } from '@/queries/systems/systems' import { + exportSystem, getExport, getProductLogo, getProductName, @@ -234,14 +235,14 @@ function getKebabMenuItems(system: System) { label: t('systems.export_to_pdf'), icon: faFilePdf, action: () => exportSystem(system, 'pdf'), - disabled: !state.value.data?.systems, + disabled: asyncStatus.value === 'loading', }, { id: 'exportToCsv', label: t('systems.export_to_csv'), icon: faFileCsv, action: () => exportSystem(system, 'csv'), - disabled: !state.value.data?.systems, + disabled: asyncStatus.value === 'loading', }, ] @@ -263,6 +264,7 @@ function getKebabMenuItems(system: System) { action: () => showDeleteSystemModal(system), disabled: asyncStatus.value === 'loading', }, + //// add restore deleted system action ] } return items @@ -277,17 +279,6 @@ const goToSystemDetails = (system: System) => { router.push({ name: 'system_detail', params: { systemId: system.id } }) } -async function exportSystem(system: System, format: 'pdf' | 'csv') { - try { - const exportData = await getExport(format, system.system_key) - const fileName = `${system.name}.${format}` - downloadFile(exportData, fileName, format) - } catch (error) { - console.error('Cannot export system to pdf:', error) - throw error - } -} - function onSecretRegenerated(secret: string) { newSecret.value = secret isShownSecretRegeneratedModal.value = true diff --git a/frontend/src/lib/systems/systems.ts b/frontend/src/lib/systems/systems.ts index 3005352b..20c708b1 100644 --- a/frontend/src/lib/systems/systems.ts +++ b/frontend/src/lib/systems/systems.ts @@ -5,7 +5,7 @@ import axios from 'axios' import { API_URL } from '../config' import { useLoginStore } from '@/stores/login' import * as v from 'valibot' -import { type Pagination } from '../common' +import { downloadFile, type Pagination } from '../common' import Ns8Logo from '@/assets/ns8_logo.svg' import NsecLogo from '@/assets/nsec_logo.svg' @@ -290,6 +290,17 @@ export const getProductLogo = (systemType: string) => { } } +export async function exportSystem(system: System, format: 'pdf' | 'csv') { + try { + const exportData = await getExport(format, system.system_key) + const fileName = `${system.name}.${format}` + downloadFile(exportData, fileName, format) + } catch (error) { + console.error(`Cannot export system to ${format}:`, error) + throw error + } +} + export const getExport = ( format: 'csv' | 'pdf', systemKey: string | undefined = undefined, From 962079d8a380def119ab15cc7348377ad717d4ed Mon Sep 17 00:00:00 2001 From: Andrea Leardini Date: Thu, 8 Jan 2026 16:18:58 +0100 Subject: [PATCH 08/35] add restore system action --- .../components/systems/RestoreSystemModal.vue | 88 +++++++++++++++++++ .../src/components/systems/SystemsTable.vue | 36 ++++++-- frontend/src/i18n/en/translation.json | 8 +- frontend/src/lib/systems/systems.ts | 12 +++ 4 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/systems/RestoreSystemModal.vue diff --git a/frontend/src/components/systems/RestoreSystemModal.vue b/frontend/src/components/systems/RestoreSystemModal.vue new file mode 100644 index 00000000..05f44187 --- /dev/null +++ b/frontend/src/components/systems/RestoreSystemModal.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/frontend/src/components/systems/SystemsTable.vue b/frontend/src/components/systems/SystemsTable.vue index 16d109e5..ab0d22ff 100644 --- a/frontend/src/components/systems/SystemsTable.vue +++ b/frontend/src/components/systems/SystemsTable.vue @@ -18,6 +18,7 @@ import { faFilePdf, faFileCsv, faKey, + faRotateLeft, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { @@ -67,6 +68,7 @@ import { downloadFile } from '@/lib/common' import RegenerateSecretModal from './RegenerateSecretModal.vue' import SecretRegeneratedModal from './SecretRegeneratedModal.vue' import ClickToCopy from '../ClickToCopy.vue' +import RestoreSystemModal from './RestoreSystemModal.vue' const { isShownCreateSystemDrawer = false } = defineProps<{ isShownCreateSystemDrawer: boolean @@ -97,6 +99,7 @@ const { state: versionFilterState, asyncStatus: versionFilterAsyncStatus } = use const currentSystem = ref() const isShownCreateOrEditSystemDrawer = ref(false) const isShownDeleteSystemModal = ref(false) +const isShownRestoreSystemModal = ref(false) const isShownRegenerateSecretModal = ref(false) const isShownSecretRegeneratedModal = ref(false) const newSecret = ref('') @@ -205,6 +208,11 @@ function showDeleteSystemModal(system: System) { isShownDeleteSystemModal.value = true } +function showRestoreSystemModal(system: System) { + currentSystem.value = system + isShownRestoreSystemModal.value = true +} + function showRegenerateSecretModal(system: System) { currentSystem.value = system isShownRegenerateSecretModal.value = true @@ -218,7 +226,7 @@ function onCloseDrawer() { function getKebabMenuItems(system: System) { let items: NeDropdownItem[] = [] - if (canManageSystems()) { + if (canManageSystems() && system.status !== 'deleted') { items.push({ id: 'editSystem', label: t('common.edit'), @@ -246,7 +254,7 @@ function getKebabMenuItems(system: System) { }, ] - if (canManageSystems()) { + if (canManageSystems() && system.status !== 'deleted') { items = [ ...items, { @@ -264,9 +272,19 @@ function getKebabMenuItems(system: System) { action: () => showDeleteSystemModal(system), disabled: asyncStatus.value === 'loading', }, - //// add restore deleted system action ] } + + if (canManageSystems() && system.status === 'deleted') { + items.push({ + id: 'restoreSystem', + label: t('common.restore'), + icon: faRotateLeft, + action: () => showRestoreSystemModal(system), + disabled: asyncStatus.value === 'loading', + }) + } + return items } @@ -591,11 +609,9 @@ function onCloseSecretRegeneratedModal() { -
+
+ + { }) } +export const restoreSystem = (system: System) => { + const loginStore = useLoginStore() + + return axios.patch( + `${API_URL}/systems/${system.id}/restore`, + {}, + { + headers: { Authorization: `Bearer ${loginStore.jwtToken}` }, + }, + ) +} + export const regenerateSystemSecret = (systemId: string) => { const loginStore = useLoginStore() From b9dbf7614be0ea488101f571a68922d9ff6a7e13 Mon Sep 17 00:00:00 2001 From: Andrea Leardini Date: Thu, 8 Jan 2026 16:46:37 +0100 Subject: [PATCH 09/35] improve go to system button --- frontend/src/i18n/en/translation.json | 7 +++--- frontend/src/i18n/it/translation.json | 4 ++-- frontend/src/views/SystemDetailView.vue | 29 ++++++++++++++----------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index bb0732bb..2ed19999 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -396,8 +396,8 @@ "overview": "Overview", "product_logo": "{product} logo", "unknown_product": "Unknown product", - "open_system": "Open system", - "open_system_tooltip": "Note: The system may be unreachable due to network settings.", + "go_to_system": "Go to system", + "go_to_system_tooltip": "Note: The system may be unreachable due to network settings.", "uptime": "Uptime", "last_inventory": "Last inventory", "timezone": "Timezone", @@ -409,7 +409,8 @@ "no_inventory_available_description": "The system has not sent any inventory data yet.", "subscription": "Subscription", "system_creation": "System creation", - "system_key": "System key" + "system_key": "System key", + "cannot_determine_system_url_description": "Cannot access the system because its URL cannot be determined." }, "ne_table": { "no_items": "No items", diff --git a/frontend/src/i18n/it/translation.json b/frontend/src/i18n/it/translation.json index e6208421..1a571a62 100644 --- a/frontend/src/i18n/it/translation.json +++ b/frontend/src/i18n/it/translation.json @@ -389,8 +389,8 @@ "overview": "Panoramica", "product_logo": "Logo di {product}", "unknown_product": "Prodotto sconosciuto", - "open_system": "Apri sistema", - "open_system_tooltip": "Nota: Il sistema potrebbe non essere raggiungibile a causa delle impostazioni di rete.", + "go_to_system": "Vai al sistema", + "go_to_system_tooltip": "Nota: Il sistema potrebbe non essere raggiungibile a causa delle impostazioni di rete.", "uptime": "Tempo di attività (Uptime)", "last_inventory": "Ultimo inventario", "timezone": "Fuso orario", diff --git a/frontend/src/views/SystemDetailView.vue b/frontend/src/views/SystemDetailView.vue index 49d89b4f..47571509 100644 --- a/frontend/src/views/SystemDetailView.vue +++ b/frontend/src/views/SystemDetailView.vue @@ -19,21 +19,22 @@ import { useTabs } from '@/composables/useTabs' import { useI18n } from 'vue-i18n' import SystemOverviewPanel from '@/components/systems/SystemOverviewPanel.vue' import { useLatestInventory } from '@/queries/systems/latestInventory' +import { computed } from 'vue' const { t } = useI18n() const { state: systemDetail } = useSystemDetail() const { state: latestInventory } = useLatestInventory() const { tabs, selectedTab } = useTabs([{ name: 'overview', label: t('system_detail.overview') }]) -const canOpenSystem = () => { - return ['ns8', 'nsec'].includes(systemDetail.value.data?.type || '') -} - -const getSystemUrl = () => { +const systemUrl = computed(() => { if (!systemDetail.value.data?.fqdn) { return '' } + if (!['ns8', 'nsec'].includes(systemDetail.value.data?.type || '')) { + return '' + } + const fqdn = systemDetail.value.data.fqdn let port = '' let path = '' @@ -45,13 +46,11 @@ const getSystemUrl = () => { } const url = `https://${fqdn}${port}${path}` return url -} +}) const openSystem = () => { - const url = getSystemUrl() - - if (url) { - window.open(url, '_blank') + if (systemUrl.value) { + window.open(systemUrl.value, '_blank') } } @@ -82,15 +81,19 @@ const openSystem = () => {
From afa97edf8db651a6a91df37be83f6538ac763f3a Mon Sep 17 00:00:00 2001 From: Andrea Leardini Date: Fri, 9 Jan 2026 17:22:35 +0100 Subject: [PATCH 10/35] add suspend/reactivate user action --- .../components/users/ReactivateUserModal.vue | 88 +++++++++++ .../src/components/users/SuspendUserModal.vue | 89 ++++++++++++ frontend/src/components/users/UsersTable.vue | 137 ++++++++++++++---- frontend/src/i18n/en/translation.json | 19 ++- frontend/src/lib/users.ts | 25 ++++ 5 files changed, 331 insertions(+), 27 deletions(-) create mode 100644 frontend/src/components/users/ReactivateUserModal.vue create mode 100644 frontend/src/components/users/SuspendUserModal.vue diff --git a/frontend/src/components/users/ReactivateUserModal.vue b/frontend/src/components/users/ReactivateUserModal.vue new file mode 100644 index 00000000..ffeac132 --- /dev/null +++ b/frontend/src/components/users/ReactivateUserModal.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/frontend/src/components/users/SuspendUserModal.vue b/frontend/src/components/users/SuspendUserModal.vue new file mode 100644 index 00000000..82b34449 --- /dev/null +++ b/frontend/src/components/users/SuspendUserModal.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/frontend/src/components/users/UsersTable.vue b/frontend/src/components/users/UsersTable.vue index adcd6daa..2eb27fce 100644 --- a/frontend/src/components/users/UsersTable.vue +++ b/frontend/src/components/users/UsersTable.vue @@ -13,6 +13,10 @@ import { faTrash, faKey, faUserSecret, + faCirclePause, + faCirclePlay, + faCircleXmark, + faCircleCheck, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { @@ -33,6 +37,7 @@ import { NeSortDropdown, NeBadge, sortByProperty, + type NeDropdownItem, } from '@nethesis/vue-components' import { computed, ref, watch } from 'vue' import CreateOrEditUserDrawer from './CreateOrEditUserDrawer.vue' @@ -46,6 +51,8 @@ import { canManageUsers, canImpersonateUsers } from '@/lib/permissions' import { useLoginStore } from '@/stores/login' import ImpersonateUserModal from './ImpersonateUserModal.vue' import { normalize } from '@/lib/common' +import SuspendUserModal from './SuspendUserModal.vue' +import ReactivateUserModal from './ReactivateUserModal.vue' const { isShownCreateUserDrawer = false } = defineProps<{ isShownCreateUserDrawer: boolean @@ -73,6 +80,8 @@ const isShownDeleteUserModal = ref(false) const isShownResetPasswordModal = ref(false) const isShownPasswordChangedModal = ref(false) const isShownImpersonateUserModal = ref(false) +const isShownSuspendUserModal = ref(false) +const isShownReactivateUserModal = ref(false) const newPassword = ref('') const isImpersonating = ref(false) @@ -118,6 +127,16 @@ function showResetPasswordModal(user: User) { isShownResetPasswordModal.value = true } +function showSuspendUserModal(user: User) { + currentUser.value = user + isShownSuspendUserModal.value = true +} + +function showReactivateUserModal(user: User) { + currentUser.value = user + isShownReactivateUserModal.value = true +} + function showImpersonateUserModal(user: User) { currentUser.value = user isShownImpersonateUserModal.value = true @@ -134,36 +153,67 @@ function onCloseDrawer() { } function getKebabMenuItems(user: User) { - const items = [ - { - id: 'resetPassword', - label: t('users.reset_password'), - icon: faKey, - action: () => showResetPasswordModal(user), - disabled: asyncStatus.value === 'loading', - }, - { - id: 'deleteAccount', - label: t('common.delete'), - icon: faTrash, - danger: true, - action: () => showDeleteUserModal(user), - disabled: asyncStatus.value === 'loading', - }, - ] + let items: NeDropdownItem[] = [] // Add impersonate option for owners, but not for self if (canImpersonateUsers() && user.id !== loginStore.userInfo?.id) { - items.unshift({ - id: 'impersonate', - label: t('users.impersonate_user'), - icon: faUserSecret, - action: () => showImpersonateUserModal(user), - disabled: - asyncStatus.value === 'loading' || isImpersonating.value || !user.can_be_impersonated, - }) + items = [ + ...items, + { + id: 'impersonate', + label: t('users.impersonate_user'), + icon: faUserSecret, + action: () => showImpersonateUserModal(user), + disabled: + asyncStatus.value === 'loading' || isImpersonating.value || !user.can_be_impersonated, + }, + ] } + if (canManageUsers()) { + if (user.suspended_at) { + items = [ + ...items, + { + id: 'reactivateUser', + label: t('users.reactivate'), + icon: faCirclePlay, + action: () => showReactivateUserModal(user), + disabled: asyncStatus.value === 'loading', + }, + ] + } else { + items = [ + ...items, + { + id: 'suspendUser', + label: t('users.suspend'), + icon: faCirclePause, + action: () => showSuspendUserModal(user), + disabled: asyncStatus.value === 'loading', + }, + ] + } + + items = [ + ...items, + { + id: 'resetPassword', + label: t('users.reset_password'), + icon: faKey, + action: () => showResetPasswordModal(user), + disabled: asyncStatus.value === 'loading', + }, + { + id: 'deleteAccount', + label: t('common.delete'), + icon: faTrash, + danger: true, + action: () => showDeleteUserModal(user), + disabled: asyncStatus.value === 'loading', + }, + ] + } return items } @@ -249,6 +299,7 @@ const onClosePasswordChangedModal = () => { $t('users.organization') }} {{ $t('users.roles') }} + {{ $t('common.status') }} @@ -316,6 +367,30 @@ const onClosePasswordChangedModal = () => { >
+ +
+ + +
+
{ :user="currentUser" @close="isShownDeleteUserModal = false" /> + + + + { }, ) } + +export const suspendUser = (user: User) => { + const loginStore = useLoginStore() + + return axios.patch( + `${API_URL}/users/${user.id}/suspend`, + {}, + { + headers: { Authorization: `Bearer ${loginStore.jwtToken}` }, + }, + ) +} + +export const reactivateUser = (user: User) => { + const loginStore = useLoginStore() + + return axios.patch( + `${API_URL}/users/${user.id}/reactivate`, + {}, + { + headers: { Authorization: `Bearer ${loginStore.jwtToken}` }, + }, + ) +} From ca2e22e24422dcb13ca784db9d992463eb89a35f Mon Sep 17 00:00:00 2001 From: Andrea Leardini Date: Thu, 15 Jan 2026 11:02:54 +0100 Subject: [PATCH 11/35] improve users and roles --- frontend/src/components/LoggedUserCard.vue | 12 ++-- frontend/src/components/UserRoleBadge.vue | 41 +++++++++++++ .../src/components/account/ProfilePanel.vue | 12 ++-- .../src/components/systems/SystemInfoCard.vue | 3 +- .../components/systems/SystemNotesModal.vue | 1 - .../src/components/systems/SystemsTable.vue | 2 - frontend/src/components/users/UsersTable.vue | 39 +++++++++---- frontend/src/i18n/en/translation.json | 5 +- frontend/src/lib/users.ts | 41 ++++++++++--- frontend/src/views/DashboardView.vue | 19 ++----- frontend/src/views/SystemsView.vue | 2 +- frontend/src/views/UsersView.vue | 57 +++++++++++++++++++ 12 files changed, 179 insertions(+), 55 deletions(-) create mode 100644 frontend/src/components/UserRoleBadge.vue diff --git a/frontend/src/components/LoggedUserCard.vue b/frontend/src/components/LoggedUserCard.vue index 80767b4a..d5033981 100644 --- a/frontend/src/components/LoggedUserCard.vue +++ b/frontend/src/components/LoggedUserCard.vue @@ -4,12 +4,12 @@ --> @@ -35,13 +35,11 @@ const loginStore = useLoginStore()
{{ loginStore.userInfo.name }}
- - {{ $t(`user_roles.${normalize(role)}`) }} - + :role="role" + />
diff --git a/frontend/src/components/UserRoleBadge.vue b/frontend/src/components/UserRoleBadge.vue new file mode 100644 index 00000000..fb8a86d9 --- /dev/null +++ b/frontend/src/components/UserRoleBadge.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/components/account/ProfilePanel.vue b/frontend/src/components/account/ProfilePanel.vue index 8a667034..4dda7782 100644 --- a/frontend/src/components/account/ProfilePanel.vue +++ b/frontend/src/components/account/ProfilePanel.vue @@ -9,7 +9,6 @@ import { getValidationIssues, isValidationError } from '@/lib/validation' import { useLoginStore } from '@/stores/login' import { useNotificationsStore } from '@/stores/notifications' import { - NeBadge, NeButton, NeFormItemLabel, NeInlineNotification, @@ -22,7 +21,7 @@ import { ref, useTemplateRef, watch, type ShallowRef } from 'vue' import { useI18n } from 'vue-i18n' import * as v from 'valibot' import { USERS_KEY } from '@/lib/users' -import { normalize } from '@/lib/common' +import UserRoleBadge from '../UserRoleBadge.vue' const { t } = useI18n() const loginStore = useLoginStore() @@ -171,14 +170,11 @@ function validate(profile: ProfileInfo): boolean { {{ $t('users.roles') }}
- + :role="role" + />
diff --git a/frontend/src/components/systems/SystemInfoCard.vue b/frontend/src/components/systems/SystemInfoCard.vue index b7b033ee..caf5cd4b 100644 --- a/frontend/src/components/systems/SystemInfoCard.vue +++ b/frontend/src/components/systems/SystemInfoCard.vue @@ -25,10 +25,9 @@ import { canManageSystems } from '@/lib/permissions' import { faFileCsv, faFilePdf, faPenToSquare } from '@fortawesome/free-solid-svg-icons' import { useI18n } from 'vue-i18n' import CreateOrEditSystemDrawer from './CreateOrEditSystemDrawer.vue' -import router from '@/router' const { t } = useI18n() -const { state: systemDetail, state, asyncStatus } = useSystemDetail() +const { state: systemDetail, asyncStatus } = useSystemDetail() const isNotesModalShown = ref(false) const isShownCreateOrEditSystemDrawer = ref(false) diff --git a/frontend/src/components/systems/SystemNotesModal.vue b/frontend/src/components/systems/SystemNotesModal.vue index 4b5bbebe..1bc689c3 100644 --- a/frontend/src/components/systems/SystemNotesModal.vue +++ b/frontend/src/components/systems/SystemNotesModal.vue @@ -5,7 +5,6 @@