diff --git a/docs/en/05-system-registration.md b/docs/en/05-system-registration.md index 8b6e2e03..aad909bc 100644 --- a/docs/en/05-system-registration.md +++ b/docs/en/05-system-registration.md @@ -323,7 +323,7 @@ MY_SYSTEM_SECRET=my_a1b2c3d4e5f6g7h8i9j0.k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d Administrators can view registration status: 1. Navigate to **Systems** -2. Find the system and click **View details** +2. Find the system and click **View** 3. Check fields: - **System_key**: Now visible (was hidden before) - **Subscription**: Shows timestamp diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a3cf78f8..5bc7c692 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,8 +15,9 @@ "@fortawesome/vue-fontawesome": "^3.0.8", "@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", + "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", + "@nethesis/vue-components": "^3.5.0", + "@pinia/colada": "^0.21.0", "@tailwindcss/vite": "^4.1.10", "@vueuse/core": "^13.4.0", "axios": "^1.11.0", @@ -30,7 +31,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 +123,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 +701,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -725,7 +724,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1345,7 +1343,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" }, @@ -1650,10 +1647,22 @@ "node": ">=6" } }, + "node_modules/@nethesis/nethesis-solid-svg-icons": { + "version": "6.2.1", + "resolved": "git+ssh://git@github.com/nethesis/Font-Awesome.git#16419000ca62bc35db676d033ef00b8ef9771024", + "hasInstallScript": true, + "license": "UNLICENSED", + "dependencies": { + "@fortawesome/fontawesome-common-types": "^6.7.2" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@nethesis/vue-components": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@nethesis/vue-components/-/vue-components-3.4.0.tgz", - "integrity": "sha512-+ST793nRmJS59l0jq7BpJCXLQDGPn3W9BGumnW4LO37jvBo9V19igjXoq9Z/pza8qNtHc/hWghtS/jW22W7M2w==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@nethesis/vue-components/-/vue-components-3.5.0.tgz", + "integrity": "sha512-8vDml217yybrkOR1bgYQarJ2p/I1dZ+JzGaC+vRaoO7EWmiKxo20ZyQ284qb1kemyIBavL9S12/F9BR00WndYA==", "dependencies": { "@fontsource/poppins": "^5.2.6", "@fortawesome/fontawesome-svg-core": "^6.5.1", @@ -1804,13 +1813,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 +1826,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 +2619,6 @@ "integrity": "sha512-UJdblFqXymSBhmZf96BnbisoFIr8ooiiBRMolQgg77Ea+VM37jXw76C2LQr9n8wm9+i/OvlUlW6xSvqwzwqznw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2670,7 +2675,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 +3202,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 +3272,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 +3451,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3642,9 +3612,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 +3670,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.2", "caniuse-lite": "^1.0.30001741", @@ -3996,7 +3965,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 +4288,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 +4349,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -4430,7 +4396,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 +6385,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 +6422,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 +6535,6 @@ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6799,7 +6756,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 +7173,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 +7272,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 +7431,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7632,7 +7585,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 +7827,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 +7840,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 +7932,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..42e6d575 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,8 +25,9 @@ "@fortawesome/vue-fontawesome": "^3.0.8", "@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", + "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", + "@nethesis/vue-components": "^3.5.0", + "@pinia/colada": "^0.21.0", "@tailwindcss/vite": "^4.1.10", "@vueuse/core": "^13.4.0", "axios": "^1.11.0", @@ -40,7 +41,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", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 98c43e10..f5f0e4e5 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -12,7 +12,7 @@ import { useTitle } from '@vueuse/core' import { PRODUCT_NAME } from './lib/config' import { useI18n } from 'vue-i18n' import ToastNotificationsArea from '@/components/ToastNotificationsArea.vue' -import { PiniaColadaDevtools } from '@pinia/colada-devtools' +import { PiniaColadaProdDevtools } from '@pinia/colada-devtools' import { configureAxios } from './lib/axios' const themeStore = useThemeStore() @@ -55,7 +55,8 @@ onMounted(() => { - + + diff --git a/frontend/src/assets/application_logos/crowdsec.svg b/frontend/src/assets/application_logos/crowdsec.svg new file mode 100644 index 00000000..eca34adb --- /dev/null +++ b/frontend/src/assets/application_logos/crowdsec.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/application_logos/dnsmask.svg b/frontend/src/assets/application_logos/dnsmask.svg new file mode 100644 index 00000000..9b14d028 --- /dev/null +++ b/frontend/src/assets/application_logos/dnsmask.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/src/assets/application_logos/ejabberd.svg b/frontend/src/assets/application_logos/ejabberd.svg new file mode 100644 index 00000000..a516a354 --- /dev/null +++ b/frontend/src/assets/application_logos/ejabberd.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/src/assets/application_logos/grafana.svg b/frontend/src/assets/application_logos/grafana.svg new file mode 100644 index 00000000..200a188a --- /dev/null +++ b/frontend/src/assets/application_logos/grafana.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/assets/application_logos/imapsync.svg b/frontend/src/assets/application_logos/imapsync.svg new file mode 100644 index 00000000..107b7d03 --- /dev/null +++ b/frontend/src/assets/application_logos/imapsync.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/application_logos/mail-piler.svg b/frontend/src/assets/application_logos/mail-piler.svg new file mode 100644 index 00000000..2e4a3f89 --- /dev/null +++ b/frontend/src/assets/application_logos/mail-piler.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/application_logos/mail.svg b/frontend/src/assets/application_logos/mail.svg new file mode 100644 index 00000000..a770c31c --- /dev/null +++ b/frontend/src/assets/application_logos/mail.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/src/assets/application_logos/matrix.svg b/frontend/src/assets/application_logos/matrix.svg new file mode 100644 index 00000000..4e132c9b --- /dev/null +++ b/frontend/src/assets/application_logos/matrix.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/src/assets/application_logos/mattermost.svg b/frontend/src/assets/application_logos/mattermost.svg new file mode 100644 index 00000000..78ba5e62 --- /dev/null +++ b/frontend/src/assets/application_logos/mattermost.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/assets/application_logos/netdata.svg b/frontend/src/assets/application_logos/netdata.svg new file mode 100644 index 00000000..d47b0664 --- /dev/null +++ b/frontend/src/assets/application_logos/netdata.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/application_logos/nethsecurity-controller.svg b/frontend/src/assets/application_logos/nethsecurity-controller.svg new file mode 100644 index 00000000..ef32906c --- /dev/null +++ b/frontend/src/assets/application_logos/nethsecurity-controller.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/assets/application_logos/nethvoice-proxy.svg b/frontend/src/assets/application_logos/nethvoice-proxy.svg new file mode 100644 index 00000000..7eb789c3 --- /dev/null +++ b/frontend/src/assets/application_logos/nethvoice-proxy.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/assets/application_logos/nethvoice.svg b/frontend/src/assets/application_logos/nethvoice.svg new file mode 100644 index 00000000..6f1cdc3d --- /dev/null +++ b/frontend/src/assets/application_logos/nethvoice.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/assets/application_logos/nextcloud.svg b/frontend/src/assets/application_logos/nextcloud.svg new file mode 100644 index 00000000..8b68c33d --- /dev/null +++ b/frontend/src/assets/application_logos/nextcloud.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/assets/application_logos/samba.svg b/frontend/src/assets/application_logos/samba.svg new file mode 100644 index 00000000..0b9501c7 --- /dev/null +++ b/frontend/src/assets/application_logos/samba.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/application_logos/webtop.svg b/frontend/src/assets/application_logos/webtop.svg new file mode 100644 index 00000000..e46449ae --- /dev/null +++ b/frontend/src/assets/application_logos/webtop.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/assets/system_logos/nethsecurity.svg b/frontend/src/assets/system_logos/nethsecurity.svg new file mode 100644 index 00000000..ce5cf9cb --- /dev/null +++ b/frontend/src/assets/system_logos/nethsecurity.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/assets/system_logos/nethserver.svg b/frontend/src/assets/system_logos/nethserver.svg new file mode 100644 index 00000000..20a94141 --- /dev/null +++ b/frontend/src/assets/system_logos/nethserver.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/components/CounterCard.vue b/frontend/src/components/CounterCard.vue index be503758..9cc7b9bd 100644 --- a/frontend/src/components/CounterCard.vue +++ b/frontend/src/components/CounterCard.vue @@ -7,6 +7,7 @@ import { NeCard, NeHeading, NeSkeleton } from '@nethesis/vue-components' import { type IconDefinition } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' +import { computed, useSlots } from 'vue' const { title, @@ -21,21 +22,34 @@ const { loading?: boolean skeletonLines?: number }>() + +const slots = useSlots() + +const hasDefaultSlot = computed(() => !!slots.default) 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/SideMenu.vue b/frontend/src/components/SideMenu.vue index e1e0be2a..c9910273 100644 --- a/frontend/src/components/SideMenu.vue +++ b/frontend/src/components/SideMenu.vue @@ -22,6 +22,7 @@ import { faUserGroup as fasUserGroup, faServer as fasServer, } from '@fortawesome/free-solid-svg-icons' +import { faGridOne as fasGridOne } from '@nethesis/nethesis-solid-svg-icons' import { faHouse as falHouse, faGlobe as falGlobe, @@ -29,8 +30,10 @@ import { faBuilding as falBuilding, faUserGroup as falUserGroup, faServer as falServer, + faGrid2 as falGrid2, } from '@nethesis/nethesis-light-svg-icons' import { + canReadApplications, canReadCustomers, canReadDistributors, canReadResellers, @@ -70,6 +73,15 @@ const navigation = computed(() => { }) } + if (canReadApplications()) { + menuItems.push({ + name: 'applications.title', + to: 'applications', + solidIcon: fasGridOne, + lightIcon: falGrid2, + }) + } + if (canReadDistributors()) { menuItems.push({ name: 'distributors.title', diff --git a/frontend/src/components/TopBar.vue b/frontend/src/components/TopBar.vue index b4db600f..c8d6d070 100644 --- a/frontend/src/components/TopBar.vue +++ b/frontend/src/components/TopBar.vue @@ -12,6 +12,7 @@ import { faBars, faBell, faChevronDown, + faCircleQuestion, faCircleUser, faMoon, faRightFromBracket, @@ -109,10 +110,10 @@ function openNotificationsDrawer() { /> - + 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/applications/ApplicationsTable.vue b/frontend/src/components/applications/ApplicationsTable.vue new file mode 100644 index 00000000..1333d241 --- /dev/null +++ b/frontend/src/components/applications/ApplicationsTable.vue @@ -0,0 +1,520 @@ + + + + + diff --git a/frontend/src/components/applications/AssignOrganizationDrawer.vue b/frontend/src/components/applications/AssignOrganizationDrawer.vue new file mode 100644 index 00000000..6121b401 --- /dev/null +++ b/frontend/src/components/applications/AssignOrganizationDrawer.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/frontend/src/components/applications/SetNotesDrawer.vue b/frontend/src/components/applications/SetNotesDrawer.vue new file mode 100644 index 00000000..48c4fb53 --- /dev/null +++ b/frontend/src/components/applications/SetNotesDrawer.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/frontend/src/components/customers/CreateOrEditCustomerDrawer.vue b/frontend/src/components/customers/CreateOrEditCustomerDrawer.vue index 2c5f6aad..96d75595 100644 --- a/frontend/src/components/customers/CreateOrEditCustomerDrawer.vue +++ b/frontend/src/components/customers/CreateOrEditCustomerDrawer.vue @@ -225,12 +225,12 @@ async function saveCustomer() { }, } - if (currentCustomer?.id) { + if (currentCustomer?.logto_id) { // editing customer const customerToEdit: Customer = { ...customer, - id: currentCustomer.id, + logto_id: currentCustomer.logto_id, } const isValidationOk = validateEdit(customerToEdit) diff --git a/frontend/src/components/customers/CustomersTable.vue b/frontend/src/components/customers/CustomersTable.vue index 5b38bbb8..0bfd31cb 100644 --- a/frontend/src/components/customers/CustomersTable.vue +++ b/frontend/src/components/customers/CustomersTable.vue @@ -68,6 +68,20 @@ const pagination = computed(() => { return state.value.data?.pagination }) +const isNoDataEmptyStateShown = computed(() => { + return ( + !customersPage.value?.length && !debouncedTextFilter.value && state.value.status === 'success' + ) +}) + +const isNoMatchEmptyStateShown = computed(() => { + return !customersPage.value?.length && !!debouncedTextFilter.value +}) + +const noEmptyStateShown = computed(() => { + return !isNoDataEmptyStateShown.value && !isNoMatchEmptyStateShown.value +}) + watch( () => isShownCreateCustomerDrawer, () => { @@ -131,156 +145,150 @@ const onSort = (payload: SortEvent) => { :description="state.error.message" class="mb-6" /> - -
-
- -
- - - -
- -
- -
- {{ $t('common.updating') }} + + + + + + {{ $t('customers.create_customer') }} + + + + + + + diff --git a/frontend/src/components/distributors/CreateOrEditDistributorDrawer.vue b/frontend/src/components/distributors/CreateOrEditDistributorDrawer.vue index 1e7771b9..559bfc28 100644 --- a/frontend/src/components/distributors/CreateOrEditDistributorDrawer.vue +++ b/frontend/src/components/distributors/CreateOrEditDistributorDrawer.vue @@ -227,12 +227,12 @@ async function saveDistributor() { }, } - if (currentDistributor?.id) { + if (currentDistributor?.logto_id) { // editing distributor const distributorToEdit: Distributor = { ...distributor, - id: currentDistributor.id, + logto_id: currentDistributor.logto_id, } const isValidationOk = validateEdit(distributorToEdit) diff --git a/frontend/src/components/distributors/DistributorsTable.vue b/frontend/src/components/distributors/DistributorsTable.vue index 3fa113af..7cd2e314 100644 --- a/frontend/src/components/distributors/DistributorsTable.vue +++ b/frontend/src/components/distributors/DistributorsTable.vue @@ -68,6 +68,22 @@ const pagination = computed(() => { return state.value.data?.pagination }) +const isNoDataEmptyStateShown = computed(() => { + return ( + !distributorsPage.value?.length && + !debouncedTextFilter.value && + state.value.status === 'success' + ) +}) + +const isNoMatchEmptyStateShown = computed(() => { + return !distributorsPage.value?.length && !!debouncedTextFilter.value +}) + +const noEmptyStateShown = computed(() => { + return !isNoDataEmptyStateShown.value && !isNoMatchEmptyStateShown.value +}) + watch( () => isShownCreateDistributorDrawer, () => { @@ -131,156 +147,150 @@ const onSort = (payload: SortEvent) => { :description="state.error.message" class="mb-6" /> - -
-
- -
- - - -
- -
- -
- {{ $t('common.updating') }} + + + + + + {{ $t('distributors.create_distributor') }} + + + { return state.value.data?.pagination }) +const isNoDataEmptyStateShown = computed(() => { + return ( + !resellersPage.value?.length && !debouncedTextFilter.value && state.value.status === 'success' + ) +}) + +const isNoMatchEmptyStateShown = computed(() => { + return !resellersPage.value?.length && !!debouncedTextFilter.value +}) + +const noEmptyStateShown = computed(() => { + return !isNoDataEmptyStateShown.value && !isNoMatchEmptyStateShown.value +}) + watch( () => isShownCreateResellerDrawer, () => { @@ -131,156 +145,150 @@ const onSort = (payload: SortEvent) => { :description="state.error.message" class="mb-6" /> - -
-
- -
- - - -
- -
- -
- {{ $t('common.updating') }} + + + + + + {{ $t('resellers.create_reseller') }} + + + { queryCache.invalidateQueries({ key: [SYSTEMS_KEY] }) queryCache.invalidateQueries({ key: [SYSTEMS_TOTAL_KEY] }) + queryCache.invalidateQueries({ key: [SYSTEM_ORGANIZATION_FILTER_KEY] }) }, }) @@ -109,7 +111,10 @@ const { console.error('Error editing system:', error) validationIssues.value = getValidationIssues(error as AxiosError, 'systems') }, - onSettled: () => queryCache.invalidateQueries({ key: [SYSTEMS_KEY] }), + onSettled: () => { + queryCache.invalidateQueries({ key: [SYSTEMS_KEY] }) + queryCache.invalidateQueries({ key: [SYSTEM_ORGANIZATION_FILTER_KEY] }) + }, }) const name = ref('') @@ -141,7 +146,7 @@ const organizationOptions = computed(() => { } return organizations.value.data?.map((org) => ({ - id: org.id, + id: org.logto_id, label: org.name, description: t(`organizations.${org.type}`), })) @@ -395,9 +400,7 @@ function copySecretAndCloseDrawer() {
{{ secret }}
-
- ************************************************************************* -
+
************************
{ queryCache.invalidateQueries({ key: [SYSTEMS_KEY] }) queryCache.invalidateQueries({ key: [SYSTEMS_TOTAL_KEY] }) + queryCache.invalidateQueries({ key: [SYSTEM_ORGANIZATION_FILTER_KEY] }) }, }) diff --git a/frontend/src/components/systems/RestoreSystemModal.vue b/frontend/src/components/systems/RestoreSystemModal.vue new file mode 100644 index 00000000..aedc65da --- /dev/null +++ b/frontend/src/components/systems/RestoreSystemModal.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/frontend/src/components/systems/SecretRegeneratedModal.vue b/frontend/src/components/systems/SecretRegeneratedModal.vue index 84d6c6b2..456e84aa 100644 --- a/frontend/src/components/systems/SecretRegeneratedModal.vue +++ b/frontend/src/components/systems/SecretRegeneratedModal.vue @@ -75,9 +75,7 @@ function onShow() {
{{ newSecret }}
-
- ************************************************************************* -
+
************************
import { NeCard, + NeDropdown, NeHeading, NeInlineNotification, NeLink, NeSkeleton, + type NeDropdownItem, } from '@nethesis/vue-components' import { useSystemDetail } from '@/queries/systems/systemDetail' -import { getProductLogo, getProductName } from '@/lib/systems/systems' +import { exportSystem, getProductLogo, getProductName } from '@/lib/systems/systems' 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' +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' -const NOTES_MAX_LENGTH = 32 - -const { state: systemDetail } = useSystemDetail() +const { t } = useI18n() +const { state: systemDetail, asyncStatus } = useSystemDetail() const isNotesModalShown = ref(false) +const isShownCreateOrEditSystemDrawer = 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 -}) +function getKebabMenuItems() { + let items: NeDropdownItem[] = [] -// 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 (canManageSystems()) { + items.push({ + id: 'editSystem', + label: t('common.edit'), + icon: faPenToSquare, + action: () => (isShownCreateOrEditSystemDrawer.value = true), + disabled: asyncStatus.value === 'loading', + }) } - if (notes.includes('\n')) { - return notes.split('\n')[0] + '...' - } - return notes -}) + + items = [ + ...items, + { + id: 'exportToPdf', + label: t('systems.export_to_pdf'), + icon: faFilePdf, + action: () => exportSystem(systemDetail.value.data!, 'pdf'), + disabled: asyncStatus.value === 'loading', + }, + { + id: 'exportToCsv', + label: t('systems.export_to_csv'), + icon: faFileCsv, + action: () => exportSystem(systemDetail.value.data!, 'csv'), + disabled: asyncStatus.value === 'loading', + }, + ] + return items +} - - - - +
+
@@ -179,5 +198,11 @@ const truncatedNotes = computed(() => { :notes="systemDetail.data?.notes" @close="isNotesModalShown = false" /> + + diff --git a/frontend/src/components/systems/SystemNotesModal.vue b/frontend/src/components/systems/SystemNotesModal.vue index 1d9432b8..1bc689c3 100644 --- a/frontend/src/components/systems/SystemNotesModal.vue +++ b/frontend/src/components/systems/SystemNotesModal.vue @@ -25,6 +25,6 @@ const emit = defineEmits(['close']) @close="emit('close')" @primary-click="emit('close')" > -
{{ notes }}
+
{{ notes }}
diff --git a/frontend/src/components/systems/SystemsTable.vue b/frontend/src/components/systems/SystemsTable.vue index 1663e63b..3b13eb34 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 { @@ -47,7 +48,7 @@ import { savePageSizeToStorage } from '@/lib/tablePageSize' import { canManageSystems } from '@/lib/permissions' import { useSystems } from '@/queries/systems/systems' import { - getExport, + exportSystem, getProductLogo, getProductName, SYSTEMS_TABLE_ID, @@ -59,13 +60,14 @@ import DeleteSystemModal from './DeleteSystemModal.vue' import { useProductFilter } from '@/queries/systems/productFilter' import { useCreatedByFilter } from '@/queries/systems/createdByFilter' import { useVersionFilter } from '@/queries/systems/versionFilter' +import { useOrganizationFilter } from '@/queries/systems/organizationFilter' import UserAvatar from '../UserAvatar.vue' import { buildVersionFilterOptions } from '@/lib/systems/versionFilter' import OrganizationIcon from '../OrganizationIcon.vue' -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 @@ -85,6 +87,7 @@ const { createdByFilter, versionFilter, statusFilter, + organizationFilter, sortBy, sortDescending, } = useSystems() @@ -92,10 +95,13 @@ const { state: productFilterState, asyncStatus: productFilterAsyncStatus } = use const { state: createdByFilterState, asyncStatus: createdByFilterAsyncStatus } = useCreatedByFilter() const { state: versionFilterState, asyncStatus: versionFilterAsyncStatus } = useVersionFilter() +const { state: organizationFilterState, asyncStatus: organizationFilterAsyncStatus } = + useOrganizationFilter() 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('') @@ -163,6 +169,31 @@ const createdByFilterOptions = computed(() => { } }) +const organizationFilterOptions = computed(() => { + if (!organizationFilterState.value.data || !organizationFilterState.value.data.organizations) { + return [] + } else { + return organizationFilterState.value.data.organizations.map((org) => ({ + id: org.id, + label: org.name, + })) + } +}) + +const isNoDataEmptyStateShown = computed(() => { + return ( + !systemsPage.value?.length && !debouncedTextFilter.value && state.value.status === 'success' + ) +}) + +const isNoMatchEmptyStateShown = computed(() => { + return !systemsPage.value?.length && !!debouncedTextFilter.value +}) + +const noEmptyStateShown = computed(() => { + return !isNoDataEmptyStateShown.value && !isNoMatchEmptyStateShown.value +}) + watch( () => isShownCreateSystemDrawer, () => { @@ -204,6 +235,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 @@ -217,7 +253,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'), @@ -234,18 +270,18 @@ 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', }, ] - if (canManageSystems()) { + if (canManageSystems() && system.status !== 'deleted') { items = [ ...items, { @@ -265,6 +301,17 @@ function getKebabMenuItems(system: System) { }, ] } + + if (canManageSystems() && system.status === 'deleted') { + items.push({ + id: 'restoreSystem', + label: t('common.restore'), + icon: faRotateLeft, + action: () => showRestoreSystemModal(system), + disabled: asyncStatus.value === 'loading', + }) + } + return items } @@ -277,17 +324,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 @@ -309,113 +345,9 @@ function onCloseSecretRegeneratedModal() { :description="state.error.message" class="mb-6" /> - -
-
- -
- - - - - - - - - - {{ t('systems.reset_filters') }} - -
- -
- -
- {{ $t('common.updating') }} -
-
-
-
- - - - {{ $t('systems.reset_filters') }} - - - - - {{ - $t('systems.name') - }} - {{ - $t('systems.version') - }} - {{ - $t('systems.fqdn_ip_address') - }} - {{ - $t('systems.organization') - }} - {{ - $t('systems.created_by') - }} - {{ - $t('systems.status') - }} - - - - - - - -
- -
- - - {{ item.name || '-' }} - -
-
-
-
- -
- {{ item.version || '-' }} -
-
- + +
+
+ +
+ + + + + + + + + + + {{ t('systems.reset_filters') }} + +
+ +
-
- - -
- {{ item.ipv6_address }} -
-
-
+ +
+ {{ $t('common.updating') }}
- - -
-
- - - - - {{ item.organization.name || '-' }} +
+
+
+ + + + {{ $t('systems.reset_filters') }} + + + + + {{ + $t('systems.name') + }} + {{ + $t('systems.version') + }} + {{ + $t('systems.fqdn_ip_address') + }} + {{ + $t('systems.organization') + }} + {{ + $t('systems.created_by') + }} + {{ + $t('systems.status') + }} + + + + + + + +
+ +
+ + + {{ item.name || '-' }} + +
+
-
- - -
- + + { queryCache.invalidateQueries({ key: [USERS_KEY] }) queryCache.invalidateQueries({ key: [USERS_TOTAL_KEY] }) + queryCache.invalidateQueries({ key: [SYSTEM_ORGANIZATION_FILTER_KEY] }) }, }) @@ -120,7 +124,10 @@ const { console.error('Error editing user:', error) validationIssues.value = getValidationIssues(error as AxiosError, 'users') }, - onSettled: () => queryCache.invalidateQueries({ key: [USERS_KEY] }), + onSettled: () => { + queryCache.invalidateQueries({ key: [USERS_KEY] }) + // queryCache.invalidateQueries({ key: [ORGANIZATION_FILTER_KEY] }) //// + }, }) const email = ref('') @@ -153,7 +160,7 @@ const organizationOptions = computed(() => { } return organizations.value.data?.map((org) => ({ - id: org.id, + id: org.logto_id, label: org.name, description: t(`organizations.${org.type}`), })) 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..d65fa348 100644 --- a/frontend/src/components/users/UsersTable.vue +++ b/frontend/src/components/users/UsersTable.vue @@ -13,6 +13,9 @@ import { faTrash, faKey, faUserSecret, + faCirclePause, + faCirclePlay, + faCircleCheck, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { @@ -31,8 +34,9 @@ import { NeDropdown, type SortEvent, NeSortDropdown, - NeBadge, sortByProperty, + type NeDropdownItem, + NeTooltip, } from '@nethesis/vue-components' import { computed, ref, watch } from 'vue' import CreateOrEditUserDrawer from './CreateOrEditUserDrawer.vue' @@ -45,7 +49,11 @@ import { useUsers } from '@/queries/users' 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' +import OrganizationIcon from '../OrganizationIcon.vue' +import UserRoleBadge from '../UserRoleBadge.vue' +// import { useOrganizationFilter } from '@/queries/systems/organizationFilter' //// const { isShownCreateUserDrawer = false } = defineProps<{ isShownCreateUserDrawer: boolean @@ -64,8 +72,10 @@ const { sortBy, sortDescending, } = useUsers() - const loginStore = useLoginStore() +// const { state: organizationFilterState } = useOrganizationFilter() //// +// const { state: userRoleFilterState, asyncStatus: userRoleFilterAsyncStatus } = +// useUserRoleFilter() //// const currentUser = ref() const isShownCreateOrEditUserDrawer = ref(false) @@ -73,6 +83,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) @@ -84,6 +96,43 @@ const pagination = computed(() => { return state.value.data?.pagination }) +const isNoDataEmptyStateShown = computed(() => { + return !usersPage.value?.length && !debouncedTextFilter.value && state.value.status === 'success' +}) + +const isNoMatchEmptyStateShown = computed(() => { + return !usersPage.value?.length && !!debouncedTextFilter.value +}) + +const noEmptyStateShown = computed(() => { + return !isNoDataEmptyStateShown.value && !isNoMatchEmptyStateShown.value +}) + +//// +// const organizationFilterOptions = computed(() => { +// if (!organizationFilterState.value.data || !organizationFilterState.value.data.organizations) { +// return [] +// } else { +// return organizationFilterState.value.data.organizations.map((org) => ({ +// id: org.id, +// label: org.name, +// })) +// } +// }) + +//// +// const userRoleOptions = computed(() => { +// if (!allUserRoles.value.data) { +// return [] +// } + +// return allUserRoles.value.data?.map((role) => ({ +// id: role.id, +// label: t(`user_roles.${normalize(role.name)}`), +// description: t(`user_roles.${normalize(role.name)}_description`), +// })) +// }) + watch( () => isShownCreateUserDrawer, () => { @@ -118,6 +167,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 +193,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 } @@ -188,177 +278,228 @@ const onClosePasswordChangedModal = () => { :description="state.error.message" class="mb-6" /> - -
-
- -
- - - -
- -
- -
- {{ $t('common.updating') }} + + + + + + {{ $t('users.create_user') }} + + +