@cryptotaxi247 / CoPilot / commits / e4d11400

General refactor (#884)

* feat(CardEntity): Add prop to hide footer extra * refactor(customers): Refine components and utils * refactor(ui): Improve component display * refactor(agents): Refine card UI and CSV export * refactor(agents): Standardize overview UI * refactor(agents): Restructure agent toolbar * refactor(agents): Improve responsive layout * chore(deps): Update dependencies * refactor(agents): Rewrite AgentCard component * refactor(ui): Style tab button numbers * refactor(agents): Enhance agent UI presentation * refactor(ui): Standardize tab pane and card styles * refactor(ui): Standardize padding and spacing across various components * refactor(ui): Enhance CardEntity component with conditional slot visibility - Updated CardEntity.vue to include new props for controlling the visibility of header, footer, and extra slots based on conditions. - Modified CaseTaskItem.vue to utilize the new props for better task data handling. - Refactored CaseTasksList.vue to streamline task grouping and presentation using CardEntity. * refactor(case-templates): Convert list to tabbed view * refactor(templates): Refine UI, data, and date formatting * refactor(templates): Unify components styling * lint * refactor(threat-intel): Refine UI --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed May 20, 2026 at 08:32 UTC e4d1140042cba7990dac44acae606900a576887f
74 files changed +1463 -1886
frontend/package.json
+8 -6
@@ -50,7 +50,7 @@
50 "@microsoft/fetch-event-source": "^2.0.1",
51 "@mui/material": "^9.0.1",
52 "@shikijs/markdown-it": "^4.0.2",
53 - "@shuffleio/shuffle-mcps": "^0.0.11",
53 + "@shuffleio/shuffle-mcps": "^0.0.13",
54 "@types/codemirror": "^5.60.17",
55 "@vueuse/core": "^14.3.0",
56 "@vueuse/motion": "^3.0.3",
@@ -72,6 +72,7 @@
72 "mitt": "^3.0.1",
73 "naive-ui": "^2.44.1",
74 "nanoid": "^5.1.11",
75 + "papaparse": "^5.5.3",
76 "password-validator": "^5.3.0",
77 "pinia": "^3.0.4",
78 "pinia-plugin-persistedstate": "^4.7.1",
@@ -88,7 +89,7 @@
89 "vue-advanced-cropper": "^2.8.9",
90 "vue-codemirror": "^6.1.1",
91 "vue-highlight-words": "^3.0.1",
91 - "vue-i18n": "^11.4.2",
92 + "vue-i18n": "^11.4.4",
93 "vue-router": "^5.0.7",
94 "vue-sjv": "^0.0.6",
95 "vue3-apexcharts": "^1.11.1",
@@ -114,7 +115,8 @@
115 "@types/jsdom": "^28.0.3",
116 "@types/lodash": "^4.17.24",
117 "@types/markdown-it": "^14.1.2",
117 - "@types/node": "^25.8.0",
118 + "@types/node": "^25.9.0",
119 + "@types/papaparse": "^5.5.2",
120 "@types/react": "^19.2.14",
121 "@types/react-dom": "^19.2.3",
122 "@types/validator": "^13.15.10",
@@ -122,12 +124,12 @@
124 "@vitejs/plugin-vue-jsx": "^5.1.5",
125 "@vue/test-utils": "^2.4.10",
126 "@vue/tsconfig": "~0.9.1",
125 - "baseline-browser-mapping": "^2.10.29",
127 + "baseline-browser-mapping": "^2.10.31",
128 "depcheck": "^1.4.7",
129 "eslint": "~10.4.0",
130 "flourite": "^1.3.0",
131 "fs-extra": "^11.3.5",
130 - "jscpd": "^4.2.2",
132 + "jscpd": "^4.2.3",
133 "jsdom": "^29.1.1",
134 "npm-run-all2": "^8.0.4",
135 "prettier": "^3.8.3",
@@ -145,6 +147,6 @@
147 "vite-plugin-vue-devtools": "^8.1.2",
148 "vite-svg-loader": "^5.1.1",
149 "vitest": "^4.1.6",
148 - "vue-tsc": "^3.2.9"
150 + "vue-tsc": "^3.3.0"
151 }
152 }
\ No newline at end of file
frontend/pnpm-lock.yaml
+184 -175
@@ -57,8 +57,8 @@ importers:
57 specifier: ^4.0.2
58 version: 4.0.2
59 '@shuffleio/shuffle-mcps':
60 - specifier: ^0.0.11
61 - version: 0.0.11(eeed91f181808c3b62bfac5431c1b580)
60 + specifier: ^0.0.13
61 + version: 0.0.13(@emotion/is-prop-valid@1.4.0)(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@mui/material@9.0.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/react@19.2.14)(lucide-react@1.16.0(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react-router-dom@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)
62 '@types/codemirror':
63 specifier: ^5.60.17
64 version: 5.60.17
@@ -122,6 +122,9 @@ importers:
122 nanoid:
123 specifier: ^5.1.11
124 version: 5.1.11
125 + papaparse:
126 + specifier: ^5.5.3
127 + version: 5.5.3
128 password-validator:
129 specifier: ^5.3.0
130 version: 5.3.0
@@ -171,8 +174,8 @@ importers:
174 specifier: ^3.0.1
175 version: 3.0.1(vue@3.5.34(typescript@6.0.3))
176 vue-i18n:
174 - specifier: ^11.4.2
175 - version: 11.4.2(vue@3.5.34(typescript@6.0.3))
177 + specifier: ^11.4.4
178 + version: 11.4.4(vue@3.5.34(typescript@6.0.3))
179 vue-router:
180 specifier: ^5.0.7
181 version: 5.0.7(@vue/compiler-sfc@3.5.34)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))
@@ -193,11 +196,11 @@ importers:
196 version: 0.1.1
197 xmllint-wasm:
198 specifier: ^5.2.0
196 - version: 5.2.0(@types/node@25.8.0)
199 + version: 5.2.0(@types/node@25.9.0)
200 devDependencies:
201 '@antfu/eslint-config':
202 specifier: ~9.0.0
200 - version: 9.0.0(@typescript-eslint/rule-tester@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(@typescript-eslint/typescript-estree@8.59.3(typescript@6.0.3))(@typescript-eslint/utils@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.34)(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.6(@types/node@25.8.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)))
203 + version: 9.0.0(@typescript-eslint/rule-tester@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(@typescript-eslint/typescript-estree@8.59.3(typescript@6.0.3))(@typescript-eslint/utils@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.34)(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.6(@types/node@25.9.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)))
204 '@clack/prompts':
205 specifier: ^1.4.0
206 version: 1.4.0
@@ -206,7 +209,7 @@ importers:
209 version: 5.0.1(vue@3.5.34(typescript@6.0.3))
210 '@tailwindcss/vite':
211 specifier: ^4.3.0
209 - version: 4.3.0(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
212 + version: 4.3.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
213 '@tsconfig/node20':
214 specifier: ^20.1.9
215 version: 20.1.9
@@ -229,8 +232,11 @@ importers:
232 specifier: ^14.1.2
233 version: 14.1.2
234 '@types/node':
232 - specifier: ^25.8.0
233 - version: 25.8.0
235 + specifier: ^25.9.0
236 + version: 25.9.0
237 + '@types/papaparse':
238 + specifier: ^5.5.2
239 + version: 5.5.2
240 '@types/react':
241 specifier: ^19.2.14
242 version: 19.2.14
@@ -242,10 +248,10 @@ importers:
248 version: 13.15.10
249 '@vitejs/plugin-vue':
250 specifier: ^6.0.7
245 - version: 6.0.7(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))
251 + version: 6.0.7(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))
252 '@vitejs/plugin-vue-jsx':
253 specifier: ^5.1.5
248 - version: 5.1.5(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))
254 + version: 5.1.5(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))
255 '@vue/test-utils':
256 specifier: ^2.4.10
257 version: 2.4.10(@vue/compiler-dom@3.5.34)(@vue/server-renderer@3.5.34(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))
@@ -253,8 +259,8 @@ importers:
259 specifier: ~0.9.1
260 version: 0.9.1(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
261 baseline-browser-mapping:
256 - specifier: ^2.10.29
257 - version: 2.10.29
262 + specifier: ^2.10.31
263 + version: 2.10.31
264 depcheck:
265 specifier: ^1.4.7
266 version: 1.4.7
@@ -268,8 +274,8 @@ importers:
274 specifier: ^11.3.5
275 version: 11.3.5
276 jscpd:
271 - specifier: ^4.2.2
272 - version: 4.2.2
277 + specifier: ^4.2.3
278 + version: 4.2.3
279 jsdom:
280 specifier: ^29.1.1
281 version: 29.1.1
@@ -305,25 +311,25 @@ importers:
311 version: 6.0.3
312 vite:
313 specifier: ^8.0.13
308 - version: 8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
314 + version: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
315 vite-bundle-visualizer:
316 specifier: ^1.2.1
317 version: 1.2.1(rolldown@1.0.1)
318 vite-plugin-inspect:
319 specifier: ^11.3.3
314 - version: 11.3.3(@nuxt/kit@3.21.5)(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
320 + version: 11.3.3(@nuxt/kit@3.21.5)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
321 vite-plugin-vue-devtools:
322 specifier: ^8.1.2
317 - version: 8.1.2(@nuxt/kit@3.21.5)(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))
323 + version: 8.1.2(@nuxt/kit@3.21.5)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))
324 vite-svg-loader:
325 specifier: ^5.1.1
326 version: 5.1.1(vue@3.5.34(typescript@6.0.3))
327 vitest:
328 specifier: ^4.1.6
323 - version: 4.1.6(@types/node@25.8.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
329 + version: 4.1.6(@types/node@25.9.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
330 vue-tsc:
325 - specifier: ^3.2.9
326 - version: 3.2.9(typescript@6.0.3)
331 + specifier: ^3.3.0
332 + version: 3.3.0(typescript@6.0.3)
333 optionalDependencies:
334 '@rollup/rollup-linux-x64-gnu':
335 specifier: ^4.46.2
@@ -957,21 +963,21 @@ packages:
963 peerDependencies:
964 vue: '>=3.0.0'
965
960 - '@intlify/core-base@11.4.2':
961 - resolution: {integrity: sha512-7fpuCcVmeLv2T9qHsARqGvh8xt+sV2fH+Q+gMHFwB/rPXzo85DpbJFKn7dBH1L5p0c2cSh2DW+2h/64EKrISmA==}
962 - engines: {node: '>= 16'}
966 + '@intlify/core-base@11.4.4':
967 + resolution: {integrity: sha512-w/vItlylrAmhebkIbVl5YY8XMCtj8Mb2g70ttxktMYuf5AuRahgEHL2iLgLIsZBIbTSgs4hkUo7ucCL0uTJvOg==}
968 + engines: {node: '>= 22'}
969
964 - '@intlify/devtools-types@11.4.2':
965 - resolution: {integrity: sha512-3u8EN1kB6EMSi96KXs5k7a8y2X2g4+h3X6iwVZU47cP4n+mTuq//WMjG588BzSp/2XQ/dTXo2BLUXX+XS+PNfA==}
966 - engines: {node: '>= 16'}
970 + '@intlify/devtools-types@11.4.4':
971 + resolution: {integrity: sha512-PcBLmGmDQsTSVV911P8upzpcLJO1CNVYi/IH6bGnLR2nA+0L963+kXN1ZrisTEnbtw2ewN6HMMSldqzjronA0Q==}
972 + engines: {node: '>= 22'}
973
968 - '@intlify/message-compiler@11.4.2':
969 - resolution: {integrity: sha512-a6CDSGSMTGrg0BjD97x8TBYPf7qQMDlZipJ6UDfv/pd4OIym8TMlHu3MsH0bTNnRdAG2D6EFEykIgiQPqvtTkA==}
970 - engines: {node: '>= 16'}
974 + '@intlify/message-compiler@11.4.4':
975 + resolution: {integrity: sha512-vn0OAV9pYkJlPPmgnsSm5eAG3mL0+9C/oaded2JY9jmxBbhmUXT3TcAUY8WRgLY9Hte7lkUJKpXrVlYjMXBD2w==}
976 + engines: {node: '>= 22'}
977
972 - '@intlify/shared@11.4.2':
973 - resolution: {integrity: sha512-NzpHbguRCsOHDwxmlBa9qu/imc+/QWgsYUaK6FZeNC0wK8QfAbhqrktEp/haVzxU1aikH8IX4ytD+mfFEMi/9A==}
974 - engines: {node: '>= 16'}
978 + '@intlify/shared@11.4.4':
979 + resolution: {integrity: sha512-QRUCHqda1U6aR14FR0vvXD4+4gj6+fm0AhAozvSuRCw0fCvrmCugWpgiR4xH2NI6s8am6N9p5OhirplsX8ZS3g==}
980 + engines: {node: '>= 22'}
981
982 '@isaacs/cliui@8.0.2':
983 resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
@@ -993,20 +999,20 @@ packages:
999 '@jridgewell/trace-mapping@0.3.31':
1000 resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
1001
996 - '@jscpd/badge-reporter@4.2.2':
997 - resolution: {integrity: sha512-03ngWGBtvdL6dRC7+Q/4FdoUozN4KBHpaJ1SjGnPaMuPoEmcGDdw7W/+HfhPWSwpzam3lfTM2yBsGhrG2g3LqQ==}
1002 + '@jscpd/badge-reporter@4.2.3':
1003 + resolution: {integrity: sha512-yNvbwWl/NwogHT5XrHyqXgF9yVZeLWA2QOhGqYTopvgi7LsSbDumpOqOcJMHP9Z4RalhMfahh+dVXFSI7tMcaA==}
1004
999 - '@jscpd/core@4.2.2':
1000 - resolution: {integrity: sha512-YheyoZAVIeZXyQ5mE1lZ+no8Rs8bo02PXnTfW6PN9QrPUOlRsuJ0br5F3T/H6GU8S2DUP5gUJPOp1Lz7oxN+aA==}
1005 + '@jscpd/core@4.2.3':
1006 + resolution: {integrity: sha512-VQ2gH+tiI51ty3PBRD4HClNNgyX/VH9cs0dcFKuywxDzLQ64jYp7vhJPcqnyiVX9tVEIAa12sucRHQP/VHwugA==}
1007
1002 - '@jscpd/finder@4.2.2':
1003 - resolution: {integrity: sha512-HGsvprcIVTjSU3x8+Ofb5mKq2gRYYZ8Cd1O00w97x8GWUub5sHLGOdvlP4Bw19FVoZzBz+1pjnF5dDvzEoj4rg==}
1008 + '@jscpd/finder@4.2.3':
1009 + resolution: {integrity: sha512-ZpjviFAg6zLojQHS+owvrn8DG1OY1d4835Je4LUKzbMurndmQDhvRRFDkN9V6xPn6gvRaMVkJHN2tyljsnUjWA==}
1010
1005 - '@jscpd/html-reporter@4.2.2':
1006 - resolution: {integrity: sha512-pw+spkYfgWNiDH9bURB0jWzGlfbpQjc8gUe3c726qzPSgMUe9QB3fBA44e9iAJP/o0jtizgUd4MZHyWRbVAw1w==}
1011 + '@jscpd/html-reporter@4.2.3':
1012 + resolution: {integrity: sha512-kp1pqJXCKwyRu5mJK5IvXdFQEDHWQDb7svLFlbVXGI0dVH1y1XNl8mrIrSoRw+0AySxhDkuSyIlQOSDC2GRwQg==}
1013
1008 - '@jscpd/tokenizer@4.2.2':
1009 - resolution: {integrity: sha512-haV7OWM2xzQ3I7tWgNTAOcR+kkFr0lYxkdnTOSd5Pve4QGx1Jk7kwk9gotqpXTd6AauaPnBdfHPxeZrUY21QYA==}
1014 + '@jscpd/tokenizer@4.2.3':
1015 + resolution: {integrity: sha512-RvjD7/hwqtcQC9MWOl31odTti6kGCFxZ77DKEhwyMn+r6oVEUFbXgcGvzn0GC/wuTl7f3j5MF9JNMeTneOFwYA==}
1016
1017 '@juggle/resize-observer@3.4.0':
1018 resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
@@ -1035,17 +1041,6 @@ packages:
1041 '@mui/core-downloads-tracker@9.0.1':
1042 resolution: {integrity: sha512-GzamIIhZ1bH77dq7eKaeyRgJdkypsxin4jBFq2EMs4lBWRR0LFO1CSVMsoebn/VvjcNrnrOrjy48MkrkQUK2iw==}
1043
1038 - '@mui/icons-material@7.3.11':
1039 - resolution: {integrity: sha512-+hz5ilwHZ3djd5es3sCErLioqe/NhZcYTsV/TNXZAMdJdb23F4xzJjqnnZdnurc3S1+ietcssRNqieOhPQLZ7Q==}
1040 - engines: {node: '>=14.0.0'}
1041 - peerDependencies:
1042 - '@mui/material': ^7.3.11
1043 - '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
1044 - react: ^17.0.0 || ^18.0.0 || ^19.0.0
1045 - peerDependenciesMeta:
1046 - '@types/react':
1047 - optional: true
1048 -
1044 '@mui/material@9.0.1':
1045 resolution: {integrity: sha512-voyCpeUxcSWLN7KPZuq0pGCIt726T9K6kiVM3XUcywZDAlZSarLHaUxJVQpospbjjOzN53hwyjo8s6KoWl6utw==}
1046 engines: {node: '>=14.0.0'}
@@ -1407,24 +1402,16 @@ packages:
1402 '@shikijs/vscode-textmate@10.0.2':
1403 resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
1404
1410 - '@shuffleio/shuffle-mcps@0.0.11':
1411 - resolution: {integrity: sha512-wZnGUT96Ka5nE10ocOYjwn1AGAV4XhzNkAv9n7wXINrzKCtVKzM08rs+10UkW0eIEmMJwZsIzjohLNjzeOa/MA==}
1405 + '@shuffleio/shuffle-mcps@0.0.13':
1406 + resolution: {integrity: sha512-Fs26fqOYorPZ39LCvFbKS4vfy9k7Wp5A3dm6xzb3NSP2IYmZMp9iwYKpNAlOPeC2SE/a7lR0KqNXwicQxhXoNQ==}
1407 peerDependencies:
1408 '@emotion/react': '>=11'
1409 '@emotion/styled': '>=11'
1415 - '@mui/icons-material': ^7.3.9
1410 '@mui/material': '>=5'
1417 - framer-motion: '>=10'
1411 lucide-react: '>=0.300.0'
1412 react: '>=18'
1413 react-dom: '>=18'
1421 - react-markdown: '>=8'
1414 react-router-dom: '>=6'
1423 - remark-gfm: '>=3'
1424 - sonner: '>=1'
1425 - peerDependenciesMeta:
1426 - react-router-dom:
1427 - optional: true
1415
1416 '@sindresorhus/base62@1.0.0':
1417 resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==}
@@ -1614,6 +1601,12 @@ packages:
1601 '@types/node@25.8.0':
1602 resolution: {integrity: sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==}
1603
1604 + '@types/node@25.9.0':
1605 + resolution: {integrity: sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==}
1606 +
1607 + '@types/papaparse@5.5.2':
1608 + resolution: {integrity: sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==}
1609 +
1610 '@types/parse-json@4.0.2':
1611 resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
1612
@@ -1870,8 +1863,8 @@ packages:
1863 '@vue/devtools-shared@8.1.2':
1864 resolution: {integrity: sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==}
1865
1873 - '@vue/language-core@3.2.9':
1874 - resolution: {integrity: sha512-ie0ojt/0fU/GfIogh+zgHbaYRPlt9S+cLOxcWwF7nTSFh897BVgnFKL2byT4kpp1mlqYWZ2psGwSniyE2xsxYw==}
1866 + '@vue/language-core@3.3.0':
1867 + resolution: {integrity: sha512-EyUxq1b8Yoxk6hQ6X33BIRnfFLb9Rbm9w/8G8y6uMxlQu7CW7yy9JS/z54xSpIvBvVWX6Lt5v1aBGwmrqD4aJw==}
1868
1869 '@vue/reactivity@3.5.34':
1870 resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==}
@@ -2076,6 +2069,11 @@ packages:
2069 engines: {node: '>=6.0.0'}
2070 hasBin: true
2071
2072 + baseline-browser-mapping@2.10.31:
2073 + resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==}
2074 + engines: {node: '>=6.0.0'}
2075 + hasBin: true
2076 +
2077 bidi-js@1.0.3:
2078 resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
2079
@@ -2916,8 +2914,8 @@ packages:
2914 resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
2915 engines: {node: '>=0.4.x'}
2916
2919 - framer-motion@12.38.0:
2920 - resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
2917 + framer-motion@11.18.2:
2918 + resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==}
2919 peerDependencies:
2920 '@emotion/is-prop-valid': '*'
2921 react: ^18.0.0 || ^19.0.0
@@ -3257,11 +3255,11 @@ packages:
3255 resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
3256 hasBin: true
3257
3260 - jscpd-sarif-reporter@4.2.2:
3261 - resolution: {integrity: sha512-qpLhFZ6pVJcIjvEcUVRxnsrQuTbRY7bBnAgzXY9th0mL1uYhb1ii+SkMcBrBchH23vFfZu/9EG4lIxLCGfEpCQ==}
3258 + jscpd-sarif-reporter@4.2.3:
3259 + resolution: {integrity: sha512-rM0LM5S0kdASLCtDsr1s51rJOPf8nubaxaWQUTWVVPda1UMPymXbELG+A3Rgpoa4D4QFUFfXqz60Jn/W+vlFtA==}
3260
3263 - jscpd@4.2.2:
3264 - resolution: {integrity: sha512-zmoSXtG5Dsgo3QHhnsA06okqnKu3DlYBeAuD7MRnNQoNiL13UgBBh28pEEhe0tyR4yqYXk4Q3GAFoHx5cY+oDw==}
3261 + jscpd@4.2.3:
3262 + resolution: {integrity: sha512-/1BEga1E1cY56/sdQOzU/PFtnea+n1beqG8/Xx4HopG9c5rkUO8ptnu9En8Xf1ILGW6KSWidV4vLQTm2FGYvpw==}
3263 hasBin: true
3264
3265 jsdoc-type-pratt-parser@7.1.1:
@@ -3707,11 +3705,11 @@ packages:
3705 module-replacements@3.0.0-beta.7:
3706 resolution: {integrity: sha512-n1F9l3gF1wNh13xmnXS2JU7P9c3DlzCgVEyLKrVN0U37RwrXyYoePMMvYvs/6aUONAxbnscphzESZTCorXFh7Q==}
3707
3710 - motion-dom@12.38.0:
3711 - resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
3708 + motion-dom@11.18.1:
3709 + resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==}
3710
3713 - motion-utils@12.36.0:
3714 - resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
3711 + motion-utils@11.18.1:
3712 + resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==}
3713
3714 mrmime@2.0.1:
3715 resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
@@ -3843,6 +3841,9 @@ packages:
3841 package-manager-detector@1.6.0:
3842 resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
3843
3844 + papaparse@5.5.3:
3845 + resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==}
3846 +
3847 parent-module@1.0.1:
3848 resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
3849 engines: {node: '>=6'}
@@ -4126,8 +4127,8 @@ packages:
4127 react-is@19.2.6:
4128 resolution: {integrity: sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==}
4129
4129 - react-markdown@10.1.0:
4130 - resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
4130 + react-markdown@9.1.0:
4131 + resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==}
4132 peerDependencies:
4133 '@types/react': '>=18'
4134 react: '>=18'
@@ -4376,8 +4377,8 @@ packages:
4377 sisteransi@1.0.5:
4378 resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
4379
4379 - sonner@2.0.7:
4380 - resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
4380 + sonner@1.7.4:
4381 + resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==}
4382 peerDependencies:
4383 react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
4384 react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
@@ -4870,9 +4871,9 @@ packages:
4871 peerDependencies:
4872 vue: ^3.0.0
4873
4873 - vue-i18n@11.4.2:
4874 - resolution: {integrity: sha512-sADDeKXqAGsPX6tK3t3y2ZiMpbVWN12tG+MhTiJ06rVoh58eGtM4wFyw3uWGbVkXByVp9Ne/AP+nSSzI+J9OAQ==}
4875 - engines: {node: '>= 16'}
4874 + vue-i18n@11.4.4:
4875 + resolution: {integrity: sha512-gIbXVSFQV4jcSJxfwdZ5zSZmZ+12CnX0K3vBkRSd6Zn+HSzCp+QwUgPwpD/uN0oKNKI9RzlUXPKVedEuMgNG0A==}
4876 + engines: {node: '>= 22'}
4877 peerDependencies:
4878 vue: ^3.0.0
4879
@@ -4896,8 +4897,8 @@ packages:
4897 peerDependencies:
4898 vue: ^3.3.4
4899
4899 - vue-tsc@3.2.9:
4900 - resolution: {integrity: sha512-qm8/nbo+9eZc1SCndm9wT+gq23pM+wRIdHY0wjm83B3lIginHTwcdrLUyTrKjDWXbMVNjKegNrnymhpdqnCL3A==}
4900 + vue-tsc@3.3.0:
4901 + resolution: {integrity: sha512-kY8RcoTOENASi0P1GLPvJgA2+hoGF+t8We1UGgmnAb1r/GjTUMSE3zz+WGfjPORZNnBHdAt67sVPhBLXWunkeg==}
4902 hasBin: true
4903 peerDependencies:
4904 typescript: '>=5.0.0'
@@ -5161,7 +5162,7 @@ snapshots:
5162 dependencies:
5163 '@algolia/client-common': 5.52.1
5164
5164 - '@antfu/eslint-config@9.0.0(@typescript-eslint/rule-tester@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(@typescript-eslint/typescript-estree@8.59.3(typescript@6.0.3))(@typescript-eslint/utils@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.34)(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.6(@types/node@25.8.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)))':
5165 + '@antfu/eslint-config@9.0.0(@typescript-eslint/rule-tester@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(@typescript-eslint/typescript-estree@8.59.3(typescript@6.0.3))(@typescript-eslint/utils@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.34)(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.6(@types/node@25.9.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)))':
5166 dependencies:
5167 '@antfu/install-pkg': 1.1.0
5168 '@clack/prompts': 1.4.0
@@ -5171,7 +5172,7 @@ snapshots:
5172 '@stylistic/eslint-plugin': 5.10.0(eslint@10.4.0(jiti@2.7.0))
5173 '@typescript-eslint/eslint-plugin': 8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
5174 '@typescript-eslint/parser': 8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
5174 - '@vitest/eslint-plugin': 1.6.17(@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.6(@types/node@25.8.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)))
5175 + '@vitest/eslint-plugin': 1.6.17(@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.6(@types/node@25.9.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)))
5176 ansis: 4.3.0
5177 cac: 7.0.0
5178 eslint: 10.4.0(jiti@2.7.0)
@@ -5830,23 +5831,23 @@ snapshots:
5831 '@iconify/types': 2.0.0
5832 vue: 3.5.34(typescript@6.0.3)
5833
5833 - '@intlify/core-base@11.4.2':
5834 + '@intlify/core-base@11.4.4':
5835 dependencies:
5835 - '@intlify/devtools-types': 11.4.2
5836 - '@intlify/message-compiler': 11.4.2
5837 - '@intlify/shared': 11.4.2
5836 + '@intlify/devtools-types': 11.4.4
5837 + '@intlify/message-compiler': 11.4.4
5838 + '@intlify/shared': 11.4.4
5839
5839 - '@intlify/devtools-types@11.4.2':
5840 + '@intlify/devtools-types@11.4.4':
5841 dependencies:
5841 - '@intlify/core-base': 11.4.2
5842 - '@intlify/shared': 11.4.2
5842 + '@intlify/core-base': 11.4.4
5843 + '@intlify/shared': 11.4.4
5844
5844 - '@intlify/message-compiler@11.4.2':
5845 + '@intlify/message-compiler@11.4.4':
5846 dependencies:
5846 - '@intlify/shared': 11.4.2
5847 + '@intlify/shared': 11.4.4
5848 source-map-js: 1.2.1
5849
5849 - '@intlify/shared@11.4.2': {}
5850 + '@intlify/shared@11.4.4': {}
5851
5852 '@isaacs/cliui@8.0.2':
5853 dependencies:
@@ -5876,20 +5877,20 @@ snapshots:
5877 '@jridgewell/resolve-uri': 3.1.2
5878 '@jridgewell/sourcemap-codec': 1.5.5
5879
5879 - '@jscpd/badge-reporter@4.2.2':
5880 + '@jscpd/badge-reporter@4.2.3':
5881 dependencies:
5882 badgen: 3.3.2
5883 colors: 1.4.0
5884 fs-extra: 11.3.5
5885
5885 - '@jscpd/core@4.2.2':
5886 + '@jscpd/core@4.2.3':
5887 dependencies:
5888 eventemitter3: 5.0.4
5889
5889 - '@jscpd/finder@4.2.2':
5890 + '@jscpd/finder@4.2.3':
5891 dependencies:
5891 - '@jscpd/core': 4.2.2
5892 - '@jscpd/tokenizer': 4.2.2
5892 + '@jscpd/core': 4.2.3
5893 + '@jscpd/tokenizer': 4.2.3
5894 blamer: 1.0.7
5895 bytes: 3.1.2
5896 cli-table3: 0.6.5
@@ -5899,15 +5900,15 @@ snapshots:
5900 markdown-table: 2.0.0
5901 pug: 3.0.4
5902
5902 - '@jscpd/html-reporter@4.2.2':
5903 + '@jscpd/html-reporter@4.2.3':
5904 dependencies:
5905 colors: 1.4.0
5906 fs-extra: 11.3.5
5907 pug: 3.0.4
5908
5908 - '@jscpd/tokenizer@4.2.2':
5909 + '@jscpd/tokenizer@4.2.3':
5910 dependencies:
5910 - '@jscpd/core': 4.2.2
5911 + '@jscpd/core': 4.2.3
5912 spark-md5: 3.0.2
5913
5914 '@juggle/resize-observer@3.4.0': {}
@@ -5940,14 +5941,6 @@ snapshots:
5941
5942 '@mui/core-downloads-tracker@9.0.1': {}
5943
5943 - '@mui/icons-material@7.3.11(@mui/material@9.0.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/react@19.2.14)(react@19.2.6)':
5944 - dependencies:
5945 - '@babel/runtime': 7.29.2
5946 - '@mui/material': 9.0.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
5947 - react: 19.2.6
5948 - optionalDependencies:
5949 - '@types/react': 19.2.14
5950 -
5944 '@mui/material@9.0.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
5945 dependencies:
5946 '@babel/runtime': 7.29.2
@@ -6252,22 +6245,26 @@ snapshots:
6245
6246 '@shikijs/vscode-textmate@10.0.2': {}
6247
6255 - '@shuffleio/shuffle-mcps@0.0.11(eeed91f181808c3b62bfac5431c1b580)':
6248 + '@shuffleio/shuffle-mcps@0.0.13(@emotion/is-prop-valid@1.4.0)(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@mui/material@9.0.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/react@19.2.14)(lucide-react@1.16.0(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react-router-dom@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)':
6249 dependencies:
6250 '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.6)
6251 '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6)
6259 - '@mui/icons-material': 7.3.11(@mui/material@9.0.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/react@19.2.14)(react@19.2.6)
6252 '@mui/material': 9.0.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6253 algoliasearch: 5.52.1
6262 - framer-motion: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6254 + framer-motion: 11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6255 lucide-react: 1.16.0(react@19.2.6)
6256 react: 19.2.6
6257 react-dom: 19.2.6(react@19.2.6)
6266 - react-markdown: 10.1.0(@types/react@19.2.14)(react@19.2.6)
6267 - remark-gfm: 4.0.1
6268 - sonner: 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6269 - optionalDependencies:
6258 + react-markdown: 9.1.0(@types/react@19.2.14)(react@19.2.6)
6259 react-router-dom: 7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6260 + react18-json-view: 0.2.10(react@19.2.6)
6261 + remark-breaks: 4.0.0
6262 + remark-gfm: 4.0.1
6263 + sonner: 1.7.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6264 + transitivePeerDependencies:
6265 + - '@emotion/is-prop-valid'
6266 + - '@types/react'
6267 + - supports-color
6268
6269 '@sindresorhus/base62@1.0.0': {}
6270
@@ -6344,12 +6341,12 @@ snapshots:
6341 '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
6342 '@tailwindcss/oxide-win32-x64-msvc': 4.3.0
6343
6347 - '@tailwindcss/vite@4.3.0(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))':
6344 + '@tailwindcss/vite@4.3.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))':
6345 dependencies:
6346 '@tailwindcss/node': 4.3.0
6347 '@tailwindcss/oxide': 4.3.0
6348 tailwindcss: 4.3.0
6352 - vite: 8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
6349 + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
6350
6351 '@tsconfig/node20@20.1.9': {}
6352
@@ -6438,6 +6435,14 @@ snapshots:
6435 dependencies:
6436 undici-types: 7.24.6
6437
6438 + '@types/node@25.9.0':
6439 + dependencies:
6440 + undici-types: 7.24.6
6441 +
6442 + '@types/papaparse@5.5.2':
6443 + dependencies:
6444 + '@types/node': 25.8.0
6445 +
6446 '@types/parse-json@4.0.2': {}
6447
6448 '@types/prop-types@15.7.15': {}
@@ -6577,25 +6582,25 @@ snapshots:
6582
6583 '@ungap/structured-clone@1.3.1': {}
6584
6580 - '@vitejs/plugin-vue-jsx@5.1.5(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))':
6585 + '@vitejs/plugin-vue-jsx@5.1.5(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))':
6586 dependencies:
6587 '@babel/core': 7.29.0
6588 '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
6589 '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
6590 '@rolldown/pluginutils': 1.0.1
6591 '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0)
6587 - vite: 8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
6592 + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
6593 vue: 3.5.34(typescript@6.0.3)
6594 transitivePeerDependencies:
6595 - supports-color
6596
6592 - '@vitejs/plugin-vue@6.0.7(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))':
6597 + '@vitejs/plugin-vue@6.0.7(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))':
6598 dependencies:
6599 '@rolldown/pluginutils': 1.0.1
6595 - vite: 8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
6600 + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
6601 vue: 3.5.34(typescript@6.0.3)
6602
6598 - '@vitest/eslint-plugin@1.6.17(@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.6(@types/node@25.8.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)))':
6603 + '@vitest/eslint-plugin@1.6.17(@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.6(@types/node@25.9.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)))':
6604 dependencies:
6605 '@typescript-eslint/scope-manager': 8.59.3
6606 '@typescript-eslint/utils': 8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
@@ -6603,7 +6608,7 @@ snapshots:
6608 optionalDependencies:
6609 '@typescript-eslint/eslint-plugin': 8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
6610 typescript: 6.0.3
6606 - vitest: 4.1.6(@types/node@25.8.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
6611 + vitest: 4.1.6(@types/node@25.9.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
6612 transitivePeerDependencies:
6613 - supports-color
6614
@@ -6616,13 +6621,13 @@ snapshots:
6621 chai: 6.2.2
6622 tinyrainbow: 3.1.0
6623
6619 - '@vitest/mocker@4.1.6(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))':
6624 + '@vitest/mocker@4.1.6(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))':
6625 dependencies:
6626 '@vitest/spy': 4.1.6
6627 estree-walker: 3.0.3
6628 magic-string: 0.30.21
6629 optionalDependencies:
6625 - vite: 8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
6630 + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
6631
6632 '@vitest/pretty-format@4.1.6':
6633 dependencies:
@@ -6797,7 +6802,7 @@ snapshots:
6802
6803 '@vue/devtools-shared@8.1.2': {}
6804
6800 - '@vue/language-core@3.2.9':
6805 + '@vue/language-core@3.3.0':
6806 dependencies:
6807 '@volar/language-core': 2.4.28
6808 '@vue/compiler-dom': 3.5.34
@@ -7007,6 +7012,8 @@ snapshots:
7012
7013 baseline-browser-mapping@2.10.29: {}
7014
7015 + baseline-browser-mapping@2.10.31: {}
7016 +
7017 bidi-js@1.0.3:
7018 dependencies:
7019 require-from-string: 2.0.2
@@ -7906,10 +7913,10 @@ snapshots:
7913
7914 format@0.2.2: {}
7915
7909 - framer-motion@12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
7916 + framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
7917 dependencies:
7911 - motion-dom: 12.38.0
7912 - motion-utils: 12.36.0
7918 + motion-dom: 11.18.1
7919 + motion-utils: 11.18.1
7920 tslib: 2.8.1
7921 optionalDependencies:
7922 '@emotion/is-prop-valid': 1.4.0
@@ -8242,23 +8249,23 @@ snapshots:
8249 argparse: 1.0.10
8250 esprima: 4.0.1
8251
8245 - jscpd-sarif-reporter@4.2.2:
8252 + jscpd-sarif-reporter@4.2.3:
8253 dependencies:
8254 colors: 1.4.0
8255 fs-extra: 11.3.5
8256 node-sarif-builder: 3.4.0
8257
8251 - jscpd@4.2.2:
8258 + jscpd@4.2.3:
8259 dependencies:
8253 - '@jscpd/badge-reporter': 4.2.2
8254 - '@jscpd/core': 4.2.2
8255 - '@jscpd/finder': 4.2.2
8256 - '@jscpd/html-reporter': 4.2.2
8257 - '@jscpd/tokenizer': 4.2.2
8260 + '@jscpd/badge-reporter': 4.2.3
8261 + '@jscpd/core': 4.2.3
8262 + '@jscpd/finder': 4.2.3
8263 + '@jscpd/html-reporter': 4.2.3
8264 + '@jscpd/tokenizer': 4.2.3
8265 colors: 1.4.0
8266 commander: 5.1.0
8267 fs-extra: 11.3.5
8261 - jscpd-sarif-reporter: 4.2.2
8268 + jscpd-sarif-reporter: 4.2.3
8269
8270 jsdoc-type-pratt-parser@7.1.1: {}
8271
@@ -8911,11 +8918,11 @@ snapshots:
8918
8919 module-replacements@3.0.0-beta.7: {}
8920
8914 - motion-dom@12.38.0:
8921 + motion-dom@11.18.1:
8922 dependencies:
8916 - motion-utils: 12.36.0
8923 + motion-utils: 11.18.1
8924
8918 - motion-utils@12.36.0: {}
8925 + motion-utils@11.18.1: {}
8926
8927 mrmime@2.0.1: {}
8928
@@ -9066,6 +9073,8 @@ snapshots:
9073
9074 package-manager-detector@1.6.0: {}
9075
9076 + papaparse@5.5.3: {}
9077 +
9078 parent-module@1.0.1:
9079 dependencies:
9080 callsites: 3.1.0
@@ -9309,7 +9318,7 @@ snapshots:
9318
9319 react-is@19.2.6: {}
9320
9312 - react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.6):
9321 + react-markdown@9.1.0(@types/react@19.2.14)(react@19.2.6):
9322 dependencies:
9323 '@types/hast': 3.0.4
9324 '@types/mdast': 4.0.4
@@ -9584,7 +9593,7 @@ snapshots:
9593
9594 sisteransi@1.0.5: {}
9595
9587 - sonner@2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
9596 + sonner@1.7.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
9597 dependencies:
9598 react: 19.2.6
9599 react-dom: 19.2.6(react@19.2.6)
@@ -9942,17 +9951,17 @@ snapshots:
9951 - rollup
9952 - supports-color
9953
9945 - vite-dev-rpc@1.1.0(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)):
9954 + vite-dev-rpc@1.1.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)):
9955 dependencies:
9956 birpc: 2.9.0
9948 - vite: 8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
9949 - vite-hot-client: 2.2.0(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
9957 + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
9958 + vite-hot-client: 2.2.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
9959
9951 - vite-hot-client@2.2.0(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)):
9960 + vite-hot-client@2.2.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)):
9961 dependencies:
9953 - vite: 8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
9962 + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
9963
9955 - vite-plugin-inspect@11.3.3(@nuxt/kit@3.21.5)(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)):
9964 + vite-plugin-inspect@11.3.3(@nuxt/kit@3.21.5)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)):
9965 dependencies:
9966 ansis: 4.3.0
9967 debug: 4.4.3
@@ -9962,28 +9971,28 @@ snapshots:
9971 perfect-debounce: 2.1.0
9972 sirv: 3.0.2
9973 unplugin-utils: 0.3.1
9965 - vite: 8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
9966 - vite-dev-rpc: 1.1.0(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
9974 + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
9975 + vite-dev-rpc: 1.1.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
9976 optionalDependencies:
9977 '@nuxt/kit': 3.21.5
9978 transitivePeerDependencies:
9979 - supports-color
9980
9972 - vite-plugin-vue-devtools@8.1.2(@nuxt/kit@3.21.5)(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)):
9981 + vite-plugin-vue-devtools@8.1.2(@nuxt/kit@3.21.5)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)):
9982 dependencies:
9983 '@vue/devtools-core': 8.1.2(vue@3.5.34(typescript@6.0.3))
9984 '@vue/devtools-kit': 8.1.2
9985 '@vue/devtools-shared': 8.1.2
9986 sirv: 3.0.2
9978 - vite: 8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
9979 - vite-plugin-inspect: 11.3.3(@nuxt/kit@3.21.5)(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
9980 - vite-plugin-vue-inspector: 6.0.0(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
9987 + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
9988 + vite-plugin-inspect: 11.3.3(@nuxt/kit@3.21.5)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
9989 + vite-plugin-vue-inspector: 6.0.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
9990 transitivePeerDependencies:
9991 - '@nuxt/kit'
9992 - supports-color
9993 - vue
9994
9986 - vite-plugin-vue-inspector@6.0.0(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)):
9995 + vite-plugin-vue-inspector@6.0.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)):
9996 dependencies:
9997 '@babel/core': 7.29.0
9998 '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
@@ -9994,7 +10003,7 @@ snapshots:
10003 '@vue/compiler-dom': 3.5.34
10004 kolorist: 1.8.0
10005 magic-string: 0.30.21
9997 - vite: 8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
10006 + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
10007 transitivePeerDependencies:
10008 - supports-color
10009
@@ -10006,7 +10015,7 @@ snapshots:
10015 transitivePeerDependencies:
10016 - supports-color
10017
10009 - vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0):
10018 + vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0):
10019 dependencies:
10020 lightningcss: 1.32.0
10021 picomatch: 4.0.4
@@ -10014,16 +10023,16 @@ snapshots:
10023 rolldown: 1.0.1
10024 tinyglobby: 0.2.16
10025 optionalDependencies:
10017 - '@types/node': 25.8.0
10026 + '@types/node': 25.9.0
10027 fsevents: 2.3.3
10028 jiti: 2.7.0
10029 sass: 1.99.0
10030 yaml: 2.9.0
10031
10023 - vitest@4.1.6(@types/node@25.8.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)):
10032 + vitest@4.1.6(@types/node@25.9.0)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)):
10033 dependencies:
10034 '@vitest/expect': 4.1.6
10026 - '@vitest/mocker': 4.1.6(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
10035 + '@vitest/mocker': 4.1.6(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))
10036 '@vitest/pretty-format': 4.1.6
10037 '@vitest/runner': 4.1.6
10038 '@vitest/snapshot': 4.1.6
@@ -10040,10 +10049,10 @@ snapshots:
10049 tinyexec: 1.1.2
10050 tinyglobby: 0.2.16
10051 tinyrainbow: 3.1.0
10043 - vite: 8.0.13(@types/node@25.8.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
10052 + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)
10053 why-is-node-running: 2.3.0
10054 optionalDependencies:
10046 - '@types/node': 25.8.0
10055 + '@types/node': 25.9.0
10056 jsdom: 29.1.1
10057 transitivePeerDependencies:
10058 - msw
@@ -10092,11 +10101,11 @@ snapshots:
10101 highlight-words-core: 1.2.3
10102 vue: 3.5.34(typescript@6.0.3)
10103
10095 - vue-i18n@11.4.2(vue@3.5.34(typescript@6.0.3)):
10104 + vue-i18n@11.4.4(vue@3.5.34(typescript@6.0.3)):
10105 dependencies:
10097 - '@intlify/core-base': 11.4.2
10098 - '@intlify/devtools-types': 11.4.2
10099 - '@intlify/shared': 11.4.2
10106 + '@intlify/core-base': 11.4.4
10107 + '@intlify/devtools-types': 11.4.4
10108 + '@intlify/shared': 11.4.4
10109 '@vue/devtools-api': 6.6.4
10110 vue: 3.5.34(typescript@6.0.3)
10111
@@ -10128,10 +10137,10 @@ snapshots:
10137 dependencies:
10138 vue: 3.5.34(typescript@6.0.3)
10139
10131 - vue-tsc@3.2.9(typescript@6.0.3):
10140 + vue-tsc@3.3.0(typescript@6.0.3):
10141 dependencies:
10142 '@volar/typescript': 2.4.28
10134 - '@vue/language-core': 3.2.9
10143 + '@vue/language-core': 3.3.0
10144 typescript: 6.0.3
10145
10146 vue3-apexcharts@1.11.1(apexcharts@5.12.0)(vue@3.5.34(typescript@6.0.3)):
@@ -10252,9 +10261,9 @@ snapshots:
10261
10262 xmlchars@2.2.0: {}
10263
10255 - xmllint-wasm@5.2.0(@types/node@25.8.0):
10264 + xmllint-wasm@5.2.0(@types/node@25.9.0):
10265 dependencies:
10257 - '@types/node': 25.8.0
10266 + '@types/node': 25.9.0
10267
10268 xmllint@0.1.1: {}
10269
frontend/src/api/endpoints/incidentManagement/caseTemplates.ts
+1 -3
@@ -136,9 +136,7 @@ export default {
136 // https://github.com/socfortress/CoPilot-Case-Templates.
137 // ---------------------------------------------------------------------------
138 getLibrary() {
139 - return HttpClient.get<FlaskBaseResponse & CaseTemplateLibraryListResponse>(
140 - `/incidents/case_templates/library`
141 - )
139 + return HttpClient.get<FlaskBaseResponse & CaseTemplateLibraryListResponse>(`/incidents/case_templates/library`)
140 },
141 refreshLibrary() {
142 return HttpClient.post<FlaskBaseResponse & CaseTemplateLibraryRefreshResponse>(
frontend/src/assets/scss/overrides/singul-override.scss
+11
@@ -76,4 +76,15 @@
76 background-color: var(--primary-color);
77 box-shadow: 0 0 6px rgba(var(--primary-color-rgb) / 0.4);
78 }
79 +
80 + [role="tablist"] {
81 + button {
82 + cursor: pointer;
83 + .tabular-nums {
84 + border: none;
85 + font-size: var(--font-mono);
86 + color: var(--fg-secondary-color);
87 + }
88 + }
89 + }
90 }
frontend/src/components/activeResponse/ActiveResponseWizard.vue
+2 -2
@@ -3,7 +3,7 @@
3 <div class="flex min-h-120 flex-col">
4 <div class="flex grow flex-col">
5 <n-scrollbar x-scrollable trigger="none">
6 - <div class="p-7 pt-4">
6 + <div class="p-6 pt-3">
7 <n-steps :current size="small" :status="currentStatus">
8 <n-step title="Operative System" />
9 <n-step title="Active Response" />
@@ -82,7 +82,7 @@
82 </div>
83 </div>
84
85 - <div v-if="current !== 3" class="flex justify-between gap-4 p-7 pt-4">
85 + <div v-if="current !== 3" class="flex justify-between gap-4 p-6 pt-3">
86 <div class="flex gap-4">
87 <n-button v-if="isPrevStepEnabled" @click.stop="prev()">
88 <template #icon>
frontend/src/components/agents/AgentCard.vue
+96 -214
@@ -1,95 +1,115 @@
1 <template>
2 - <CardEntity
3 - class="agent-card"
4 - :loading
5 - :embedded
6 - :hoverable
7 - :clickable
8 - :class="{ critical: agent.critical_asset, selectable, selected }"
9 - @click="$emit('click')"
10 - >
11 - <div class="card-content">
12 - <!-- Selection checkbox -->
13 - <div v-if="selectable" class="selection-checkbox" @click.stop>
14 - <n-checkbox :checked="selected" @update:checked="$emit('toggle-selection')" />
15 - </div>
16 -
17 - <div class="wrapper">
18 - <div class="agent-header">
19 - <div class="title">
20 - <n-tooltip>
21 - {{ `${isOnline ? "online" : "last seen"} - ${formatLastSeen}` }}
22 - <template #trigger>
23 - <div class="hostname" :class="{ online: isOnline }">
24 - {{ agent.hostname }}
25 - </div>
26 - </template>
27 - </n-tooltip>
28 - <div class="critical" :class="{ active: agent.critical_asset }">
29 - <n-tooltip>
30 - Toggle Critical Assets
31 - <template #trigger>
32 - <n-button
33 - quaternary
34 - circle
35 - :type="agent.critical_asset ? 'warning' : 'default'"
36 - @click.stop="toggleCritical(agent.agent_id, agent.critical_asset)"
37 - >
38 - <template #icon>
39 - <Icon :name="StarIcon" />
40 - </template>
41 - </n-button>
42 - </template>
43 - </n-tooltip>
44 - </div>
45 - <div v-show="agent.quarantined" class="quarantined">
46 - <n-tooltip>
47 - Quarantined
48 - <template #trigger>
49 - <Icon :name="QuarantinedIcon" :size="18" />
50 - </template>
51 - </n-tooltip>
52 - </div>
53 - </div>
54 - <div class="info">#{{ agent.agent_id }} / {{ agent.label }}</div>
2 + <CardEntity :loading :embedded :hoverable :clickable :highlighted="agent.critical_asset" @click="$emit('click')">
3 + <template #headerMain>
4 + <div class="flex items-center gap-3">
5 + <div v-if="selectable" class="selection-checkbox" @click.stop>
6 + <n-checkbox :checked="selected" @update:checked="$emit('toggle-selection')" />
7 </div>
56 - <div class="agent-info">
57 - <div class="os" :title="agent.os">
58 - {{ agent.os }}
59 - </div>
60 - <div class="ip-address" :title="agent.ip_address">
61 - {{ agent.ip_address }}
62 - </div>
8 + <div class="text-default text-lg font-bold">
9 + {{ agent.hostname }}
10 </div>
64 -
65 - <div v-if="showActions" class="agent-actions">
66 - <div class="box">
67 - <n-tooltip>
68 - Delete
69 - <template #trigger>
70 - <n-button quaternary circle type="error" @click.stop="handleDelete">
71 - <template #icon>
72 - <Icon :name="DeleteIcon" />
73 - </template>
74 - </n-button>
11 + </div>
12 + </template>
13 + <template #headerExtra>
14 + <div class="flex items-center gap-2">
15 + <n-tooltip>
16 + <span class="text-sm">
17 + {{ `${isOnline ? "online" : "last seen"}: ${formatLastSeen}` }}
18 + </span>
19 + <template #trigger>
20 + <n-tag v-if="isOnline" type="success" round class="rounded-lg!" :bordered="false" size="small">
21 + ONLINE
22 + </n-tag>
23 + <n-tag v-else round class="rounded-lg!" :bordered="false" size="small">OFFLINE</n-tag>
24 + </template>
25 + </n-tooltip>
26 +
27 + <n-tooltip>
28 + <span class="text-sm">Toggle Critical Assets</span>
29 + <template #trigger>
30 + <n-button
31 + :type="agent.critical_asset ? 'error' : 'default'"
32 + ghost
33 + size="tiny"
34 + @click.stop="toggleCritical(agent.agent_id, agent.critical_asset)"
35 + >
36 + <template #icon>
37 + <Icon :name="agent.critical_asset ? 'carbon:warning-alt' : 'carbon:checkmark'" />
38 </template>
76 - </n-tooltip>
77 - </div>
39 + {{ agent.critical_asset ? "Critical Asset" : "Non-Critical Asset" }}
40 + </n-button>
41 + </template>
42 + </n-tooltip>
43 +
44 + <n-tag v-if="agent.quarantined" type="warning" round class="rounded-lg!" :bordered="false" size="small">
45 + Quarantined
46 + </n-tag>
47 + </div>
48 + </template>
49 +
50 + <template #default>
51 + <div class="flex flex-wrap items-center gap-2">
52 + <Badge type="splitted" color="primary">
53 + <template #label>ID</template>
54 + <template #value>#{{ agent.agent_id }}</template>
55 + </Badge>
56 + <Badge type="splitted" color="primary">
57 + <template #label>label</template>
58 + <template #value>{{ agent.label }}</template>
59 + </Badge>
60 + <Badge type="splitted" color="primary">
61 + <template #label>OS</template>
62 + <template #value>
63 + <Icon :name="iconFromOs(agent.os)" :size="14" />
64 + {{ agent.os || "-" }}
65 + </template>
66 + </Badge>
67 + <Badge type="splitted" color="primary">
68 + <template #label>ip address</template>
69 + <template #value>
70 + {{ agent.ip_address || "-" }}
71 + </template>
72 + </Badge>
73 + <Badge type="splitted" color="primary">
74 + <template #label>wazuh agent status</template>
75 + <template #value>
76 + {{ agent.wazuh_agent_status || "-" }}
77 + </template>
78 + </Badge>
79 + </div>
80 + </template>
81 +
82 + <template #footerExtra>
83 + <div v-if="showActions" class="agent-actions">
84 + <div class="box flex items-center justify-end gap-4">
85 + <slot name="actions-left" />
86 + <n-tooltip to="body">
87 + <span class="text-sm">Delete Agent</span>
88 + <template #trigger>
89 + <n-button text type="error" size="small" @click.stop="handleDelete">
90 + <template #icon>
91 + <Icon :name="DeleteIcon" />
92 + </template>
93 + Delete
94 + </n-button>
95 + </template>
96 + </n-tooltip>
97 </div>
98 </div>
80 - </div>
99 + </template>
100 </CardEntity>
101 </template>
102
103 <script setup lang="ts">
85 -// TODO-FE: refactor
104 import type { Agent } from "@/types/agents.d"
87 -import { NButton, NCheckbox, NTooltip, useDialog, useMessage } from "naive-ui"
105 +import { NButton, NCheckbox, NTag, NTooltip, useDialog, useMessage } from "naive-ui"
106 import { computed, ref, toRefs } from "vue"
107 +import Badge from "@/components/common/Badge.vue"
108 import CardEntity from "@/components/common/cards/CardEntity.vue"
109 import Icon from "@/components/common/Icon.vue"
110 import { useSettingsStore } from "@/stores/settings"
111 import { AgentStatus } from "@/types/agents.d"
112 +import { iconFromOs } from "@/utils"
113 import dayjs from "@/utils/dayjs"
114 import { handleDeleteAgent, toggleAgentCritical } from "./utils"
115
@@ -111,8 +131,6 @@ const emit = defineEmits<{
131
132 const { agent, showActions, embedded, hoverable, clickable, selectable, selected } = toRefs(props)
133
114 -const QuarantinedIcon = "ph:seal-warning-light"
115 -const StarIcon = "carbon:star"
134 const DeleteIcon = "ph:trash"
135 const dFormats = useSettingsStore().dateFormat
136 const loading = ref(false)
@@ -162,139 +180,3 @@ function toggleCritical(agentId: string, criticalStatus: boolean) {
180 })
181 }
182 </script>
165 -
166 -<style lang="scss" scoped>
167 -.agent-card {
168 - container-type: inline-size;
169 -
170 - &.selected {
171 - border-color: var(--primary-color);
172 - background-color: var(--primary-color-opacity-1);
173 - }
174 -
175 - .card-content {
176 - display: flex;
177 - align-items: center;
178 - gap: 12px;
179 -
180 - .selection-checkbox {
181 - flex-shrink: 0;
182 - display: flex;
183 - align-items: center;
184 - justify-content: center;
185 - cursor: pointer;
186 - }
187 -
188 - .wrapper {
189 - display: flex;
190 - flex-direction: row;
191 - align-items: center;
192 - gap: calc(var(--spacing) * 6);
193 - overflow: hidden;
194 - flex-grow: 1;
195 -
196 - .agent-header {
197 - display: flex;
198 - flex-direction: column;
199 - min-width: 300px;
200 -
201 - .title {
202 - display: flex;
203 - align-items: center;
204 - gap: calc(var(--spacing) * 2);
205 - margin-bottom: 4px;
206 -
207 - .hostname {
208 - font-weight: bold;
209 - white-space: nowrap;
210 - line-height: 32px;
211 - height: 32px;
212 - border-radius: 4px;
213 - border: 1px solid var(--info-color);
214 - border-color: transparent;
215 - box-sizing: border-box;
216 - overflow: hidden;
217 - text-overflow: ellipsis;
218 -
219 - &.online {
220 - padding: 0px 15px;
221 - color: var(--success-color);
222 - border-color: var(--success-color);
223 - }
224 - }
225 -
226 - .quarantined {
227 - display: flex;
228 - padding-top: 1px;
229 - color: var(--warning-color);
230 - }
231 - }
232 - .info {
233 - font-family: var(--font-family-mono);
234 - font-size: var(--text-xs);
235 - opacity: 0.7;
236 - white-space: nowrap;
237 - overflow: hidden;
238 - text-overflow: ellipsis;
239 - margin-left: 2px;
240 - }
241 - }
242 -
243 - .agent-info {
244 - display: flex;
245 - flex-direction: column;
246 - flex-grow: 1;
247 - overflow: hidden;
248 -
249 - .os {
250 - line-height: 32px;
251 - height: 32px;
252 - margin-bottom: 4px;
253 - white-space: nowrap;
254 - overflow: hidden;
255 - text-overflow: ellipsis;
256 - }
257 - .ip-address {
258 - white-space: nowrap;
259 - font-size: var(--text-xs);
260 - font-family: var(--font-family-mono);
261 - opacity: 0.7;
262 - overflow: hidden;
263 - text-overflow: ellipsis;
264 - }
265 - }
266 -
267 - .agent-actions {
268 - display: flex;
269 - }
270 - }
271 - }
272 -
273 - &.critical {
274 - border-color: var(--warning-color);
275 - }
276 -
277 - @container (max-width: 550px) {
278 - .card-content .wrapper {
279 - gap: calc(var(--spacing) * 5);
280 -
281 - .agent-header {
282 - min-width: initial;
283 - }
284 - }
285 - }
286 - @container (max-width: 400px) {
287 - .card-content .wrapper {
288 - gap: calc(var(--spacing) * 4);
289 -
290 - .agent-header {
291 - flex-grow: 1;
292 - overflow: hidden;
293 - }
294 - .agent-info {
295 - display: none;
296 - }
297 - }
298 - }
299 -}
300 -</style>
frontend/src/components/agents/AgentToolbar.vue
+62 -165
@@ -1,13 +1,14 @@
1 <template>
2 - <n-card class="agent-toolbar" content-class="p-0!">
3 - <div class="wrapper flex flex-col gap-6 px-4 py-3">
2 + <n-card class="agent-toolbar @container w-full max-w-full min-w-85 overflow-hidden" content-class="p-0!">
3 + <div class="flex h-full flex-col gap-6 overflow-hidden px-4 py-3">
4 <div class="flex flex-col gap-2">
5 - <div class="agent-search flex gap-3">
6 - <n-input v-model:value="textFilter" placeholder="Search for an agent" clearable>
7 - <template #prefix>
8 - <Icon :name="SearchIcon" />
9 - </template>
10 - </n-input>
5 + <div class="flex items-center justify-between gap-2">
6 + <div class="text-secondary">
7 + <strong v-if="agentsFilteredLength !== agentsLength">{{ agentsFilteredLength }}</strong>
8 + <span v-if="agentsFilteredLength !== agentsLength">/</span>
9 + <strong class="font-mono">{{ agentsLength }}</strong>
10 + Agents
11 + </div>
12
13 <n-dropdown
14 v-if="enableSyncVulnerabilitiesDropdown"
@@ -16,81 +17,89 @@
17 :options="customersOptions"
18 @select="emit('run', $event)"
19 >
19 - <n-button :loading="syncing" secondary @click="load()">Sync</n-button>
20 + <n-button :loading="syncing" secondary size="small" @click="load()">Sync</n-button>
21 </n-dropdown>
21 - <n-button v-else :loading="syncing" secondary @click="emit('run', 'sync-agents')">Sync</n-button>
22 - </div>
23 - <div class="search-info">
24 - <strong v-if="agentsFilteredLength !== agentsLength">{{ agentsFilteredLength }}</strong>
25 - <span v-if="agentsFilteredLength !== agentsLength" class="mh-5">/</span>
26 - <strong class="font-mono">{{ agentsLength }}</strong>
27 - Agents
22 + <n-button v-else :loading="syncing" secondary size="small" @click="emit('run', 'sync-agents')">
23 + Sync
24 + </n-button>
25 </div>
26 + <n-input v-model:value="textFilter" placeholder="Search for an agent" clearable>
27 + <template #prefix>
28 + <Icon :name="SearchIcon" />
29 + </template>
30 + </n-input>
31 </div>
32
33 <!-- Selection Mode & Bulk Delete Section -->
32 - <div class="bulk-actions flex flex-col gap-3">
33 - <div class="selection-toggle flex items-center gap-2">
34 - <n-switch
35 - v-model:value="selectionModeInternal"
36 - @update:value="emit('update:selection-mode', $event)"
37 - >
34 + <n-card embedded content-class="flex flex-col gap-3" size="small" class="hidden! lg:flex!">
35 + <div v-if="!hideSelectionSwitch" class="flex items-center gap-2">
36 + <n-switch v-model:value="selectionMode" @update:value="emit('update:selection-mode', $event)">
37 <template #checked>Selection ON</template>
38 <template #unchecked>Selection OFF</template>
39 </n-switch>
40 </div>
41
43 - <p v-if="selectionModeInternal" class="text-secondary-color text-xs">
42 + <p v-if="selectionMode" class="text-secondary-color text-xs">
43 Click agents to select them for bulk operations. Or select "Bulk Delete" and apply a filter to
44 delete multiple agents at once.
45 </p>
46
48 - <!-- Selected count and actions -->
49 - <div v-if="selectedCount && selectedCount > 0" class="selected-info flex items-center gap-2">
50 - <n-tag type="info" size="small">{{ selectedCount }} selected</n-tag>
51 - <n-button size="tiny" quaternary @click="emit('clear-selection')">Clear</n-button>
52 - </div>
47 + <div class="flex items-center justify-between gap-2">
48 + <n-button
49 + type="error"
50 + secondary
51 + size="small"
52 + :disabled="syncing || !selectedCount"
53 + @click="emit('bulk-delete')"
54 + >
55 + <template #icon>
56 + <Icon :name="DeleteIcon" />
57 + </template>
58 + {{ selectedCount ? `Delete ${selectedCount}` : "Bulk Delete" }}
59 + </n-button>
60
54 - <n-button type="error" secondary size="small" :disabled="syncing" @click="emit('bulk-delete')">
55 - <template #icon>
56 - <Icon :name="DeleteIcon" />
57 - </template>
58 - Bulk Delete
59 - </n-button>
60 - </div>
61 + <div v-if="selectedCount && selectedCount > 0">
62 + <n-button size="small" quaternary @click="emit('clear-selection')">Clear</n-button>
63 + </div>
64 + </div>
65 + </n-card>
66
62 - <div class="agents-list flex grow flex-col overflow-hidden">
67 + <div class="hidden grow flex-col overflow-hidden lg:flex">
68 <n-scrollbar>
64 - <div v-if="agentsCritical?.length" class="agents-critical-list">
65 - <div class="title">
69 + <div v-if="agentsCritical?.length" class="mb-5">
70 + <div class="mb-2">
71 Critical Assets
72 <small class="text-secondary font-mono">({{ agentsCritical.length }})</small>
73 </div>
69 - <div class="list">
70 - <div
74 + <div class="flex flex-col gap-2">
75 + <n-tag
76 v-for="agent in agentsCritical"
77 :key="agent.agent_id"
73 - class="item"
78 + type="error"
79 + :bordered="false"
80 + class="cursor-pointer!"
81 @click="emit('click', agent)"
82 >
83 {{ agent.hostname }}
77 - </div>
84 + </n-tag>
85 </div>
86 </div>
80 - <div v-if="agentsOnline?.length" class="agents-online-list">
81 - <div class="title">
87 + <div v-if="agentsOnline?.length">
88 + <div class="mb-2">
89 Online Agents
90 <small class="text-secondary font-mono">({{ agentsOnline.length }})</small>
91 </div>
85 - <div class="list">
86 - <div
92 + <div class="flex flex-col gap-2">
93 + <n-tag
94 v-for="agent in agentsOnline"
95 :key="agent.agent_id"
89 - class="item"
96 + type="success"
97 + :bordered="false"
98 + class="cursor-pointer!"
99 @click="emit('click', agent)"
100 >
101 {{ agent.hostname }}
93 - </div>
102 + </n-tag>
103 </div>
104 </div>
105 </n-scrollbar>
@@ -106,7 +115,7 @@ import type { Agent } from "@/types/agents.d"
115 import type { Customer } from "@/types/customers.d"
116 import { useWindowSize } from "@vueuse/core"
117 import { NButton, NCard, NDropdown, NInput, NScrollbar, NSwitch, NTag, useMessage } from "naive-ui"
109 -import { computed, h, ref, toRefs, watch } from "vue"
118 +import { computed, h, ref, toRefs } from "vue"
119 import Api from "@/api"
120 import Icon from "@/components/common/Icon.vue"
121
@@ -118,8 +127,8 @@ const props = defineProps<{
127 agentsFilteredLength?: number
128 agentsCritical?: Agent[]
129 agentsOnline?: Agent[]
121 - selectionMode?: boolean
130 selectedCount?: number
131 + hideSelectionSwitch?: boolean
132 }>()
133
134 const emit = defineEmits<{
@@ -139,8 +148,8 @@ const {
148 agentsCritical,
149 agentsOnline,
150 enableSyncVulnerabilitiesDropdown,
142 - selectionMode,
143 - selectedCount
151 + selectedCount,
152 + hideSelectionSwitch
153 } = toRefs(props)
154
155 const SearchIcon = "carbon:search"
@@ -156,14 +165,7 @@ const textFilter = computed<string>({
165 }
166 })
167
159 -const selectionModeInternal = ref(selectionMode?.value ?? false)
160 -
161 -watch(
162 - () => selectionMode?.value,
163 - val => {
164 - selectionModeInternal.value = val ?? false
165 - }
166 -)
168 +const selectionMode = defineModel<boolean>("selectionMode", { default: true, required: false })
169
170 const loadingCustomersList = ref(false)
171 const customersList = ref<Customer[]>([])
@@ -235,108 +237,3 @@ function load() {
237 }
238 }
239 </script>
238 -
239 -<style lang="scss" scoped>
240 -.agent-toolbar {
241 - container-type: inline-size;
242 - overflow: hidden;
243 - max-width: 100%;
244 - min-width: 340px;
245 -
246 - .wrapper {
247 - overflow: hidden;
248 - height: 100%;
249 -
250 - .search-info {
251 - color: var(--fg-secondary-color);
252 - }
253 -
254 - .bulk-actions {
255 - padding: 12px;
256 - background: var(--bg-secondary-color);
257 - border-radius: var(--border-radius);
258 -
259 - .selection-toggle {
260 - font-size: 13px;
261 - }
262 -
263 - .selected-info {
264 - flex-wrap: wrap;
265 - }
266 - }
267 -
268 - .agents-list {
269 - .title {
270 - margin-bottom: calc(var(--spacing) * 2);
271 - }
272 -
273 - .list {
274 - .item {
275 - border: 2px solid transparent;
276 - padding-inline: calc(var(--spacing) * 3);
277 - padding-block: calc(var(--spacing) * 2);
278 - font-size: 14px;
279 - font-weight: bold;
280 - cursor: pointer;
281 - border-radius: var(--border-radius);
282 -
283 - &:not(:last-child) {
284 - margin-bottom: calc(var(--spacing) * 2);
285 - }
286 -
287 - &:hover {
288 - background-color: var(--hover-color);
289 - }
290 - }
291 - }
292 -
293 - .agents-critical-list {
294 - margin-bottom: calc(var(--spacing) * 5);
295 -
296 - .list {
297 - .item {
298 - border-color: var(--warning-color);
299 - }
300 - }
301 - }
302 - .agents-online-list {
303 - .list {
304 - .item {
305 - border-color: var(--success-color);
306 - }
307 - }
308 - }
309 - }
310 - }
311 -
312 - @container (min-width: 350px) {
313 - .wrapper {
314 - .agent-search {
315 - flex-grow: 1;
316 - }
317 - .search-info {
318 - display: none;
319 - }
320 - .agents-list {
321 - display: none;
322 - }
323 - }
324 - }
325 - @media (max-width: 500px) {
326 - min-width: 100%;
327 -
328 - .wrapper {
329 - .agent-search {
330 - flex-grow: 1;
331 - width: 100%;
332 - }
333 - .search-info {
334 - display: none;
335 - }
336 - .agents-list {
337 - display: none;
338 - }
339 - }
340 - }
341 -}
342 -</style>
frontend/src/components/agents/agentFlow/AgentFlowItem.vue
+7 -7
@@ -82,7 +82,7 @@
82 >
83 <n-tabs type="line" animated :tabs-padding="24">
84 <n-tab-pane name="Info" tab="Info" display-directive="show">
85 - <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
85 + <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
86 <CardKV v-for="(value, key) of properties" :key>
87 <template #key>
88 {{ key }}
@@ -94,7 +94,7 @@
94 </div>
95 </n-tab-pane>
96 <n-tab-pane name="Backtrace" tab="Backtrace" display-directive="show">
97 - <div class="p-7 pt-4">
97 + <div class="p-6 pt-3">
98 <n-input
99 :value="flow.backtrace"
100 type="textarea"
@@ -109,12 +109,12 @@
109 </div>
110 </n-tab-pane>
111 <n-tab-pane name="Timeline" tab="Timeline" display-directive="show:lazy">
112 - <div class="p-7 pt-4">
112 + <div class="p-6 pt-3">
113 <AgentFlowTimeline :flow />
114 </div>
115 </n-tab-pane>
116 <n-tab-pane name="Logs" tab="Logs" display-directive="show:lazy">
117 - <div v-if="flow.logs.length" class="p-7 pt-4">
117 + <div v-if="flow.logs.length" class="p-6 pt-3">
118 <ul>
119 <li v-for="log of flow.logs" :key="log">
120 {{ log }}
@@ -124,7 +124,7 @@
124 <n-empty v-else description="No items found" class="h-48 justify-center" />
125 </n-tab-pane>
126 <n-tab-pane name="Uploaded files" tab="Uploaded files" display-directive="show:lazy">
127 - <div v-if="flow.uploaded_files.length" class="p-7 pt-4">
127 + <div v-if="flow.uploaded_files.length" class="p-6 pt-3">
128 <ul>
129 <li v-for="file of flow.uploaded_files" :key="file">
130 {{ file }}
@@ -134,7 +134,7 @@
134 <n-empty v-else description="No items found" class="h-48 justify-center" />
135 </n-tab-pane>
136 <n-tab-pane name="Query stats" tab="Query stats" display-directive="show:lazy">
137 - <div class="p-7 pt-4">
137 + <div class="p-6 pt-3">
138 <template v-if="flow.query_stats.length">
139 <AgentFlowQueryStat
140 v-for="stat of flow.query_stats"
@@ -148,7 +148,7 @@
148 </div>
149 </n-tab-pane>
150 <n-tab-pane name="Request" tab="Request" display-directive="show:lazy">
151 - <div class="p-7 pt-4">
151 + <div class="p-6 pt-3">
152 <SimpleJsonViewer
153 class="vuesjv-override"
154 :model-value="flow.request"
frontend/src/components/agents/agentFlow/AgentFlowQueryStat.vue
+2 -2
@@ -53,7 +53,7 @@
53 >
54 <n-tabs type="line" animated :tabs-padding="24">
55 <n-tab-pane name="Info" tab="Info" display-directive="show">
56 - <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
56 + <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
57 <CardKV v-for="(value, key) of properties" :key>
58 <template #key>
59 {{ key }}
@@ -65,7 +65,7 @@
65 </div>
66 </n-tab-pane>
67 <n-tab-pane name="Backtrace" tab="Backtrace" display-directive="show">
68 - <div class="p-7 pt-4">
68 + <div class="p-6 pt-3">
69 <n-input
70 :value="stat.backtrace"
71 type="textarea"
frontend/src/components/agents/dataStore/AgentDataStoreTab.vue
+1 -1
@@ -114,7 +114,7 @@ import Icon from "@/components/common/Icon.vue"
114 import ArtifactCard from "./ArtifactCard.vue"
115 import ArtifactDetails from "./ArtifactDetails.vue"
116
117 -// TODO-FE: join AgentDataStoreTab + AgentDataStoreTabCompact
117 +// TODO-FE: join AgentDataStoreTab + AgentDataStoreTabCompact (Data Store Tab on Agent Details page)
118
119 const props = defineProps<{
120 agent: Agent
frontend/src/components/agents/sca/ScaItem.vue
+40 -46
@@ -1,34 +1,32 @@
1 <template>
2 - <n-tabs type="line" animated :tabs-padding="24">
3 - <n-tab-pane name="Details" tab="Details" display-directive="show" class="flex flex-col gap-4 py-8!">
4 - <div class="px-7">
5 - <n-card embedded class="overflow-hidden">
6 - <div class="flex flex-wrap justify-between gap-8">
7 - <n-statistic label="Checks" :value="sca.total_checks" tabular-nums />
8 - <n-statistic label="Pass" :value="sca.pass" tabular-nums />
9 - <n-statistic label="Fail" :value="sca.fail" tabular-nums />
10 - <n-statistic label="Invalid" :value="sca.invalid" tabular-nums />
11 - <n-statistic label="Score" :value="`${sca.score}%`" tabular-nums />
12 - </div>
13 - </n-card>
14 - </div>
15 - <div class="px-7">
16 - <n-card embedded class="overflow-hidden">
17 - <div class="xs:flex-row! flex flex-col justify-between gap-8">
18 - <n-statistic
19 - class="grow"
20 - label="Start scan"
21 - :value="formatDate(sca.start_scan, dFormats.datetime).toString()"
22 - />
23 - <n-statistic
24 - class="grow"
25 - label="End scan"
26 - :value="formatDate(sca.end_scan, dFormats.datetime).toString()"
27 - />
28 - </div>
29 - </n-card>
30 - </div>
31 - <div v-if="properties" class="grid-auto-fit-200 grid gap-2 px-7">
2 + <n-tabs type="line" animated :tabs-padding="24" pane-class="min-h-100 p-6!">
3 + <n-tab-pane name="Details" tab="Details" display-directive="show" class="flex flex-col gap-4">
4 + <n-card embedded class="overflow-hidden">
5 + <div class="flex flex-wrap justify-between gap-8">
6 + <n-statistic label="Checks" :value="sca.total_checks" tabular-nums />
7 + <n-statistic label="Pass" :value="sca.pass" tabular-nums />
8 + <n-statistic label="Fail" :value="sca.fail" tabular-nums />
9 + <n-statistic label="Invalid" :value="sca.invalid" tabular-nums />
10 + <n-statistic label="Score" :value="`${sca.score}%`" tabular-nums />
11 + </div>
12 + </n-card>
13 +
14 + <n-card embedded class="overflow-hidden">
15 + <div class="xs:flex-row! flex flex-col justify-between gap-8">
16 + <n-statistic
17 + class="grow"
18 + label="Start scan"
19 + :value="formatDate(sca.start_scan, dFormats.datetime).toString()"
20 + />
21 + <n-statistic
22 + class="grow"
23 + label="End scan"
24 + :value="formatDate(sca.end_scan, dFormats.datetime).toString()"
25 + />
26 + </div>
27 + </n-card>
28 +
29 + <div v-if="properties" class="grid-auto-fit-200 grid gap-2">
30 <CardKV v-for="(value, key) of properties" :key>
31 <template #key>
32 {{ key }}
@@ -56,24 +54,20 @@
54 </div>
55 </n-tab-pane>
56 <n-tab-pane name="Description" tab="Description" display-directive="show">
59 - <div class="p-7 pt-4">
60 - <n-input
61 - :value="sca.description"
62 - type="textarea"
63 - readonly
64 - placeholder="Empty"
65 - size="large"
66 - :autosize="{
67 - minRows: 3,
68 - maxRows: 18
69 - }"
70 - />
71 - </div>
57 + <n-input
58 + :value="sca.description"
59 + type="textarea"
60 + readonly
61 + placeholder="Empty"
62 + size="large"
63 + :autosize="{
64 + minRows: 3,
65 + maxRows: 18
66 + }"
67 + />
68 </n-tab-pane>
69 <n-tab-pane name="SCA Results" tab="SCA Results" display-directive="show:lazy">
74 - <div class="p-7 pt-4">
75 - <ScaResults :sca :agent />
76 - </div>
70 + <ScaResults :sca :agent />
71 </n-tab-pane>
72 </n-tabs>
73 </template>
frontend/src/components/agents/sca/ScaResultItemDetails.vue
+96 -114
@@ -1,44 +1,40 @@
1 <template>
2 - <n-tabs type="line" animated :tabs-padding="24">
3 - <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy" class="flex flex-col gap-4 py-8!">
4 - <div class="px-7">
5 - <n-card embedded class="overflow-hidden">
6 - <div class="flex flex-wrap justify-between gap-8">
7 - <n-statistic label="Result" tabular-nums>
8 - <span
9 - class="uppercase"
10 - :class="
11 - data.result === 'failed'
12 - ? 'text-error'
13 - : data.result === 'not applicable'
14 - ? 'text-warning'
15 - : 'text-success'
16 - "
17 - >
18 - {{ data.result }}
19 - </span>
20 - </n-statistic>
21 - <n-statistic label="Condition" tabular-nums>
22 - <span class="uppercase">{{ data.condition }}</span>
23 - </n-statistic>
24 - <n-statistic label="Compliance" :value="data.compliance.length" tabular-nums />
25 - <n-statistic label="Rules" :value="data.rules.length" tabular-nums />
26 - </div>
27 - </n-card>
28 - </div>
2 + <n-tabs type="line" animated :tabs-padding="24" pane-class="min-h-100 p-6!">
3 + <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy" class="flex flex-col gap-4">
4 + <n-card embedded class="overflow-hidden">
5 + <div class="flex flex-wrap justify-between gap-8">
6 + <n-statistic label="Result" tabular-nums>
7 + <span
8 + class="uppercase"
9 + :class="
10 + data.result === 'failed'
11 + ? 'text-error'
12 + : data.result === 'not applicable'
13 + ? 'text-warning'
14 + : 'text-success'
15 + "
16 + >
17 + {{ data.result }}
18 + </span>
19 + </n-statistic>
20 + <n-statistic label="Condition" tabular-nums>
21 + <span class="uppercase">{{ data.condition }}</span>
22 + </n-statistic>
23 + <n-statistic label="Compliance" :value="data.compliance.length" tabular-nums />
24 + <n-statistic label="Rules" :value="data.rules.length" tabular-nums />
25 + </div>
26 + </n-card>
27
30 - <div class="px-7">
31 - <n-card content-class="p-0!" embedded class="overflow-hidden">
32 - <div
33 - v-shiki="{ lang: 'shell', decode: true }"
34 - class="scrollbar-styled code-bg-transparent overflow-hidden"
35 - >
36 - <pre v-html="data.command"></pre>
37 - </div>
38 - </n-card>
39 - </div>
28 + <n-card content-class="p-0!" embedded class="overflow-hidden">
29 + <div
30 + v-shiki="{ lang: 'shell', decode: true }"
31 + class="scrollbar-styled code-bg-transparent overflow-hidden"
32 + >
33 + <pre v-html="data.command"></pre>
34 + </div>
35 + </n-card>
36
41 - <div v-if="properties" class="grid-auto-fit-200 grid gap-2 px-7">
37 + <div v-if="properties" class="grid-auto-fit-200 grid gap-2">
38 <CardKV v-for="(value, key) of properties" :key>
39 <template #key>
40 {{ key }}
@@ -50,93 +46,79 @@
46 </div>
47 </n-tab-pane>
48 <n-tab-pane name="Description" tab="Description" display-directive="show:lazy">
53 - <div class="p-7 pt-4">
54 - <n-input
55 - :value="data.description"
56 - type="textarea"
57 - readonly
58 - placeholder="Empty"
59 - size="large"
60 - :autosize="{
61 - minRows: 3,
62 - maxRows: 18
63 - }"
64 - />
65 - </div>
49 + <n-input
50 + :value="data.description"
51 + type="textarea"
52 + readonly
53 + placeholder="Empty"
54 + size="large"
55 + :autosize="{
56 + minRows: 3,
57 + maxRows: 18
58 + }"
59 + />
60 </n-tab-pane>
61 <n-tab-pane name="Rationale" tab="Rationale" display-directive="show:lazy">
68 - <div class="p-7 pt-4">
69 - <n-input
70 - :value="data.rationale"
71 - type="textarea"
72 - readonly
73 - placeholder="Empty"
74 - size="large"
75 - :autosize="{
76 - minRows: 3,
77 - maxRows: 18
78 - }"
79 - />
80 - </div>
62 + <n-input
63 + :value="data.rationale"
64 + type="textarea"
65 + readonly
66 + placeholder="Empty"
67 + size="large"
68 + :autosize="{
69 + minRows: 3,
70 + maxRows: 18
71 + }"
72 + />
73 </n-tab-pane>
74 <n-tab-pane name="Reason" tab="Reason" display-directive="show:lazy">
83 - <div class="p-7 pt-4">
84 - <n-input
85 - :value="data.reason"
86 - type="textarea"
87 - readonly
88 - placeholder="Empty"
89 - size="large"
90 - :autosize="{
91 - minRows: 3,
92 - maxRows: 18
93 - }"
94 - />
95 - </div>
75 + <n-input
76 + :value="data.reason"
77 + type="textarea"
78 + readonly
79 + placeholder="Empty"
80 + size="large"
81 + :autosize="{
82 + minRows: 3,
83 + maxRows: 18
84 + }"
85 + />
86 </n-tab-pane>
87 <n-tab-pane name="Remediation" tab="Remediation" display-directive="show:lazy">
98 - <div class="p-7 pt-4">
99 - <n-input
100 - :value="data.remediation"
101 - type="textarea"
102 - readonly
103 - placeholder="Empty"
104 - size="large"
105 - :autosize="{
106 - minRows: 3,
107 - maxRows: 18
108 - }"
109 - />
110 - </div>
88 + <n-input
89 + :value="data.remediation"
90 + type="textarea"
91 + readonly
92 + placeholder="Empty"
93 + size="large"
94 + :autosize="{
95 + minRows: 3,
96 + maxRows: 18
97 + }"
98 + />
99 </n-tab-pane>
100 <n-tab-pane name="Compliance" tab="Compliance" display-directive="show:lazy">
113 - <div class="flex flex-col gap-1 p-7 pt-4">
114 - <n-card
115 - v-for="item of data.compliance"
116 - :key="item.key"
117 - content-class="flex flex-col gap-2"
118 - embedded
119 - class="overflow-hidden"
120 - size="small"
121 - >
122 - <div>{{ item.key }}</div>
123 - <p>{{ item.value }}</p>
124 - </n-card>
101 + <div class="flex flex-col gap-2">
102 + <CardKV v-for="item of data.compliance" :key="item.key">
103 + <template #key>
104 + {{ item.key }}
105 + </template>
106 + <template #value>
107 + {{ item.value ?? "-" }}
108 + </template>
109 + </CardKV>
110 </div>
111 </n-tab-pane>
112 <n-tab-pane name="Rules" tab="Rules" display-directive="show:lazy">
128 - <div class="flex flex-col gap-1 p-7 pt-4">
129 - <n-card
130 - v-for="item of data.rules"
131 - :key="item.type + item.rule"
132 - content-class="flex flex-col gap-2"
133 - embedded
134 - class="overflow-hidden"
135 - size="small"
136 - >
137 - <div>{{ item.type }}</div>
138 - <p>{{ item.rule }}</p>
139 - </n-card>
113 + <div class="flex flex-col gap-2">
114 + <CardKV v-for="item of data.rules" :key="item.type + item.rule">
115 + <template #key>
116 + {{ item.type }}
117 + </template>
118 + <template #value>
119 + {{ item.rule }}
120 + </template>
121 + </CardKV>
122 </div>
123 </n-tab-pane>
124 </n-tabs>
frontend/src/components/agents/sca/ScaResults.vue
+1 -1
@@ -41,7 +41,7 @@
41 :item-count="itemsFiltered.length"
42 :simple="simpleMode"
43 />
44 - <n-popover overlap placement="right" class="px-0!">
44 + <n-popover overlap placement="right" class="px-0!" to="body">
45 <template #trigger>
46 <div class="bg-default rounded-lg">
47 <n-button size="small">
frontend/src/components/agents/vulnerabilities/VulnerabilityCard.vue
+2 -6
@@ -60,11 +60,11 @@
60 class="vulnerability-dialog"
61 :title="extract"
62 content-class="p-0!"
63 - style="width: 90vw; max-width: 1000px"
63 + style="width: 90vw; max-width: 1000px; min-height: min(500px, 90vh)"
64 >
65 <n-tabs type="line" animated :tabs-padding="24">
66 <n-tab-pane name="Details" tab="Details" display-directive="show">
67 - <div class="p-7 pt-4 pb-0">
67 + <div class="p-6 pt-3 pb-0">
68 <CardKV>
69 <template #key>title</template>
70 <template #value>
@@ -260,8 +260,6 @@ const showDialog = ref(false)
260 border-color: var(--error-color);
261 }
262 }
263 - &.severity-Low {
264 - }
263 &.severity-Medium {
264 border-color: var(--warning-color);
265 .severity {
@@ -269,7 +267,5 @@ const showDialog = ref(false)
267 border-color: var(--warning-color);
268 }
269 }
272 - &.severity-Untriaged {
273 - }
270 }
271 </style>
frontend/src/components/aiChatbot/ChatButton.vue
+1 -1
@@ -1,6 +1,6 @@
1 <template>
2 <div>
3 - <div class="ai-chatbot-button-wrapper fixed right-0 bottom-10 h-10 w-15" @click="showDrawer = true">
3 + <div class="ai-chatbot-button-wrapper fixed right-0 bottom-20 h-10 w-15" @click="showDrawer = true">
4 <n-tooltip trigger="hover" placement="left">
5 <template #trigger>
6 <n-float-button type="primary" class="ai-chatbot-button">
frontend/src/components/alerts/Alert.vue
+5 -5
@@ -129,7 +129,7 @@
129 >
130 <n-tabs type="line" animated :tabs-padding="24">
131 <n-tab-pane v-if="alert._id" name="Agent" tab="Agent" display-directive="show">
132 - <div v-if="agentProperties" class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
132 + <div v-if="agentProperties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
133 <CardKV v-for="(value, key) of agentProperties" :key>
134 <template #key>
135 {{ key }}
@@ -168,7 +168,7 @@
168 tab="SOCFortress Response"
169 display-directive="show"
170 >
171 - <div class="p-7 pt-4">
171 + <div class="p-6 pt-3">
172 <n-input
173 :value="alert._source.ask_socfortress_message"
174 type="textarea"
@@ -182,7 +182,7 @@
182 </div>
183 </n-tab-pane>
184 <n-tab-pane v-if="alert._source.message" name="Message" tab="Message" display-directive="show">
185 - <div class="p-7 pt-4">
185 + <div class="p-6 pt-3">
186 <CodeSource :code="alert._source.message" :decode="false" />
187 </div>
188 </n-tab-pane>
@@ -192,7 +192,7 @@
192 tab="Data document"
193 display-directive="show"
194 >
195 - <div class="p-7 pt-4">
195 + <div class="p-6 pt-3">
196 <n-input
197 :value="alert._source.data_document"
198 type="textarea"
@@ -206,7 +206,7 @@
206 </div>
207 </n-tab-pane>
208 <n-tab-pane name="Details" tab="Details" display-directive="show:lazy">
209 - <div class="p-7 pt-4">
209 + <div class="p-6 pt-3">
210 <CodeSource :code="alert._source" lang="json" :decode="false" />
211 </div>
212 </n-tab-pane>
frontend/src/components/alerts/AlertWazuhRules.vue
+2 -2
@@ -1,12 +1,12 @@
1 <template>
2 <n-tabs type="line" animated :tabs-padding="24">
3 <n-tab-pane name="Wazuh Rules" tab="Wazuh Rules" display-directive="show">
4 - <div class="p-7 pt-4">
4 + <div class="p-6 pt-3">
5 <CodeSource :code="wazuh_rule" lang="xml" />
6 </div>
7 </n-tab-pane>
8 <n-tab-pane name="Explanation" tab="Explanation" display-directive="show">
9 - <div class="p-7 pt-4">
9 + <div class="p-6 pt-3">
10 <n-input
11 :value="data.explanation"
12 type="textarea"
frontend/src/components/common/Badge.vue
+5 -3
@@ -40,7 +40,7 @@ const { type, hintCursor, pointCursor, color, href, fluid, size } = defineProps<
40 align-items: center;
41 font-size: 14px;
42 padding: 0px 6px;
43 - height: 26px;
43 + height: 28px;
44 line-height: 1;
45 gap: 6px;
46 transition: all 0.3s var(--bezier-ease);
@@ -162,7 +162,7 @@ const { type, hintCursor, pointCursor, color, href, fluid, size } = defineProps<
162 }
163
164 &.fluid {
165 - min-height: 26px;
165 + min-height: 28px;
166 height: unset;
167
168 &.splitted {
@@ -180,7 +180,9 @@ const { type, hintCursor, pointCursor, color, href, fluid, size } = defineProps<
180
181 &.badge-size-small {
182 font-size: 12px;
183 - height: 20px;
183 + height: 22px;
184 + min-height: 22px;
185 + border-radius: var(--border-radius-small);
186 }
187 }
188 </style>
frontend/src/components/common/cards/CardEntity.vue
+17 -6
@@ -22,14 +22,14 @@
22 </div>
23
24 <div
25 - v-if="!$slots.header && ($slots.headerMain || $slots.headerExtra)"
25 + v-if="!$slots.header && ($slots.headerMain || ($slots.headerExtra && !hideHeaderExtra))"
26 class="header-box flex flex-wrap items-center justify-between"
27 :class="headerBoxClass"
28 >
29 <div>
30 <slot name="headerMain" />
31 </div>
32 - <div>
32 + <div v-if="$slots.headerExtra && !hideHeaderExtra">
33 <slot name="headerExtra" />
34 </div>
35 </div>
@@ -39,23 +39,26 @@
39 </div>
40 </div>
41
42 - <div v-if="$slots.mainExtra" class="extra-box">
42 + <div v-if="$slots.mainExtra && !hideMainExtra" class="extra-box">
43 <slot name="mainExtra" />
44 </div>
45
46 - <div v-if="$slots.footer" class="footer-box" :class="footerBoxClass">
46 + <div v-if="$slots.footer && !hideFooter" class="footer-box" :class="footerBoxClass">
47 <slot name="footer" />
48 </div>
49
50 <div
51 - v-if="!$slots.footer && ($slots.footerMain || $slots.footerExtra)"
51 + v-if="
52 + (!$slots.footer || hideFooter) &&
53 + ($slots.footerMain || ($slots.footerExtra && !hideFooterExtra))
54 + "
55 class="footer-box flex flex-wrap items-start justify-between"
56 :class="footerBoxClass"
57 >
58 <div>
59 <slot name="footerMain" />
60 </div>
58 - <div>
61 + <div v-if="$slots.footerExtra && !hideFooterExtra">
62 <slot name="footerExtra" />
63 </div>
64 </div>
@@ -77,6 +80,10 @@ const {
80 disabled,
81 loading,
82 loadingDescription,
83 + hideFooter,
84 + hideFooterExtra,
85 + hideHeaderExtra,
86 + hideMainExtra,
87 mainBoxClass,
88 headerBoxClass,
89 cardEntityClass,
@@ -92,6 +99,10 @@ const {
99 disabled?: boolean
100 loading?: boolean
101 loadingDescription?: string
102 + hideFooter?: boolean
103 + hideFooterExtra?: boolean
104 + hideHeaderExtra?: boolean
105 + hideMainExtra?: boolean
106 mainBoxClass?: string
107 headerBoxClass?: string
108 cardEntityClass?: string
frontend/src/components/customers/CustomerAgents.vue
+26 -49
@@ -1,6 +1,6 @@
1 <template>
2 <n-spin :show="loading">
3 - <div class="flex min-h-28 flex-col gap-2">
3 + <div class="flex min-h-28 flex-col gap-2 p-px">
4 <div v-if="sortedList.length" class="mb-2 flex justify-end">
5 <n-button type="primary" @click="exportToCSV">
6 <template #icon>
@@ -16,9 +16,21 @@
16 embedded
17 show-actions
18 class="item-appear item-appear-bottom item-appear-005"
19 - @delete="getAgents()"
20 - @click="openAgentInNewTab(agent.agent_id)"
21 - />
19 + >
20 + <template #actions-left>
21 + <n-tooltip to="body">
22 + <span class="text-sm">Open in new tab</span>
23 + <template #trigger>
24 + <n-button text size="small" type="primary" @click.stop="openAgentInNewTab(agent.agent_id)">
25 + <template #icon>
26 + <Icon name="carbon:launch" />
27 + </template>
28 + Open
29 + </n-button>
30 + </template>
31 + </n-tooltip>
32 + </template>
33 + </AgentCard>
34 <n-empty v-if="!sortedList.length" description="No Agents found" class="h-48 justify-center" />
35 </div>
36 </n-spin>
@@ -28,7 +40,10 @@
40 // TODO-FE: refactor
41 import type { Agent } from "@/types/agents.d"
42 import type { Customer } from "@/types/customers.d"
31 -import { NButton, NEmpty, NSpin, useMessage } from "naive-ui"
43 +import { saveAs } from "file-saver"
44 +import _sortBy from "lodash/sortBy"
45 +import { NButton, NEmpty, NSpin, NTooltip, useMessage } from "naive-ui"
46 +import Papa from "papaparse"
47 import { computed, onBeforeMount, ref, toRefs } from "vue"
48 import { useRouter } from "vue-router"
49 import Api from "@/api"
@@ -41,18 +56,13 @@ const props = defineProps<{
56 const { customer } = toRefs(props)
57
58 const DownloadIcon = "carbon:download"
44 -const QUOTE_ESCAPE_REGEX = /"/g
59
60 const loading = ref(false)
61 const router = useRouter()
62 const message = useMessage()
63 const list = ref<Agent[] | []>([])
64
51 -const sortedList = computed(() => {
52 - return list.value.toSorted((a, b) => {
53 - return a.hostname.toLowerCase().localeCompare(b.hostname.toLowerCase())
54 - })
55 -})
65 +const sortedList = computed(() => _sortBy(list.value, "critical_asset").reverse())
66
67 function exportToCSV() {
68 if (!sortedList.value.length) {
@@ -60,46 +70,13 @@ function exportToCSV() {
70 return
71 }
72
63 - // CSV headers
64 - const headers = [
65 - "customer_code",
66 - "agent_id",
67 - "hostname",
68 - "ip_address",
69 - "os",
70 - "wazuh_last_seen",
71 - "velociraptor_last_seen"
72 - ]
73 -
74 - // Create CSV content
75 - const csvContent = [
76 - headers.join(","),
77 - ...sortedList.value.map(agent => {
78 - return [
79 - agent.customer_code || "",
80 - agent.agent_id,
81 - `"${agent.hostname.replace(QUOTE_ESCAPE_REGEX, '""')}"`, // Escape quotes in hostname
82 - agent.ip_address,
83 - `"${agent.os.replace(QUOTE_ESCAPE_REGEX, '""')}"`, // Escape quotes in OS
84 - agent.wazuh_last_seen,
85 - agent.velociraptor_last_seen || ""
86 - ].join(",")
87 - })
88 - ].join("\n")
89 -
90 - // Create blob and download
91 - const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" })
92 - const link = document.createElement("a")
93 - const url = URL.createObjectURL(blob)
73 + const csv = Papa.unparse(sortedList.value, {
74 + header: true
75 + })
76
77 const filename = `agents_${customer.value.customer_code}_${new Date().toISOString().split("T")[0]}.csv`
96 -
97 - link.setAttribute("href", url)
98 - link.setAttribute("download", filename)
99 - link.style.visibility = "hidden"
100 - document.body.appendChild(link)
101 - link.click()
102 - document.body.removeChild(link)
78 + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" })
79 + saveAs(blob, filename)
80
81 message.success("Export completed successfully")
82 }
frontend/src/components/customers/CustomerItem.vue
+8 -17
@@ -1,6 +1,6 @@
1 <template>
2 <div :id="`customer-${customer.customer_code}`">
3 - <CardEntity :loading :highlighted="!!highlight">
3 + <CardEntity :loading :highlighted="!!highlight" :hide-footer-extra="!!hideCardActions">
4 <template #default>
5 <div class="flex items-start gap-4">
6 <n-avatar
@@ -123,7 +123,7 @@
123 </div>
124 </template>
125
126 - <template v-if="!hideCardActions" #footerExtra>
126 + <template #footerExtra>
127 <n-button size="small" @click.stop="showDetails = true">
128 <template #icon>
129 <Icon :name="DetailsIcon" />
@@ -207,7 +207,7 @@
207 name="AI Notifications"
208 tab="AI Notifications"
209 display-directive="show:lazy"
210 - class="p-4!"
210 + class="overflow-hidden p-4!"
211 >
212 <CustomerAiNotifications :customer-code="customer.customer_code" />
213 </n-tab-pane>
@@ -283,7 +283,6 @@
283 <script setup lang="ts">
284 // TODO-FE: refactor
285 import type { Customer, CustomerMeta } from "@/types/customers.d"
286 -import _toSafeInteger from "lodash/toSafeInteger"
286 import { NAvatar, NButton, NModal, NPopover, NScrollbar, NTabPane, NTabs, useMessage } from "naive-ui"
287 import { computed, defineAsyncComponent, onBeforeMount, ref, toRefs, watch } from "vue"
288 import Api from "@/api"
@@ -291,7 +290,7 @@ import Badge from "@/components/common/Badge.vue"
290 import CardEntity from "@/components/common/cards/CardEntity.vue"
291 import Icon from "@/components/common/Icon.vue"
292 import { useCustomerHealthcheckFilters } from "@/composables/useCustomerHealthcheckFilters"
294 -import { hashMD5 } from "@/utils"
293 +import { getAvatar, getNameInitials } from "@/utils"
294
295 const props = defineProps<{
296 customer: Customer
@@ -339,20 +338,12 @@ const agentCount = ref<number | null>(null)
338 const { healthcheckFilters } = useCustomerHealthcheckFilters()
339
340 const loading = computed(() => loadingFull.value || loadingDelete.value)
342 -const fallbackAvatar = computed(() => {
343 - let text = customer.value.customer_name.slice(0, 2).toUpperCase()
344 -
345 - if (customer.value.customer_name.includes(" ")) {
346 - const chunks = customer.value.customer_name.split(" ")
347 - text = ((chunks[0]?.[0] ?? "") + (chunks[1]?.[0] ?? "")).toUpperCase()
348 - }
341
350 - const hash = hashMD5(customer.value.customer_code)
351 - const uniq = hash.split("").find(o => _toSafeInteger(o).toString() === o)
352 - const seed = hash.slice(0, _toSafeInteger(uniq))
353 -
354 - return `https://avatar.vercel.sh/${seed}.svg?text=${text}`
342 +const fallbackAvatar = computed(() => {
343 + const initials = getNameInitials(customer.value.customer_name)
344 + return getAvatar({ seed: initials, text: initials })
345 })
346 +
347 const addressLabel = computed(
348 () => [customerInfo.value?.city, customerInfo.value?.state].filter(o => !!o).join(", ") || "-"
349 )
frontend/src/components/customers/aiNotifications/CustomerAiNotificationDispatchLog.vue
+13 -19
@@ -1,6 +1,6 @@
1 <template>
2 - <div class="customer-ai-notification-dispatch-log">
3 - <div class="flex items-center justify-between gap-4 px-7 pt-2">
2 + <div class="flex flex-col gap-4">
3 + <div class="flex items-center justify-between gap-4">
4 <div class="text-secondary text-sm">
5 Recent notification dispatches for this customer (newest first, capped at 100).
6 </div>
@@ -13,7 +13,7 @@
13 </div>
14
15 <n-spin :show="loading">
16 - <div class="min-h-52 p-7 pt-4">
16 + <div class="min-h-52">
17 <n-empty
18 v-if="!loading && !entries.length"
19 description="No dispatches yet"
@@ -24,8 +24,8 @@
24 v-else-if="entries.length"
25 :columns
26 :data="entries"
27 + :scroll-x="1000"
28 size="small"
28 - :bordered="false"
29 :row-key="(r: DispatchLogEntry) => r.id"
30 />
31 </div>
@@ -41,6 +41,7 @@ import { computed, h, onBeforeMount, ref } from "vue"
41 import Api from "@/api"
42 import Badge from "@/components/common/Badge.vue"
43 import Icon from "@/components/common/Icon.vue"
44 +import { useSettingsStore } from "@/stores/settings"
45 import { getApiErrorMessage } from "@/utils"
46 import { formatDate } from "@/utils/format"
47
@@ -54,6 +55,8 @@ const message = useMessage()
55 const loading = ref(false)
56 const entries = ref<DispatchLogEntry[]>([])
57
58 +const dFormats = useSettingsStore().dateFormat
59 +
60 function statusColor(status: string): "success" | "warning" | "danger" | undefined {
61 if (status === "sent") return "success"
62 if (status === "skipped") return "warning"
@@ -66,7 +69,7 @@ const columns = computed<DataTableColumns<DispatchLogEntry>>(() => [
69 title: "When",
70 key: "dispatched_at",
71 width: 160,
69 - render: row => String(formatDate(row.dispatched_at, "MMM D, YYYY HH:mm:ss"))
72 + render: row => String(formatDate(row.dispatched_at, dFormats.datetime))
73 },
74 {
75 title: "Alert",
@@ -83,11 +86,11 @@ const columns = computed<DataTableColumns<DispatchLogEntry>>(() => [
86 {
87 title: "Status",
88 key: "status",
86 - width: 110,
89 + minWidth: 130,
90 render: row =>
91 h(
92 Badge,
90 - { type: "splitted", color: statusColor(row.status) },
93 + { type: "splitted", color: statusColor(row.status), class: "whitespace-nowrap" },
94 {
95 label: () => "Status",
96 value: () => row.status
@@ -103,18 +106,9 @@ const columns = computed<DataTableColumns<DispatchLogEntry>>(() => [
106 {
107 title: "Error / Preview",
108 key: "detail",
106 - // Long column — collapses content with title on hover for full text.
107 - render: row => {
108 - const text = row.error_message || row.payload_preview || ""
109 - return h(
110 - "div",
111 - {
112 - class: "truncate max-w-md",
113 - title: text
114 - },
115 - text
116 - )
117 - }
109 + maxWidth: 200,
110 + ellipsis: { tooltip: { to: "body ", class: "max-w-[90vw] text-sm" } },
111 + render: row => row.error_message || row.payload_preview || ""
112 }
113 ])
114
frontend/src/components/customers/aiNotifications/CustomerAiNotifications.vue
+1 -1
@@ -12,7 +12,7 @@
12 >
13 <CustomerShuffleIntegrations :customer-code />
14 </n-tab-pane>
15 - <n-tab-pane name="dispatch_log" tab="Dispatch log" display-directive="show:lazy" class="pt-4!">
15 + <n-tab-pane name="dispatch_log" tab="Dispatch log" display-directive="show:lazy" class="pt-3!">
16 <CustomerAiNotificationDispatchLog :customer-code />
17 </n-tab-pane>
18 </n-tabs>
frontend/src/components/customers/eventSources/CustomerEventSourceItem.vue
+12 -17
@@ -1,14 +1,18 @@
1 <template>
2 <div>
3 - <CardEntity hoverable :embedded>
4 - <template #default>
5 - <div class="flex items-center gap-3">
6 - <Icon :name="SourceIcon" :size="18" />
7 - <span class="font-semibold">{{ source.name }}</span>
8 - </div>
3 + <CardEntity hoverable :embedded header-box-class="text-default!">
4 + <template #headerMain>
5 + {{ source.name }}
6 </template>
10 -
11 - <template #footerMain>
7 + <template #headerExtra>
8 + <Badge type="splitted" :color="source.enabled ? 'success' : 'danger'" bright>
9 + <template #iconLeft>
10 + <Icon :name="source.enabled ? 'carbon:checkmark' : 'carbon:close'" :size="13" />
11 + </template>
12 + <template #value>{{ source.enabled ? "Enabled" : "Disabled" }}</template>
13 + </Badge>
14 + </template>
15 + <template #default>
16 <div class="flex flex-wrap items-center gap-3">
17 <Badge type="splitted" color="primary">
18 <template #iconLeft>
@@ -33,13 +37,6 @@
37 <template #label>Time Field</template>
38 <template #value>{{ source.time_field }}</template>
39 </Badge>
36 -
37 - <Badge type="splitted" :color="source.enabled ? 'success' : 'danger'" bright>
38 - <template #iconLeft>
39 - <Icon :name="StatusIcon" :size="13" />
40 - </template>
41 - <template #value>{{ source.enabled ? "Enabled" : "Disabled" }}</template>
42 - </Badge>
40 </div>
41 </template>
42
@@ -82,11 +79,9 @@ const emit = defineEmits<{
79 (e: "deleted"): void
80 }>()
81
85 -const SourceIcon = "carbon:data-base"
82 const TypeIcon = "carbon:category"
83 const IndexIcon = "carbon:catalog"
84 const TimeIcon = "carbon:time"
89 -const StatusIcon = "carbon:circle-dash"
85 const EditIcon = "carbon:edit"
86 const DeleteIcon = "ph:trash"
87
frontend/src/components/customers/eventSources/CustomerEventSources.vue
+1 -1
@@ -10,7 +10,7 @@
10 @close="closeForm()"
11 />
12 </div>
13 - <div v-else>
13 + <div v-else class="flex flex-col gap-4">
14 <div class="flex items-center justify-between gap-4">
15 <n-button size="small" type="primary" @click="openForm()">
16 <template #icon>
frontend/src/components/graylog/Events/Item.vue
+2 -2
@@ -41,7 +41,7 @@
41 >
42 <n-tabs type="line" animated :tabs-padding="24">
43 <n-tab-pane name="query" tab="Query" display-directive="show">
44 - <div class="p-7 pt-4">
44 + <div class="p-6 pt-3">
45 <n-input
46 :value="event?.config?.query"
47 type="textarea"
@@ -56,7 +56,7 @@
56 </div>
57 </n-tab-pane>
58 <n-tab-pane name="fieldSpec" tab="Field Spec" display-directive="show:lazy">
59 - <div class="p-7 pt-4">
59 + <div class="p-6 pt-3">
60 <SimpleJsonViewer
61 class="vuesjv-override"
62 :model-value="event.field_spec"
frontend/src/components/graylog/Inputs/Item.vue
+2 -2
@@ -72,7 +72,7 @@
72 >
73 <n-tabs type="line" animated :tabs-padding="24">
74 <n-tab-pane name="info" tab="Info" display-directive="show:lazy">
75 - <div class="p-7 pt-4">
75 + <div class="p-6 pt-3">
76 <div class="mb-2">
77 Id :
78 <code>{{ input.id }}</code>
@@ -98,7 +98,7 @@
98 </div>
99 </n-tab-pane>
100 <n-tab-pane name="attributes" tab="Attributes" display-directive="show:lazy">
101 - <div class="p-7 pt-4">
101 + <div class="p-6 pt-3">
102 <SimpleJsonViewer
103 class="vuesjv-override"
104 :model-value="input.attributes"
frontend/src/components/graylog/Pipelines/PipeInfo.vue
+2 -2
@@ -1,7 +1,7 @@
1 <template>
2 <n-tabs type="line" animated :tabs-padding="24">
3 <n-tab-pane name="info" tab="Info" display-directive="show">
4 - <div class="p-7 pt-4">
4 + <div class="p-6 pt-3">
5 <div class="mb-2">
6 Id :
7 <code>{{ pipeline?.id }}</code>
@@ -25,7 +25,7 @@
25 </div>
26 </n-tab-pane>
27 <n-tab-pane name="source" tab="Source" display-directive="show">
28 - <div class="p-7 pt-4">
28 + <div class="p-6 pt-3">
29 <n-input
30 :value="pipeline?.source"
31 type="textarea"
frontend/src/components/graylog/Pipelines/Rule.vue
+1 -1
@@ -43,7 +43,7 @@
43 :bordered="false"
44 segmented
45 >
46 - <div class="p-7 pt-4">
46 + <div class="p-6 pt-3">
47 <div class="mb-2">
48 Created:
49 <code>{{ formatDateTime(rule.created_at) }}</code>
frontend/src/components/incidentManagement/alerts/AlertAsset.vue
+3 -3
@@ -91,7 +91,7 @@
91 </n-tab-pane>
92 <n-tab-pane name="Context" tab="Context" display-directive="show">
93 <n-spin :show="loading" class="min-h-40">
94 - <div v-if="alertContext" class="p-7 pt-4">
94 + <div v-if="alertContext" class="p-6 pt-3">
95 <div class="mb-4 flex flex-wrap gap-3">
96 <Badge type="splitted">
97 <template #label>id</template>
@@ -115,7 +115,7 @@
115 tab="Investigate"
116 display-directive="show:lazy"
117 >
118 - <div class="p-7 pt-4">
118 + <div class="p-6 pt-3">
119 <div class="flex flex-wrap gap-2">
120 <ThreatIntelProcessEvaluationProvider
121 v-for="pn of processNameList"
@@ -141,7 +141,7 @@
141 tab="CoPilot Searches"
142 display-directive="show:lazy"
143 >
144 - <div class="p-7 pt-4">
144 + <div class="p-6 pt-3">
145 <AlertAssetSearches :asset />
146 </div>
147 </n-tab-pane>
frontend/src/components/incidentManagement/alerts/AlertAssetInfo.vue
+3 -3
@@ -1,5 +1,5 @@
1 <template>
2 - <div class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
2 + <div class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
3 <CardKV v-for="(value, key) of asset" :key>
4 <template #key>
5 {{ key }}
@@ -61,7 +61,7 @@
61 <n-spin :show="loading" class="min-h-40">
62 <n-tabs type="line" animated :tabs-padding="24">
63 <n-tab-pane name="Info" tab="Info" display-directive="show">
64 - <div class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
64 + <div class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
65 <CardKV v-for="(value, key) of alertDetailsInfo" :key>
66 <template #key>
67 {{ key }}
@@ -84,7 +84,7 @@
84 </div>
85 </n-tab-pane>
86 <n-tab-pane name="Source" tab="Source" display-directive="show">
87 - <div v-if="alertDetailsSource" class="p-7 pt-4">
87 + <div v-if="alertDetailsSource" class="p-6 pt-3">
88 <CodeSource :code="alertDetailsSource" lang="json" />
89 </div>
90 </n-tab-pane>
frontend/src/components/incidentManagement/alerts/AlertDetailTimelineItem.vue
+2 -2
@@ -15,7 +15,7 @@
15 >
16 <n-tabs type="line" animated :tabs-padding="24">
17 <n-tab-pane name="Info" tab="Info" display-directive="show">
18 - <div class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
18 + <div class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
19 <CardKV v-for="(value, key) of timelineDetailsInfo" :key>
20 <template #key>
21 {{ key }}
@@ -38,7 +38,7 @@
38 </div>
39 </n-tab-pane>
40 <n-tab-pane name="Source" tab="Source" display-directive="show">
41 - <div v-if="timelineDetailsSource" class="p-7 pt-4">
41 + <div v-if="timelineDetailsSource" class="p-6 pt-3">
42 <CodeSource :code="timelineDetailsSource" lang="json" />
43 </div>
44 </n-tab-pane>
frontend/src/components/incidentManagement/alerts/AlertDetails.vue
+6 -6
@@ -22,22 +22,22 @@
22 <span v-if="hasAiReport" class="bg-primary inline-block h-2 w-2 animate-pulse rounded-full" />
23 </div>
24 </template>
25 - <div class="p-7 pt-4">
25 + <div class="p-6 pt-3">
26 <AlertAiAnalyst :alert-id="alert.id" />
27 </div>
28 </n-tab-pane>
29 <n-tab-pane name="Timeline" tab="Timeline" display-directive="show:lazy">
30 - <div class="p-7 pt-4">
30 + <div class="p-6 pt-3">
31 <AlertTimeline :alert />
32 </div>
33 </n-tab-pane>
34 <n-tab-pane name="Assets" tab="Assets" display-directive="show:lazy">
35 - <div class="p-7 pt-4">
35 + <div class="p-6 pt-3">
36 <AlertAssetsList :assets="alert.assets" />
37 </div>
38 </n-tab-pane>
39 <n-tab-pane name="Comments" tab="Comments" display-directive="show:lazy">
40 - <div class="p-7 pt-4">
40 + <div class="p-6 pt-3">
41 <AlertCommentsList
42 :comments="alert.comments"
43 :alert-id="alert.id"
@@ -46,7 +46,7 @@
46 </div>
47 </n-tab-pane>
48 <n-tab-pane name="IoCs" tab="IoCs" display-directive="show:lazy">
49 - <div class="p-7 pt-4">
49 + <div class="p-6 pt-3">
50 <AlertIoCsList :iocs="alert.iocs" :alert-id="alert.id" @updated="updateIos($event)" />
51 </div>
52 </n-tab-pane>
@@ -57,7 +57,7 @@
57 <span>Try MCP</span>
58 </div>
59 </template>
60 - <div class="p-7 pt-4">
60 + <div class="p-6 pt-3">
61 <AlertTryMcp :alert />
62 </div>
63 </n-tab-pane>
frontend/src/components/incidentManagement/caseTemplates/CaseTemplateEditor.vue
+35 -36
@@ -48,25 +48,20 @@
48 never trigger. Example: field "data_win_system_eventID", value "1" applies
49 this template only to Sysmon Event ID 1 events.
50 -->
51 - <n-card size="small" title="Conditional auto-apply (optional)">
51 + <n-card size="small" class="my-4" title="Conditional auto-apply (optional)">
52 <template #header-extra>
53 - <div v-if="matchHalfSet" class="text-warning text-xs">
54 - Both field and value are required
55 - </div>
53 + <div v-if="matchHalfSet" class="text-warning text-xs">Both field and value are required</div>
54 </template>
55 <p class="text-secondary mb-2 text-xs">
56 When set, auto-apply only fires if the originating Wazuh document has
59 - <code>{{ form.match_field || "<field>" }}</code>
57 + <code>{{ form.match_field || "[field]" }}</code>
58 equal to
61 - <code>{{ form.match_value || "<value>" }}</code>
59 + <code>{{ form.match_value || "[value]" }}</code>
60 . Leave blank for an unconditional template.
61 </p>
62 <div class="grid grid-cols-1 gap-3 @md:grid-cols-2">
63 <n-form-item label="Match field" path="match_field" :show-feedback="false">
66 - <n-input
67 - v-model:value="form.match_field"
68 - placeholder="e.g., data_win_system_eventID"
69 - />
64 + <n-input v-model:value="form.match_field" placeholder="e.g., data_win_system_eventID" />
65 </n-form-item>
66 <n-form-item label="Match value" path="match_value" :show-feedback="false">
67 <n-input v-model:value="form.match_value" placeholder="e.g., 1" />
@@ -74,7 +69,7 @@
69 </div>
70 </n-card>
71
77 - <n-card size="small" title="Tasks" content-class="flex flex-col gap-3">
72 + <n-card size="small" class="my-4" title="Tasks" content-class="flex flex-col gap-3">
73 <template #header-extra>
74 <div
75 v-if="taskSaving"
@@ -152,7 +147,7 @@
147 </div>
148 </n-card>
149
155 - <div class="mt-4 flex justify-end gap-2">
150 + <div class="flex justify-end gap-2">
151 <n-button @click="emit('cancel')">Cancel</n-button>
152 <n-button type="primary" :loading="saving" @click="handleSave">
153 {{ props.template ? "Save changes" : "Create template" }}
@@ -177,9 +172,9 @@ import { getApiErrorMessage } from "@/utils"
172 interface DraftTask {
173 _key: string // stable client-side key for v-for
174 id?: number // present when persisted (template_task_id from backend)
180 - title: string
181 - description: string
182 - guidelines: string
175 + title: string | null
176 + description: string | null
177 + guidelines: string | null
178 mandatory: boolean
179 order_index: number
180 }
@@ -195,7 +190,7 @@ interface FormModel {
190 }
191
192 const props = defineProps<{
198 - template: CaseTemplate | null
193 + template?: CaseTemplate | null
194 }>()
195
196 const emit = defineEmits<{
@@ -245,25 +240,25 @@ function nextKey() {
240 return `t${Date.now()}-${keyCounter}`
241 }
242
248 -function loadFromTemplate(t: CaseTemplate | null) {
243 +function loadFromTemplate(t?: CaseTemplate | null) {
244 if (t) {
245 form.value = {
251 - name: t.name,
252 - description: t.description ?? "",
253 - customer_code: t.customer_code ?? "",
254 - source: t.source ?? "",
246 + name: t.name ?? null,
247 + description: t.description ?? null,
248 + customer_code: t.customer_code ?? null,
249 + source: t.source ?? null,
250 is_default: t.is_default,
251 match_field: t.match_field ?? null,
252 match_value: t.match_value ?? null
253 }
254 tasks.value = (t.tasks ?? []).map(task => ({
255 _key: nextKey(),
261 - id: task.id,
262 - title: task.title,
263 - description: task.description ?? "",
264 - guidelines: task.guidelines ?? "",
265 - mandatory: task.mandatory,
266 - order_index: task.order_index
256 + id: task.id ?? null,
257 + title: task.title ?? null,
258 + description: task.description ?? null,
259 + guidelines: task.guidelines ?? null,
260 + mandatory: task.mandatory ?? false,
261 + order_index: task.order_index ?? 0
262 }))
263 } else {
264 form.value = {
@@ -278,9 +273,9 @@ function loadFromTemplate(t: CaseTemplate | null) {
273 tasks.value = [
274 {
275 _key: nextKey(),
281 - title: "",
282 - description: "",
283 - guidelines: "",
276 + title: null,
277 + description: null,
278 + guidelines: null,
279 mandatory: false,
280 order_index: 0
281 }
@@ -352,14 +347,14 @@ async function saveTask(idx: number) {
347
348 const draft = tasks.value[idx]
349
355 - if (!draft.title.trim()) return // skip empty drafts; user is still typing
350 + if (!draft.title?.trim()) return // skip empty drafts; user is still typing
351
352 taskSaving.value = true
353
354 const payload = {
360 - title: draft.title,
361 - description: draft.description || null,
362 - guidelines: draft.guidelines || null,
355 + title: draft.title ?? "",
356 + description: draft.description || "",
357 + guidelines: draft.guidelines || "",
358 mandatory: draft.mandatory,
359 order_index: draft.order_index
360 }
@@ -418,9 +413,9 @@ async function handleSave() {
413 }
414 } else {
415 const cleanTasks = tasks.value
421 - .filter(t => t.title.trim().length > 0)
416 + .filter(t => (t.title?.trim() || "").length > 0)
417 .map(t => ({
423 - title: t.title,
418 + title: t.title ?? "",
419 description: t.description || null,
420 guidelines: t.guidelines || null,
421 mandatory: t.mandatory,
@@ -489,4 +484,8 @@ onBeforeMount(() => {
484 getCustomers()
485 getConfiguredSources()
486 })
487 +
488 +defineExpose({
489 + validate: () => formRef.value?.restoreValidation()
490 +})
491 </script>
frontend/src/components/incidentManagement/caseTemplates/CaseTemplateLibraryImportModal.vue
+55 -116
@@ -1,6 +1,6 @@
1 <template>
2 <n-modal
3 - v-model:show="showLocal"
3 + v-model:show="show"
4 preset="card"
5 display-directive="show"
6 :title="entry ? `Import — ${entry.name}` : 'Import library entry'"
@@ -10,58 +10,61 @@
10 <template v-if="entry">
11 <div class="flex flex-col gap-3">
12 <n-alert v-if="!result" type="info" :show-icon="false">
13 - This will create a new
14 - <strong>global</strong>
15 - case template (no customer or source scope). If you want a customer-specific version,
16 - edit the template after import.
13 + <div class="text-sm">
14 + This will create a new
15 + <strong>global</strong>
16 + case template (no customer or source scope). If you want a customer-specific version, edit the
17 + template after import.
18 + </div>
19 </n-alert>
20
21 <div v-if="!result" class="flex flex-col gap-3">
20 - <div class="grid grid-cols-2 gap-2">
21 - <div class="info-cell">
22 - <div class="info-label">Key</div>
23 - <code class="text-xs">{{ entry.key }}</code>
24 - </div>
25 - <div class="info-cell">
26 - <div class="info-label">Source</div>
27 - <code v-if="entry.source" class="text-xs">{{ entry.source }}</code>
28 - <span v-else class="text-tertiary text-xs">—</span>
29 - </div>
22 + <div class="grid grid-cols-2 gap-3">
23 + <CardKV>
24 + <template #key>Key</template>
25 + <template #value>
26 + {{ entry.key }}
27 + </template>
28 + </CardKV>
29 + <CardKV>
30 + <template #key>Source</template>
31 + <template #value>
32 + <span v-if="entry.source">{{ entry.source }}</span>
33 + <span v-else class="text-secondary">—</span>
34 + </template>
35 + </CardKV>
36 </div>
37
32 - <div
33 - v-if="entry.match_field && entry.match_value"
34 - class="info-cell"
35 - style="grid-column: 1 / -1;"
36 - >
37 - <div class="info-label">Conditional auto-apply</div>
38 - <div class="text-xs">
38 + <CardKV v-if="entry.match_field && entry.match_value">
39 + <template #key>Conditional auto-apply</template>
40 + <template #value>
41 Only fires when
42 <code>{{ entry.match_field }}</code>
43 ==
44 <code>{{ entry.match_value }}</code>
45 on the originating Wazuh document.
44 - </div>
45 - </div>
46 -
47 - <div v-if="entry.description" class="text-secondary text-sm">
48 - {{ entry.description }}
49 - </div>
50 -
51 - <div class="flex flex-col gap-2">
52 - <div class="text-secondary text-xs uppercase tracking-wide">
53 - Tasks ({{ entry.tasks.length }})
54 - </div>
55 - <div class="task-list">
46 + </template>
47 + </CardKV>
48 +
49 + <CardKV v-if="entry.description">
50 + <template #key>Description</template>
51 + <template #value>
52 + {{ entry.description }}
53 + </template>
54 + </CardKV>
55 +
56 + <CardKV value-class="flex flex-col divide-y divide-border p-0!">
57 + <template #key>Tasks ({{ entry.tasks.length }})</template>
58 + <template #value>
59 <div
60 v-for="(t, idx) of entry.tasks"
61 :key="`${entry.key}-${idx}`"
59 - class="task-row"
62 + class="flex items-center gap-4 px-4 py-2"
63 >
61 - <div class="task-index">{{ t.order_index }}</div>
62 - <div class="flex min-w-0 flex-col gap-1">
64 + <div class="text-secondary font-mono">{{ t.order_index }}</div>
65 + <div class="flex min-w-0 flex-col gap-0.5">
66 <div class="flex items-center gap-2">
64 - <div class="task-title">{{ t.title }}</div>
67 + <div class="font-medium">{{ t.title }}</div>
68 <Badge v-if="t.mandatory" color="warning" size="small">
69 <template #value>mandatory</template>
70 </Badge>
@@ -71,13 +74,11 @@
74 </div>
75 </div>
76 </div>
74 - </div>
75 - </div>
77 + </template>
78 + </CardKV>
79
80 <div class="flex justify-end gap-2">
78 - <n-button size="small" quaternary :disabled="submitting" @click="close">
79 - Cancel
80 - </n-button>
81 + <n-button size="small" quaternary :disabled="submitting" @click="close">Cancel</n-button>
82 <n-button size="small" type="primary" :loading="submitting" @click="submit">
83 Import as global template
84 </n-button>
@@ -90,8 +91,8 @@
91 <template #header>Template imported</template>
92 <div class="text-sm">
93 <strong>{{ result.name }}</strong>
93 - is now in your Templates list. You can apply it to any case from the case
94 - detail page, or edit/customize it from the Templates tab.
94 + is now in your Templates list. You can apply it to any case from the case detail page, or
95 + edit/customize it from the Templates tab.
96 </div>
97 </n-alert>
98 <div class="flex justify-end">
@@ -104,31 +105,24 @@
105 </template>
106
107 <script setup lang="ts">
107 -import type {
108 - CaseTemplate,
109 - CaseTemplateLibraryEntry
110 -} from "@/types/incidentManagement/caseTemplates.d"
108 +import type { CaseTemplate, CaseTemplateLibraryEntry } from "@/types/incidentManagement/caseTemplates.d"
109 import { NAlert, NButton, NModal, useMessage } from "naive-ui"
112 -import { computed, ref, watch } from "vue"
110 +import { ref, watch } from "vue"
111 import Api from "@/api"
112 import Badge from "@/components/common/Badge.vue"
113 +import CardKV from "@/components/common/cards/CardKV.vue"
114
115 const props = defineProps<{
117 - show: boolean
116 entry: CaseTemplateLibraryEntry | null
117 }>()
118
119 const emit = defineEmits<{
122 - (e: "update:show", value: boolean): void
120 (e: "imported", template: CaseTemplate): void
121 }>()
122
123 const message = useMessage()
124
128 -const showLocal = computed({
129 - get: () => props.show,
130 - set: v => emit("update:show", v)
131 -})
125 +const show = defineModel<boolean>("show", { required: true, default: false })
126
127 const submitting = ref(false)
128 const result = ref<CaseTemplate | null>(null)
@@ -149,10 +143,7 @@ async function submit() {
143 // Backend returns HTTP 409 on name collision with a useful detail message.
144 const status = err.response?.status
145 const detail =
152 - err.response?.data?.detail ||
153 - err.response?.data?.message ||
154 - err.message ||
155 - "Failed to import library entry"
146 + err.response?.data?.detail || err.response?.data?.message || err.message || "Failed to import library entry"
147 if (status === 409) {
148 message.warning(detail)
149 } else {
@@ -164,63 +155,11 @@ async function submit() {
155 }
156
157 function close() {
167 - showLocal.value = false
158 + show.value = false
159 }
160
161 // Reset result whenever the modal opens fresh OR the selected entry changes.
171 -watch(
172 - () => [props.show, props.entry?.key] as const,
173 - ([open]) => {
174 - if (open) result.value = null
175 - }
176 -)
162 +watch([show.value, () => props.entry?.key], ([open]) => {
163 + if (open) result.value = null
164 +})
165 </script>
178 -
179 -<style scoped lang="scss">
180 -.info-cell {
181 - display: flex;
182 - flex-direction: column;
183 - gap: 4px;
184 - padding: 8px 10px;
185 - background: var(--bg-secondary-color);
186 - border: 1px solid var(--border-color);
187 - border-radius: var(--border-radius);
188 -}
189 -.info-label {
190 - font-size: 0.7rem;
191 - text-transform: uppercase;
192 - letter-spacing: 0.04em;
193 - color: var(--fg-tertiary-color);
194 -}
195 -
196 -.task-list {
197 - display: flex;
198 - flex-direction: column;
199 - border: 1px solid var(--border-color);
200 - border-radius: var(--border-radius);
201 - background: var(--bg-default-color);
202 - max-height: 320px;
203 - overflow-y: auto;
204 -}
205 -.task-row {
206 - display: flex;
207 - align-items: flex-start;
208 - gap: 10px;
209 - padding: 8px 12px;
210 -}
211 -.task-row + .task-row {
212 - border-top: 1px solid var(--border-color);
213 -}
214 -.task-index {
215 - font-family: var(--font-family-mono, monospace);
216 - font-size: 0.75rem;
217 - color: var(--fg-tertiary-color);
218 - min-width: 22px;
219 - text-align: right;
220 - padding-top: 2px;
221 -}
222 -.task-title {
223 - font-size: 0.85rem;
224 - color: var(--fg-default-color);
225 -}
226 -</style>
frontend/src/components/incidentManagement/caseTemplates/CaseTemplatesLibrary.vue
+76 -100
@@ -1,5 +1,5 @@
1 <template>
2 - <div class="case-templates-library flex flex-col gap-4">
2 + <div class="@container flex flex-col gap-4">
3 <!-- Header / actions -->
4 <div class="flex flex-col gap-2">
5 <div class="flex flex-wrap items-center justify-between gap-3">
@@ -9,7 +9,7 @@
9 :href="REPO_URL"
10 target="_blank"
11 rel="noopener"
12 - class="text-secondary text-xs"
12 + class="text-secondary text-sm"
13 title="Source repository on GitHub"
14 >
15 {{ REPO_NAME }} ↗
@@ -18,7 +18,7 @@
18 <div class="flex items-center gap-2">
19 <div v-if="lastRefresh" class="text-tertiary text-xs">
20 Cached
21 - {{ formatDate(lastRefresh, dFormats.datetimesec) }}
21 + {{ formatDate(lastRefresh, dFormats.datetimesec, { tz: true }) }}
22 </div>
23 <n-button size="small" secondary :loading="refreshing" @click="refresh">
24 <template #icon><Icon name="carbon:renew" /></template>
@@ -26,46 +26,45 @@
26 </n-button>
27 </div>
28 </div>
29 +
30 <p class="text-secondary text-sm">
31 Read-only catalog of investigation playbooks. Click
32 <strong>Import</strong>
32 - to materialise a playbook as a normal case template you can apply to cases. Edits made
33 - in CoPilot after import don't flow back to the source repo, and changes pushed to the
34 - repo don't retroactively update already-imported templates.
33 + to materialise a playbook as a normal case template you can apply to cases. Edits made in CoPilot after
34 + import don't flow back to the source repo, and changes pushed to the repo don't retroactively update
35 + already-imported templates.
36 </p>
37
38 <n-alert v-if="invalidPaths.length" type="warning" :show-icon="false">
38 - <template #header>
39 - {{ invalidPaths.length }} library file(s) failed validation
40 - </template>
41 - <div class="text-xs">
42 - <code v-for="p of invalidPaths" :key="p" class="mr-2">{{ p }}</code>
39 + <template #header>{{ invalidPaths.length }} library file(s) failed validation</template>
40 + <div class="flex flex-wrap gap-2 text-xs">
41 + <code v-for="p of invalidPaths" :key="p">{{ p }}</code>
42 </div>
43 </n-alert>
44 </div>
45
46 <!-- Filter -->
48 - <n-input
49 - v-model:value="search"
50 - size="small"
51 - placeholder="Search by name, description, or source"
52 - clearable
53 - class="max-w-96"
54 - >
55 - <template #prefix><Icon name="carbon:search" /></template>
56 - </n-input>
47 + <div class="mt-2">
48 + <n-input
49 + v-model:value="search"
50 + size="small"
51 + placeholder="Search by name, description, or source"
52 + clearable
53 + class="max-w-96"
54 + >
55 + <template #prefix><Icon name="carbon:search" /></template>
56 + </n-input>
57 + </div>
58
59 <n-spin :show="loading">
59 - <div
60 - v-if="filteredEntries.length"
61 - class="grid grid-cols-1 gap-3 @2xl:grid-cols-2 @5xl:grid-cols-3"
62 - >
63 - <div v-for="entry of filteredEntries" :key="entry.key" class="library-card">
64 - <div class="library-card-header flex items-start justify-between gap-2">
65 - <div class="flex min-w-0 flex-col">
66 - <div class="library-card-name truncate">{{ entry.name }}</div>
67 - <code v-if="entry.source" class="text-tertiary text-xs">{{ entry.source }}</code>
60 + <div v-if="filteredEntries.length" class="grid grid-cols-1 gap-3 @3xl:grid-cols-2 @6xl:grid-cols-3">
61 + <CardEntity v-for="entry of filteredEntries" :key="entry.key" size="small">
62 + <template #headerMain>
63 + <div class="text-default text-base font-semibold">
64 + {{ entry.name }}
65 </div>
66 + </template>
67 + <template #headerExtra>
68 <n-button
69 size="small"
70 type="primary"
@@ -77,40 +76,42 @@
76 <template #icon><Icon name="carbon:download" /></template>
77 Import
78 </n-button>
80 - </div>
81 -
82 - <p v-if="entry.description" class="text-secondary line-clamp-3 text-sm">
83 - {{ entry.description }}
84 - </p>
85 -
86 - <div class="library-card-meta flex flex-wrap items-center gap-2 text-xs">
87 - <Badge type="splitted">
88 - <template #label>Tasks</template>
89 - <template #value>{{ entry.tasks.length }}</template>
90 - </Badge>
91 - <Badge v-if="mandatoryCount(entry) > 0" color="warning" type="splitted" bright>
92 - <template #label>Mandatory</template>
93 - <template #value>{{ mandatoryCount(entry) }}</template>
94 - </Badge>
95 - <n-tag
96 - v-if="entry.match_field && entry.match_value"
97 - size="tiny"
98 - :bordered="false"
99 - type="success"
100 - :title="`Conditional: applies when ${entry.match_field} == ${entry.match_value}`"
101 - >
102 - {{ entry.match_field }} == {{ entry.match_value }}
103 - </n-tag>
104 - <n-tag
105 - v-for="tactic of mitreTactics(entry)"
106 - :key="tactic"
107 - size="tiny"
108 - :bordered="false"
109 - >
110 - {{ tactic }}
111 - </n-tag>
112 - </div>
113 - </div>
79 + </template>
80 + <template #default>
81 + <p v-if="entry.description" class="text-secondary line-clamp-3 text-sm">
82 + {{ entry.description }}
83 + </p>
84 + </template>
85 +
86 + <template #footer>
87 + <div class="flex flex-wrap items-center gap-2 text-xs">
88 + <Badge type="splitted" size="small">
89 + <template #label>Source</template>
90 + <template #value>{{ entry.source || "—" }}</template>
91 + </Badge>
92 + <Badge type="splitted" size="small">
93 + <template #label>Tasks</template>
94 + <template #value>{{ entry.tasks.length }}</template>
95 + </Badge>
96 + <Badge v-if="mandatoryCount(entry) > 0" color="warning" type="splitted" bright size="small">
97 + <template #label>Mandatory</template>
98 + <template #value>{{ mandatoryCount(entry) }}</template>
99 + </Badge>
100 + <n-tag
101 + v-if="entry.match_field && entry.match_value"
102 + :bordered="false"
103 + size="small"
104 + type="success"
105 + :title="`Conditional: applies when ${entry.match_field} == ${entry.match_value}`"
106 + >
107 + {{ entry.match_field }} == {{ entry.match_value }}
108 + </n-tag>
109 + <n-tag v-for="tactic of mitreTactics(entry)" :key="tactic" size="small" :bordered="false">
110 + {{ tactic }}
111 + </n-tag>
112 + </div>
113 + </template>
114 + </CardEntity>
115 </div>
116
117 <n-empty
@@ -123,18 +124,10 @@
124 </template>
125 </n-empty>
126
126 - <n-empty
127 - v-else-if="!loading"
128 - description="No entries match your search."
129 - class="h-32 justify-center"
130 - />
127 + <n-empty v-else-if="!loading" description="No entries match your search." class="h-32 justify-center" />
128 </n-spin>
129
133 - <CaseTemplateLibraryImportModal
134 - v-model:show="showImport"
135 - :entry="selectedEntry"
136 - @imported="onImported"
137 - />
130 + <CaseTemplateLibraryImportModal v-model:show="showImport" :entry="selectedEntry" @imported="onImported" />
131 </div>
132 </template>
133
@@ -144,21 +137,22 @@ import { NAlert, NButton, NEmpty, NInput, NSpin, NTag, useMessage } from "naive-
137 import { computed, onBeforeMount, ref } from "vue"
138 import Api from "@/api"
139 import Badge from "@/components/common/Badge.vue"
140 +import CardEntity from "@/components/common/cards/CardEntity.vue"
141 import Icon from "@/components/common/Icon.vue"
142 import { useSettingsStore } from "@/stores/settings"
143 import { formatDate } from "@/utils/format"
144 import CaseTemplateLibraryImportModal from "./CaseTemplateLibraryImportModal.vue"
145
146 +const emit = defineEmits<{
147 + (e: "imported"): void
148 +}>()
149 +
150 const REPO_NAME = "socfortress/CoPilot-Case-Templates"
151 const REPO_URL = `https://github.com/${REPO_NAME}`
152
153 const message = useMessage()
154 const dFormats = useSettingsStore().dateFormat
155
158 -const emit = defineEmits<{
159 - (e: "imported"): void
160 -}>()
161 -
156 const entries = ref<CaseTemplateLibraryEntry[]>([])
157 const invalidPaths = ref<string[]>([])
158 const lastRefresh = ref<string | null>(null)
@@ -172,7 +166,9 @@ const selectedEntry = ref<CaseTemplateLibraryEntry | null>(null)
166
167 const filteredEntries = computed(() => {
168 const q = (search.value || "").trim().toLowerCase()
169 +
170 if (!q) return entries.value
171 +
172 return entries.value.filter(e => {
173 const haystack = `${e.name} ${e.description ?? ""} ${e.source ?? ""}`.toLowerCase()
174 return haystack.includes(q)
@@ -191,12 +187,14 @@ function mitreTactics(entry: CaseTemplateLibraryEntry): string[] {
187
188 function load() {
189 loading.value = true
190 +
191 Api.incidentManagement.caseTemplates
192 .getLibrary()
193 .then(res => {
194 entries.value = res.data.entries || []
195 invalidPaths.value = res.data.invalid_paths || []
196 lastRefresh.value = res.data.last_refresh || null
197 +
198 if (!res.data.success) {
199 message.warning(res.data.message || "Failed to load case-template library")
200 }
@@ -211,6 +209,7 @@ function load() {
209
210 async function refresh() {
211 refreshing.value = true
212 +
213 try {
214 const res = await Api.incidentManagement.caseTemplates.refreshLibrary()
215 if (res.data.success) {
@@ -239,26 +238,3 @@ function onImported() {
238
239 onBeforeMount(load)
240 </script>
242 -
243 -<style scoped lang="scss">
244 -.library-card {
245 - display: flex;
246 - flex-direction: column;
247 - gap: 10px;
248 - padding: 12px;
249 - border: 1px solid var(--border-color);
250 - border-radius: var(--border-radius);
251 - background: var(--bg-default-color);
252 - transition: border-color 0.12s, box-shadow 0.12s;
253 -}
254 -.library-card:hover {
255 - border-color: rgba(var(--primary-color-rgb) / 0.5);
256 -}
257 -.library-card-name {
258 - font-weight: 600;
259 - color: var(--fg-default-color);
260 -}
261 -.library-card-meta {
262 - margin-top: auto;
263 -}
264 -</style>
frontend/src/components/incidentManagement/caseTemplates/CaseTemplatesList.vue
+14 -381
@@ -1,400 +1,33 @@
1 <template>
2 - <div class="case-templates-list flex flex-col gap-4">
3 - <!-- Page header — kept outside the tabs so context is always visible -->
4 - <div class="flex flex-col gap-2">
5 - <h2>Case Templates</h2>
6 - <p>
7 - Reusable investigation playbooks. Templates are matched to new cases by customer + alert source on case
8 - creation, with priority customer+source &gt; customer-only &gt; source-only &gt; global default.
9 - </p>
10 - </div>
11 -
2 + <div>
3 <n-tabs v-model:value="activeTab" type="line" animated>
4 <n-tab-pane name="templates" tab="Templates">
14 - <div class="flex flex-col gap-4">
15 - <div>
16 - <n-button size="small" secondary type="primary" @click="openCreate">
17 - <template #icon><Icon name="carbon:add" /></template>
18 - New template
19 - </n-button>
20 - </div>
21 -
22 - <!-- Filters -->
23 - <div class="@container mt-2 grid grid-cols-12 items-center gap-3">
24 - <n-input
25 - v-model:value="search"
26 - size="small"
27 - placeholder="Search by name or description"
28 - clearable
29 - class="col-span-full @3xl:col-span-4 @6xl:col-span-5"
30 - >
31 - <template #prefix><Icon name="carbon:search" /></template>
32 - </n-input>
33 - <n-select
34 - v-model:value="customerFilter"
35 - size="small"
36 - :options="customersOptions"
37 - placeholder="Customer code (blank = all)"
38 - :loading="loadingCustomers"
39 - filterable
40 - clearable
41 - class="col-span-full @lg:col-span-6 @3xl:col-span-3"
42 - :consistent-menu-width="false"
43 - />
44 - <n-select
45 - v-model:value="sourceFilter"
46 - :options="sourcesOptions"
47 - :consistent-menu-width="false"
48 - placeholder="Alert source (blank = all)"
49 - size="small"
50 - filterable
51 - clearable
52 - class="col-span-full @lg:col-span-6 @3xl:col-span-3 @6xl:col-span-2"
53 - :loading="loadingConfiguredSources"
54 - />
55 - <n-checkbox v-model:checked="includeGlobal" size="small" class="col-span-full @3xl:col-span-2">
56 - <div class="text-xs">Include global / source-agnostic</div>
57 - </n-checkbox>
58 - </div>
59 -
60 - <n-data-table :columns :data="filteredRows" :loading size="small" />
61 - </div>
5 + <CaseTemplatesTable ref="templatesTableRef" />
6 </n-tab-pane>
7
8 <n-tab-pane name="library" tab="Library">
65 - <CaseTemplatesLibrary @imported="onLibraryImported" />
9 + <CaseTemplatesLibrary @imported="reloadTemplates" />
10 </n-tab-pane>
67 - </n-tabs>
11
69 - <!-- Editor modal -->
70 - <n-modal
71 - v-model:show="showEditor"
72 - preset="card"
73 - :title="editing ? `Edit template — ${editing.name}` : 'New template'"
74 - display-directive="show"
75 - style="max-width: 720px"
76 - >
77 - <CaseTemplateEditor :template="editing" @saved="onTemplateSaved" @cancel="showEditor = false" />
78 - </n-modal>
12 + <template #suffix>
13 + <CreateCaseTemplatesButton @success="reloadTemplates" />
14 + </template>
15 + </n-tabs>
16 </div>
17 </template>
18
19 <script setup lang="tsx">
83 -import type { DataTableColumns } from "naive-ui"
84 -import type { ApiError } from "@/types/common"
85 -import type { Customer } from "@/types/customers"
86 -import type { CaseTemplate } from "@/types/incidentManagement/caseTemplates.d"
87 -import type { SourceName } from "@/types/incidentManagement/sources"
88 -import { useDebounceFn } from "@vueuse/core"
89 -import {
90 - NButton,
91 - NCheckbox,
92 - NDataTable,
93 - NInput,
94 - NModal,
95 - NSelect,
96 - NTabPane,
97 - NTabs,
98 - NTag,
99 - useDialog,
100 - useMessage
101 -} from "naive-ui"
102 -import { computed, onBeforeMount, ref, watch } from "vue"
103 -import Api from "@/api"
104 -import Icon from "@/components/common/Icon.vue"
105 -import { useSettingsStore } from "@/stores/settings"
106 -import { getApiErrorMessage } from "@/utils"
107 -import { formatDate } from "@/utils/format"
108 -import CaseTemplateEditor from "./CaseTemplateEditor.vue"
20 +import { NTabPane, NTabs } from "naive-ui"
21 +import { ref } from "vue"
22 import CaseTemplatesLibrary from "./CaseTemplatesLibrary.vue"
23 +import CaseTemplatesTable from "./CaseTemplatesTable.vue"
24 +import CreateCaseTemplatesButton from "./CreateCaseTemplatesButton.vue"
25
111 -const message = useMessage()
112 -const dialog = useDialog()
113 -
114 -const dFormats = useSettingsStore().dateFormat
115 -const templates = ref<CaseTemplate[]>([])
116 -const loading = ref(false)
117 -const deletingId = ref<number | null>(null)
118 -const search = ref<string | null>(null)
119 -const customerFilter = ref<string | null>(null)
120 -const sourceFilter = ref<string | null>(null)
121 -const includeGlobal = ref(true)
122 -
123 -const loadingCustomers = ref(false)
124 -const customersList = ref<Customer[]>([])
125 -const customersOptions = computed(() =>
126 - customersList.value.map(o => ({ label: `#${o.customer_code} - ${o.customer_name}`, value: o.customer_code }))
127 -)
128 -
129 -const loadingConfiguredSources = ref(false)
130 -const configuredSourcesList = ref<SourceName[]>([])
131 -const sourcesOptions = computed(() => configuredSourcesList.value.map(o => ({ label: o, value: o })))
132 -
133 -const showEditor = ref(false)
134 -const editing = ref<CaseTemplate | null>(null)
135 -
26 +const templatesTableRef = ref<InstanceType<typeof CaseTemplatesTable>>()
27 const activeTab = ref<"templates" | "library">("templates")
28
138 -function onLibraryImported() {
139 - // After a library import lands, jump the user back to the Templates tab and
140 - // refetch so the freshly-imported row appears immediately.
29 +function reloadTemplates() {
30 activeTab.value = "templates"
142 - fetchTemplates()
143 -}
144 -
145 -function renderCustomerCode(customerCode: string | null | undefined) {
146 - if (customerCode) {
147 - return <span class="font-mono">{customerCode}</span>
148 - }
149 -
150 - return <em class="text-tertiary">any</em>
151 -}
152 -
153 -function renderSource(source: string | null | undefined) {
154 - if (source) {
155 - return <span class="font-mono">{source}</span>
156 - }
157 -
158 - return <em class="text-tertiary">any</em>
159 -}
160 -
161 -const filteredRows = computed(() => {
162 - if (!search.value?.trim()) return templates.value
163 -
164 - const text = search.value.trim().toLowerCase()
165 -
166 - return templates.value.filter(
167 - t =>
168 - t.name.toLowerCase().includes(text) ||
169 - (t.description ?? "").toLowerCase().includes(text) ||
170 - (t.customer_code ?? "").toLowerCase().includes(text) ||
171 - (t.source ?? "").toLowerCase().includes(text)
172 - )
173 -})
174 -
175 -const columns: DataTableColumns<CaseTemplate> = [
176 - {
177 - title: "Name",
178 - key: "name",
179 - className: "whitespace-nowrap",
180 - render: row => (
181 - <div class="flex flex-col gap-1">
182 - <div class="flex items-center gap-2">
183 - <span class="font-medium whitespace-nowrap">{row.name}</span>
184 - {row.is_default && (
185 - <NTag size="tiny" type="info" bordered={false}>
186 - default
187 - </NTag>
188 - )}
189 - </div>
190 - {row.description && <span class="text-secondary text-xs">{row.description}</span>}
191 - </div>
192 - )
193 - },
194 - {
195 - title: "Scope",
196 - key: "scope",
197 - className: "whitespace-nowrap",
198 - render: row => (
199 - <div class="flex flex-col text-sm whitespace-nowrap">
200 - <span>
201 - <span class="text-secondary">customer: </span>
202 - {renderCustomerCode(row.customer_code)}
203 - </span>
204 - <span>
205 - <span class="text-secondary">source: </span>
206 - {renderSource(row.source)}
207 - </span>
208 - </div>
209 - )
210 - },
211 - {
212 - title: "Tasks",
213 - key: "tasks",
214 - width: 110,
215 - className: "whitespace-nowrap",
216 - render(row) {
217 - const total = row.tasks?.length ?? 0
218 - const mandatory = row.tasks?.filter(t => t.mandatory).length ?? 0
219 -
220 - return (
221 - <div class="flex flex-col text-sm whitespace-nowrap">
222 - <span>
223 - <span class="font-mono">{total}</span>
224 - {" total"}
225 - </span>
226 - {mandatory > 0 && (
227 - <span class="text-warning">
228 - <span class="font-mono">{mandatory}</span>
229 - {" mandatory"}
230 - </span>
231 - )}
232 - </div>
233 - )
234 - }
235 - },
236 - {
237 - title: "Created by",
238 - key: "created_by",
239 - className: "whitespace-nowrap",
240 - render: row => <span class="whitespace-nowrap">{row.created_by}</span>
241 - },
242 - {
243 - title: "Updated",
244 - key: "updated_at",
245 - className: "whitespace-nowrap",
246 - render: row => (
247 - <span class="font-mono text-xs whitespace-nowrap">{formatDate(row.updated_at, dFormats.datetime)}</span>
248 - )
249 - },
250 - {
251 - title: "Actions",
252 - key: "actions",
253 - width: 120,
254 - className: "whitespace-nowrap",
255 - render: row => (
256 - <div class="flex gap-2">
257 - <NButton
258 - size="small"
259 - secondary
260 - onClick={() => openEdit(row)}
261 - v-slots={{ icon: () => <Icon name="carbon:edit" size={14} /> }}
262 - >
263 - Edit
264 - </NButton>
265 - <NButton
266 - size="small"
267 - secondary
268 - type="error"
269 - loading={deletingId.value === row.id}
270 - onClick={() => confirmDelete(row)}
271 - v-slots={{ icon: () => <Icon name="carbon:trash-can" size={14} /> }}
272 - >
273 - Delete
274 - </NButton>
275 - </div>
276 - )
277 - }
278 -]
279 -
280 -const fetchTemplates = useDebounceFn(() => {
281 - loading.value = true
282 -
283 - Api.incidentManagement.caseTemplates
284 - .listTemplates({
285 - customerCode: customerFilter.value || undefined,
286 - source: sourceFilter.value || undefined,
287 - includeGlobal: includeGlobal.value
288 - })
289 - .then(res => {
290 - if (res.data.success) {
291 - templates.value = res.data.templates
292 - } else {
293 - message.warning(res.data.message)
294 - }
295 - })
296 - .catch(err => {
297 - message.error(getApiErrorMessage(err as ApiError) || "Failed to load templates")
298 - })
299 - .finally(() => {
300 - loading.value = false
301 - })
302 -}, 400)
303 -
304 -function openCreate() {
305 - editing.value = null
306 - showEditor.value = true
307 -}
308 -
309 -function openEdit(row: CaseTemplate) {
310 - editing.value = row
311 - showEditor.value = true
312 -}
313 -
314 -function onTemplateSaved() {
315 - showEditor.value = false
316 - fetchTemplates()
317 -}
318 -
319 -function confirmDelete(row: CaseTemplate) {
320 - dialog.warning({
321 - title: `Delete template "${row.name}" ?`,
322 - content:
323 - "Deleting the template removes its task definitions. Existing CaseTask snapshots on real cases are preserved (they're independent of the template).",
324 - positiveText: "Delete",
325 - negativeText: "Cancel",
326 - onPositiveClick: () => {
327 - deletingId.value = row.id
328 -
329 - Api.incidentManagement.caseTemplates
330 - .deleteTemplate(row.id)
331 - .then(res => {
332 - if (res.data.success) {
333 - message.success(`Deleted "${row.name}"`)
334 - fetchTemplates()
335 - } else {
336 - message.warning(res.data.message)
337 - }
338 - })
339 - .catch(err => {
340 - message.error(getApiErrorMessage(err as ApiError) || "Failed to delete template")
341 - })
342 - .finally(() => {
343 - deletingId.value = null
344 - })
345 - }
346 - })
347 -}
348 -
349 -function getCustomers() {
350 - loadingCustomers.value = true
351 -
352 - Api.customers
353 - .getCustomers()
354 - .then(res => {
355 - if (res.data.success) {
356 - customersList.value = res.data?.customers || []
357 - } else {
358 - message.warning(res.data?.message || "An error occurred. Please try again later.")
359 - }
360 - })
361 - .catch(err => {
362 - message.error(getApiErrorMessage(err as ApiError) || "An error occurred. Please try again later.")
363 - })
364 - .finally(() => {
365 - loadingCustomers.value = false
366 - })
31 + templatesTableRef.value?.fetchTemplates()
32 }
368 -
369 -function getConfiguredSources() {
370 - loadingConfiguredSources.value = true
371 -
372 - Api.incidentManagement.sources
373 - .getConfiguredSources()
374 - .then(res => {
375 - if (res.data.success) {
376 - configuredSourcesList.value = res.data?.sources || []
377 - } else {
378 - message.warning(res.data?.message || "An error occurred. Please try again later.")
379 - }
380 - })
381 - .catch(err => {
382 - message.error(getApiErrorMessage(err as ApiError) || "An error occurred. Please try again later.")
383 - })
384 - .finally(() => {
385 - loadingConfiguredSources.value = false
386 - })
387 -}
388 -
389 -// Re-fetch when scope filters change so the result set follows the
390 -// backend's customer+source filtering semantics.
391 -watch([customerFilter, sourceFilter, includeGlobal], () => {
392 - fetchTemplates()
393 -})
394 -
395 -onBeforeMount(() => {
396 - fetchTemplates()
397 - getCustomers()
398 - getConfiguredSources()
399 -})
33 </script>
frontend/src/components/incidentManagement/caseTemplates/CaseTemplatesTable.vue new
+359
@@ -0,0 +1,359 @@
1 +<template>
2 + <div class="flex flex-col gap-4">
3 + <div class="flex flex-col gap-2">
4 + <h2>Case Templates</h2>
5 + <p>
6 + Reusable investigation playbooks. Templates are matched to new cases by customer + alert source on case
7 + creation, with priority customer+source &gt; customer-only &gt; source-only &gt; global default.
8 + </p>
9 + </div>
10 +
11 + <div class="@container mt-2 grid grid-cols-12 items-center gap-3">
12 + <n-input
13 + v-model:value="search"
14 + size="small"
15 + placeholder="Search by name or description"
16 + clearable
17 + class="col-span-full @3xl:col-span-4 @6xl:col-span-5"
18 + >
19 + <template #prefix><Icon name="carbon:search" /></template>
20 + </n-input>
21 + <n-select
22 + v-model:value="customerFilter"
23 + size="small"
24 + :options="customersOptions"
25 + placeholder="Customer code (blank = all)"
26 + :loading="loadingCustomers"
27 + filterable
28 + clearable
29 + class="col-span-full @lg:col-span-6 @3xl:col-span-3"
30 + :consistent-menu-width="false"
31 + />
32 + <n-select
33 + v-model:value="sourceFilter"
34 + :options="sourcesOptions"
35 + :consistent-menu-width="false"
36 + placeholder="Alert source (blank = all)"
37 + size="small"
38 + filterable
39 + clearable
40 + class="col-span-full @lg:col-span-6 @3xl:col-span-3 @6xl:col-span-2"
41 + :loading="loadingConfiguredSources"
42 + />
43 + <n-checkbox v-model:checked="includeGlobal" size="small" class="col-span-full @3xl:col-span-2">
44 + <div class="text-xs">Include global / source-agnostic</div>
45 + </n-checkbox>
46 + </div>
47 +
48 + <n-data-table :columns :data="filteredRows" :loading size="small" />
49 +
50 + <!-- Editor modal -->
51 + <n-modal
52 + v-model:show="showEditor"
53 + preset="card"
54 + :title="editing ? `Edit template — ${editing.name}` : 'New template'"
55 + display-directive="show"
56 + style="max-width: 720px"
57 + >
58 + <CaseTemplateEditor :template="editing" @saved="onTemplateSaved" @cancel="showEditor = false" />
59 + </n-modal>
60 + </div>
61 +</template>
62 +
63 +<script setup lang="tsx">
64 +import type { DataTableColumns } from "naive-ui"
65 +import type { ApiError } from "@/types/common"
66 +import type { Customer } from "@/types/customers"
67 +import type { CaseTemplate } from "@/types/incidentManagement/caseTemplates.d"
68 +import type { SourceName } from "@/types/incidentManagement/sources"
69 +import { useDebounceFn } from "@vueuse/core"
70 +import { NButton, NCheckbox, NDataTable, NInput, NModal, NSelect, NTag, useDialog, useMessage } from "naive-ui"
71 +import { computed, onBeforeMount, ref, watch } from "vue"
72 +import Api from "@/api"
73 +import Icon from "@/components/common/Icon.vue"
74 +import { useSettingsStore } from "@/stores/settings"
75 +import { getApiErrorMessage } from "@/utils"
76 +import { formatDate } from "@/utils/format"
77 +import CaseTemplateEditor from "./CaseTemplateEditor.vue"
78 +
79 +const message = useMessage()
80 +const dialog = useDialog()
81 +
82 +const dFormats = useSettingsStore().dateFormat
83 +const templates = ref<CaseTemplate[]>([])
84 +const loading = ref(false)
85 +const deletingId = ref<number | null>(null)
86 +const search = ref<string | null>(null)
87 +const customerFilter = ref<string | null>(null)
88 +const sourceFilter = ref<string | null>(null)
89 +const includeGlobal = ref(true)
90 +
91 +const loadingCustomers = ref(false)
92 +const customersList = ref<Customer[]>([])
93 +const customersOptions = computed(() =>
94 + customersList.value.map(o => ({ label: `#${o.customer_code} - ${o.customer_name}`, value: o.customer_code }))
95 +)
96 +
97 +const loadingConfiguredSources = ref(false)
98 +const configuredSourcesList = ref<SourceName[]>([])
99 +const sourcesOptions = computed(() => configuredSourcesList.value.map(o => ({ label: o, value: o })))
100 +
101 +const showEditor = ref(false)
102 +const editing = ref<CaseTemplate | null>(null)
103 +
104 +function renderCustomerCode(customerCode: string | null | undefined) {
105 + if (customerCode) {
106 + return <span class="font-mono">{customerCode}</span>
107 + }
108 +
109 + return <em class="text-tertiary">any</em>
110 +}
111 +
112 +function renderSource(source: string | null | undefined) {
113 + if (source) {
114 + return <span class="font-mono">{source}</span>
115 + }
116 +
117 + return <em class="text-tertiary">any</em>
118 +}
119 +
120 +const filteredRows = computed(() => {
121 + if (!search.value?.trim()) return templates.value
122 +
123 + const text = search.value.trim().toLowerCase()
124 +
125 + return templates.value.filter(
126 + t =>
127 + t.name.toLowerCase().includes(text) ||
128 + (t.description ?? "").toLowerCase().includes(text) ||
129 + (t.customer_code ?? "").toLowerCase().includes(text) ||
130 + (t.source ?? "").toLowerCase().includes(text)
131 + )
132 +})
133 +
134 +const columns: DataTableColumns<CaseTemplate> = [
135 + {
136 + title: "Name",
137 + key: "name",
138 + width: 480,
139 + render: row => (
140 + <div class="flex flex-col gap-1">
141 + <div class="flex items-center gap-2">
142 + <span class="font-medium">{row.name}</span>
143 + {row.is_default && (
144 + <NTag size="tiny" type="info" bordered={false}>
145 + default
146 + </NTag>
147 + )}
148 + </div>
149 + {row.description && <span class="text-secondary text-xs">{row.description}</span>}
150 + </div>
151 + )
152 + },
153 + {
154 + title: "Scope",
155 + key: "scope",
156 + className: "whitespace-nowrap",
157 + render: row => (
158 + <div class="flex flex-col text-sm whitespace-nowrap">
159 + <span>
160 + <span class="text-secondary">customer: </span>
161 + {renderCustomerCode(row.customer_code)}
162 + </span>
163 + <span>
164 + <span class="text-secondary">source: </span>
165 + {renderSource(row.source)}
166 + </span>
167 + </div>
168 + )
169 + },
170 + {
171 + title: "Tasks",
172 + key: "tasks",
173 + width: 110,
174 + className: "whitespace-nowrap",
175 + render(row) {
176 + const total = row.tasks?.length ?? 0
177 + const mandatory = row.tasks?.filter(t => t.mandatory).length ?? 0
178 +
179 + return (
180 + <div class="flex flex-col text-sm whitespace-nowrap">
181 + <span>
182 + <span class="font-mono">{total}</span>
183 + {" total"}
184 + </span>
185 + {mandatory > 0 && (
186 + <span class="text-warning">
187 + <span class="font-mono">{mandatory}</span>
188 + {" mandatory"}
189 + </span>
190 + )}
191 + </div>
192 + )
193 + }
194 + },
195 + {
196 + title: "Created by",
197 + key: "created_by",
198 + className: "whitespace-nowrap",
199 + render: row => <span class="whitespace-nowrap">{row.created_by}</span>
200 + },
201 + {
202 + title: "Updated",
203 + key: "updated_at",
204 + className: "whitespace-nowrap",
205 + render: row => (
206 + <span class="font-mono text-xs whitespace-nowrap">{formatDate(row.updated_at, dFormats.datetime)}</span>
207 + )
208 + },
209 + {
210 + title: "Actions",
211 + key: "actions",
212 + width: 120,
213 + fixed: "right",
214 + className: "whitespace-nowrap",
215 + render: row => (
216 + <div class="flex gap-2">
217 + <NButton
218 + size="small"
219 + secondary
220 + onClick={() => openEdit(row)}
221 + v-slots={{ icon: () => <Icon name="carbon:edit" size={14} /> }}
222 + >
223 + Edit
224 + </NButton>
225 + <NButton
226 + size="small"
227 + secondary
228 + type="error"
229 + loading={deletingId.value === row.id}
230 + onClick={() => confirmDelete(row)}
231 + v-slots={{ icon: () => <Icon name="carbon:trash-can" size={14} /> }}
232 + >
233 + Delete
234 + </NButton>
235 + </div>
236 + )
237 + }
238 +]
239 +
240 +const fetchTemplates = useDebounceFn(() => {
241 + loading.value = true
242 +
243 + Api.incidentManagement.caseTemplates
244 + .listTemplates({
245 + customerCode: customerFilter.value || undefined,
246 + source: sourceFilter.value || undefined,
247 + includeGlobal: includeGlobal.value
248 + })
249 + .then(res => {
250 + if (res.data.success) {
251 + templates.value = res.data.templates
252 + } else {
253 + message.warning(res.data.message)
254 + }
255 + })
256 + .catch(err => {
257 + message.error(getApiErrorMessage(err as ApiError) || "Failed to load templates")
258 + })
259 + .finally(() => {
260 + loading.value = false
261 + })
262 +}, 400)
263 +
264 +function openEdit(row: CaseTemplate) {
265 + editing.value = row
266 + showEditor.value = true
267 +}
268 +
269 +function confirmDelete(row: CaseTemplate) {
270 + dialog.warning({
271 + title: `Delete template "${row.name}" ?`,
272 + content:
273 + "Deleting the template removes its task definitions. Existing CaseTask snapshots on real cases are preserved (they're independent of the template).",
274 + positiveText: "Delete",
275 + negativeText: "Cancel",
276 + onPositiveClick: () => {
277 + deletingId.value = row.id
278 +
279 + Api.incidentManagement.caseTemplates
280 + .deleteTemplate(row.id)
281 + .then(res => {
282 + if (res.data.success) {
283 + message.success(`Deleted "${row.name}"`)
284 + fetchTemplates()
285 + } else {
286 + message.warning(res.data.message)
287 + }
288 + })
289 + .catch(err => {
290 + message.error(getApiErrorMessage(err as ApiError) || "Failed to delete template")
291 + })
292 + .finally(() => {
293 + deletingId.value = null
294 + })
295 + }
296 + })
297 +}
298 +
299 +function getCustomers() {
300 + loadingCustomers.value = true
301 +
302 + Api.customers
303 + .getCustomers()
304 + .then(res => {
305 + if (res.data.success) {
306 + customersList.value = res.data?.customers || []
307 + } else {
308 + message.warning(res.data?.message || "An error occurred. Please try again later.")
309 + }
310 + })
311 + .catch(err => {
312 + message.error(getApiErrorMessage(err as ApiError) || "An error occurred. Please try again later.")
313 + })
314 + .finally(() => {
315 + loadingCustomers.value = false
316 + })
317 +}
318 +
319 +function getConfiguredSources() {
320 + loadingConfiguredSources.value = true
321 +
322 + Api.incidentManagement.sources
323 + .getConfiguredSources()
324 + .then(res => {
325 + if (res.data.success) {
326 + configuredSourcesList.value = res.data?.sources || []
327 + } else {
328 + message.warning(res.data?.message || "An error occurred. Please try again later.")
329 + }
330 + })
331 + .catch(err => {
332 + message.error(getApiErrorMessage(err as ApiError) || "An error occurred. Please try again later.")
333 + })
334 + .finally(() => {
335 + loadingConfiguredSources.value = false
336 + })
337 +}
338 +
339 +function onTemplateSaved() {
340 + showEditor.value = false
341 + fetchTemplates()
342 +}
343 +
344 +// Re-fetch when scope filters change so the result set follows the
345 +// backend's customer+source filtering semantics.
346 +watch([customerFilter, sourceFilter, includeGlobal], () => {
347 + fetchTemplates()
348 +})
349 +
350 +onBeforeMount(() => {
351 + fetchTemplates()
352 + getCustomers()
353 + getConfiguredSources()
354 +})
355 +
356 +defineExpose({
357 + fetchTemplates
358 +})
359 +</script>
frontend/src/components/incidentManagement/caseTemplates/CreateCaseTemplatesButton.vue new
+49
@@ -0,0 +1,49 @@
1 +<template>
2 + <div>
3 + <n-button size="small" secondary type="primary" @click="openCreate">
4 + <template #icon><Icon name="carbon:add" /></template>
5 + New template
6 + </n-button>
7 +
8 + <n-modal
9 + v-model:show="showEditor"
10 + preset="card"
11 + title="New template"
12 + display-directive="show"
13 + style="max-width: 720px"
14 + >
15 + <CaseTemplateEditor ref="editorRef" @saved="handleSuccess" @cancel="handleCancel" />
16 + </n-modal>
17 + </div>
18 +</template>
19 +
20 +<script setup lang="tsx">
21 +import type { CaseTemplate } from "@/types/incidentManagement/caseTemplates.d"
22 +import { NButton, NModal } from "naive-ui"
23 +import { ref } from "vue"
24 +import Icon from "@/components/common/Icon.vue"
25 +import CaseTemplateEditor from "./CaseTemplateEditor.vue"
26 +
27 +const emit = defineEmits<{
28 + (e: "success", template: CaseTemplate): void
29 + (e: "cancel"): void
30 +}>()
31 +
32 +const showEditor = ref(false)
33 +const editorRef = ref<InstanceType<typeof CaseTemplateEditor>>()
34 +
35 +function handleSuccess(template: CaseTemplate) {
36 + showEditor.value = false
37 + emit("success", template)
38 +}
39 +
40 +function handleCancel() {
41 + showEditor.value = false
42 + emit("cancel")
43 +}
44 +
45 +function openCreate() {
46 + showEditor.value = true
47 + editorRef.value?.validate()
48 +}
49 +</script>
frontend/src/components/incidentManagement/cases/CaseDetails.vue
+5 -5
@@ -14,7 +14,7 @@
14 </div>
15 </n-tab-pane>
16 <n-tab-pane name="Alerts" tab="Alerts" display-directive="show:lazy">
17 - <div class="flex flex-col gap-2 p-7 pt-4">
17 + <div class="flex flex-col gap-2 p-6 pt-3">
18 <template v-if="caseEntity.alerts.length">
19 <AlertItem
20 v-for="alert of caseEntity.alerts"
@@ -31,7 +31,7 @@
31 </div>
32 </n-tab-pane>
33 <n-tab-pane name="Tasks" tab="Tasks" display-directive="show:lazy">
34 - <div class="p-7 pt-4">
34 + <div class="p-6 pt-3">
35 <CaseTasksList
36 :case-id="caseEntity.id"
37 :customer-code="caseEntity.customer_code"
@@ -41,12 +41,12 @@
41 </div>
42 </n-tab-pane>
43 <n-tab-pane name="Timeline" tab="Timeline" display-directive="show:lazy">
44 - <div class="p-7 pt-4">
44 + <div class="p-6 pt-3">
45 <CaseTimelineFeed :case-id="caseEntity.id" />
46 </div>
47 </n-tab-pane>
48 <n-tab-pane name="Comments" tab="Comments" display-directive="show:lazy">
49 - <div class="p-7 pt-4">
49 + <div class="p-6 pt-3">
50 <CaseCommentsList
51 :comments="caseEntity.comments || []"
52 :case-id="caseEntity.id"
@@ -55,7 +55,7 @@
55 </div>
56 </n-tab-pane>
57 <n-tab-pane name="Data Store" tab="Data Store" display-directive="show:lazy">
58 - <div class="p-7 pt-4">
58 + <div class="p-6 pt-3">
59 <CaseDataStore :case-id="caseEntity.id" />
60 </div>
61 </n-tab-pane>
frontend/src/components/incidentManagement/cases/CaseOverview.vue
+12 -20
@@ -1,8 +1,8 @@
1 <template>
2 <n-spin :show="loading" class="flex grow flex-col" content-class="flex grow flex-col">
3 <div class="flex grow flex-col justify-between gap-4">
4 - <div class="content-box flex flex-col gap-4 py-3">
5 - <div class="flex flex-col gap-4 px-7 sm:flex-row!">
4 + <div class="content-box flex flex-col gap-4 px-6 py-3">
5 + <div class="flex flex-col gap-4 sm:flex-row!">
6 <CardKV
7 :color="
8 caseData.case_status === 'OPEN'
@@ -86,18 +86,16 @@
86 </CardKV>
87 </div>
88
89 - <div class="px-7">
90 - <CardKV>
91 - <template #key>description</template>
92 - <template #value>
93 - <span class="whitespace-pre-wrap">
94 - {{ caseData.case_description ?? "-" }}
95 - </span>
96 - </template>
97 - </CardKV>
98 - </div>
89 + <CardKV>
90 + <template #key>description</template>
91 + <template #value>
92 + <span class="whitespace-pre-wrap">
93 + {{ caseData.case_description ?? "-" }}
94 + </span>
95 + </template>
96 + </CardKV>
97
100 - <div class="grid-auto-fit-250 grid gap-2 px-7">
98 + <div class="grid-auto-fit-250 grid gap-2">
99 <CardKV>
100 <template #key>id</template>
101 <template #value>#{{ caseData.id }}</template>
@@ -131,7 +129,7 @@
129 </div>
130 </div>
131
134 - <div class="footer-box bg-secondary flex justify-end gap-3 px-7 py-4">
132 + <div class="bg-secondary border-default flex justify-end gap-3 border-t px-6 py-4">
133 <n-button type="error" secondary @click="handleDelete()">
134 <template #icon>
135 <Icon :name="TrashIcon" />
@@ -201,9 +199,3 @@ function handleDelete() {
199 }
200 }
201 </script>
204 -
205 -<style lang="scss" scoped>
206 -.footer-box {
207 - border-top: 1px solid var(--border-color);
208 -}
209 -</style>
frontend/src/components/incidentManagement/cases/CaseTasks/CaseTaskItem.vue
+13 -9
@@ -1,7 +1,10 @@
1 <template>
2 <CardEntity
3 :status="taskData?.status === 'DONE' ? 'success' : taskData?.status === 'NOT_NECESSARY' ? 'warning' : undefined"
4 - embedded
4 + :embedded
5 + :hide-header-extra="!taskData"
6 + :hide-main-extra="!taskData"
7 + :hide-footer="!taskData"
8 >
9 <template #headerMain>
10 <div class="flex flex-wrap items-center gap-3">
@@ -15,9 +18,9 @@
18 </n-tag>
19 </div>
20 </template>
18 - <template v-if="taskData" #headerExtra>
21 + <template #headerExtra>
22 <n-select
20 - v-if="canEdit"
23 + v-if="canEdit && taskData"
24 v-model:value="taskData.status"
25 :options="statusOptions"
26 :status="
@@ -28,7 +31,7 @@
31 :consistent-menu-width="false"
32 :loading="savingStatus"
33 />
31 - <n-tag v-else :bordered="false" :type="statusTagType(taskData.status)" size="small">
34 + <n-tag v-else-if="taskData" :bordered="false" :type="statusTagType(taskData.status)" size="small">
35 {{ statusLabel(taskData.status) }}
36 </n-tag>
37 </template>
@@ -42,7 +45,7 @@
45 </details>
46 </div>
47 </template>
45 - <template v-if="taskData" #mainExtra>
48 + <template #mainExtra>
49 <div class="flex flex-col gap-1">
50 <div class="flex items-center justify-between">
51 <div class="text-secondary text-xs uppercase">Evidence / notes</div>
@@ -54,7 +57,7 @@
57 saving...
58 </div>
59 </div>
57 - <div v-if="canEdit" class="flex flex-col gap-1">
60 + <div v-if="canEdit && taskData" class="flex flex-col gap-1">
61 <n-input
62 v-model:value="taskData.evidence_comment"
63 type="textarea"
@@ -63,14 +66,14 @@
66 :autosize="{ minRows: 2, maxRows: 8 }"
67 />
68 </div>
66 - <p v-else-if="taskData.evidence_comment" class="text-sm whitespace-pre-line">
69 + <p v-else-if="taskData?.evidence_comment" class="text-sm whitespace-pre-line">
70 {{ taskData.evidence_comment }}
71 </p>
72 <p v-else class="text-tertiary text-sm italic">No notes recorded</p>
73 </div>
74 </template>
72 - <template v-if="taskData" #footer>
73 - <div class="flex flex-wrap items-center justify-between gap-2">
75 + <template #footer>
76 + <div v-if="taskData" class="flex flex-wrap items-center justify-between gap-2">
77 <div class="text-secondary flex flex-wrap gap-x-4 gap-y-1 text-sm">
78 <span v-if="taskData.completed_by">
79 {{ task.status === "DONE" ? "Completed" : "Marked" }} by
@@ -125,6 +128,7 @@ const props = defineProps<{
128 task: CaseTask
129 caseId: number
130 canEdit: boolean
131 + embedded?: boolean
132 }>()
133
134 const emit = defineEmits<{
frontend/src/components/incidentManagement/cases/CaseTasks/CaseTasksCreateForm.vue
+1 -5
@@ -3,11 +3,7 @@
3 <n-form-item label="Title" path="title">
4 <n-input v-model:value="addForm.title" placeholder="What needs to be done?" />
5 </n-form-item>
6 - <n-form-item
7 - v-if="alertOptions.length"
8 - label="Attach to alert (optional)"
9 - path="alertId"
10 - >
6 + <n-form-item v-if="alertOptions.length" label="Attach to alert (optional)" path="alertId">
7 <n-select
8 v-model:value="addForm.alertId"
9 :options="alertOptions"
frontend/src/components/incidentManagement/cases/CaseTasks/CaseTasksList.vue
+36 -49
@@ -1,13 +1,6 @@
1 <template>
2 <div class="case-tasks-list flex flex-col gap-4">
3 - <CaseTasksToolbar
4 - :case-id
5 - :customer-code
6 - :can-edit
7 - :tasks
8 - :linked-alerts
9 - @updated="fetchTasks"
10 - />
3 + <CaseTasksToolbar :case-id :customer-code :can-edit :tasks :linked-alerts @updated="fetchTasks" />
4
5 <n-spin :show="loading">
6 <div v-if="tasks.length" class="flex flex-col gap-4">
@@ -16,34 +9,40 @@
9 orphaned / never-attached tasks (alert_id IS NULL). Group order:
10 alerts in their linked order, case-wide last.
11 -->
19 - <div v-for="group in groups" :key="group.key" class="task-group flex flex-col gap-2">
20 - <div class="task-group-header flex flex-wrap items-center gap-2">
21 - <template v-if="group.alert">
22 - <span class="task-group-title">{{ group.alert.alert_name }}</span>
23 - <code class="text-tertiary text-xs">{{ group.alert.source }}</code>
24 - <n-tag size="tiny" :bordered="false">alert #{{ group.alert.id }}</n-tag>
25 - </template>
26 - <template v-else>
27 - <span class="task-group-title">Case-wide / general</span>
28 - <n-tag size="tiny" :bordered="false" type="info">
29 - not attached to any alert
30 - </n-tag>
31 - </template>
32 - <n-tag size="tiny" :bordered="false">{{ group.tasks.length }} task(s)</n-tag>
33 - </div>
34 -
35 - <div class="flex flex-col gap-3 pl-2">
36 - <CaseTaskItem
37 - v-for="task in group.tasks"
38 - :key="task.id"
39 - :task
40 - :case-id
41 - :can-edit
42 - @deleted="fetchTasks"
43 - @updated="handleTaskUpdated"
44 - />
45 - </div>
46 - </div>
12 + <CardEntity
13 + v-for="group in groups"
14 + :key="group.key"
15 + embedded
16 + size="small"
17 + header-box-class="flex flex-wrap items-center gap-2"
18 + >
19 + <template #header>
20 + <div v-if="group.alert" class="flex flex-wrap items-center gap-2">
21 + <span class="text-default text-base font-semibold">{{ group.alert.alert_name }}</span>
22 + <n-tag size="small" :bordered="false">{{ group.alert.source }}</n-tag>
23 + <n-tag size="small" :bordered="false">alert #{{ group.alert.id }}</n-tag>
24 + <n-tag size="small" :bordered="false">{{ group.tasks.length }} task(s)</n-tag>
25 + </div>
26 + <div v-else class="flex flex-wrap items-center gap-2">
27 + <span class="text-default text-base font-semibold">Case-wide / general</span>
28 + <n-tag size="small" :bordered="false" type="info">not attached to any alert</n-tag>
29 + <n-tag size="small" :bordered="false">{{ group.tasks.length }} task(s)</n-tag>
30 + </div>
31 + </template>
32 + <template #mainExtra>
33 + <div class="flex flex-col gap-3">
34 + <CaseTaskItem
35 + v-for="task in group.tasks"
36 + :key="task.id"
37 + :task
38 + :case-id
39 + :can-edit
40 + @deleted="fetchTasks"
41 + @updated="handleTaskUpdated"
42 + />
43 + </div>
44 + </template>
45 + </CardEntity>
46 </div>
47 <n-empty v-else-if="!loading" description="No tasks on this case" class="h-32 justify-center" />
48 </n-spin>
@@ -56,6 +55,7 @@ import type { CaseTask } from "@/types/incidentManagement/caseTemplates.d"
55 import { NEmpty, NSpin, NTag, useMessage } from "naive-ui"
56 import { computed, onBeforeMount, ref } from "vue"
57 import Api from "@/api"
58 +import CardEntity from "@/components/common/cards/CardEntity.vue"
59 import CaseTaskItem from "./CaseTaskItem.vue"
60 import CaseTasksToolbar from "./CaseTasksToolbar.vue"
61
@@ -131,16 +131,3 @@ function handleTaskUpdated(task: CaseTask) {
131
132 onBeforeMount(fetchTasks)
133 </script>
134 -
135 -<style scoped lang="scss">
136 -.task-group-header {
137 - padding: 6px 8px;
138 - background: var(--bg-secondary-color);
139 - border: 1px solid var(--border-color);
140 - border-radius: var(--border-radius);
141 -}
142 -.task-group-title {
143 - font-weight: 600;
144 - color: var(--fg-default-color);
145 -}
146 -</style>
frontend/src/components/incidentManagement/cases/CaseTasks/CaseTasksToolbar.vue
+1 -5
@@ -44,11 +44,7 @@
44 title="Add custom task"
45 :style="{ maxWidth: 'min(600px, 90vw)', overflow: 'hidden' }"
46 >
47 - <CaseTasksCreateForm
48 - :case-id
49 - :linked-alerts="linkedAlerts || []"
50 - @success="handleAddTaskSuccess"
51 - />
47 + <CaseTasksCreateForm :case-id :linked-alerts="linkedAlerts || []" @success="handleAddTaskSuccess" />
48 </n-modal>
49
50 <!-- Apply template modal -->
frontend/src/components/incidentManagement/cases/CasesList.vue
+1 -1
@@ -2,7 +2,7 @@
2 <div class="cases-list">
3 <div ref="header" class="header flex items-center justify-end gap-2">
4 <div class="info flex grow gap-2">
5 - <n-popover overlap placement="left">
5 + <n-popover overlap placement="left" to="body">
6 <template #trigger>
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="cursor-help!">
frontend/src/components/incidentManagement/exclusionRules/ExclusionRuleDetails.vue
+1 -1
@@ -85,7 +85,7 @@
85 </n-spin>
86 </n-tab-pane>
87 <n-tab-pane name="Fields" tab="Fields" display-directive="show:lazy">
88 - <div class="p-7 pt-4">
88 + <div class="p-6 pt-3">
89 <CardKV v-for="(val, key) in entity.field_matches" :key size="lg">
90 <template #key>{{ key }}</template>
91 <template #value>
frontend/src/components/incidentManagement/sources/SourceConfigurationWizard.vue
+1 -1
@@ -3,7 +3,7 @@
3 <div class="flex min-h-48 flex-col">
4 <div class="flex grow flex-col">
5 <n-scrollbar x-scrollable trigger="none">
6 - <div class="p-7 pt-4">
6 + <div class="p-6 pt-3">
7 <n-steps :current size="small" :status="currentStatus">
8 <n-step title="Source" />
9 <n-step title="Configuration" />
frontend/src/components/mitre/Group/GroupCard.vue
+3 -3
@@ -36,7 +36,7 @@
36 >
37 <n-tabs type="line" animated :tabs-padding="24">
38 <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy">
39 - <div class="px-7 pt-4 pb-7">
39 + <div class="px-6 pt-3 pb-6">
40 <GroupDetails :entity="groupDetails" />
41 </div>
42 </n-tab-pane>
@@ -45,7 +45,7 @@
45 :tab="`Software (${groupDetails?.software?.length || 0})`"
46 display-directive="show:lazy"
47 >
48 - <div class="px-7 pt-4 pb-7">
48 + <div class="px-6 pt-3 pb-6">
49 <SoftwareList v-if="groupDetails" :list="groupDetails.software" />
50 </div>
51 </n-tab-pane>
@@ -54,7 +54,7 @@
54 :tab="`Techniques (${groupDetails?.techniques?.length || 0})`"
55 display-directive="show:lazy"
56 >
57 - <div class="px-7 pt-4 pb-7">
57 + <div class="px-6 pt-3 pb-6">
58 <TechniquesList v-if="groupDetails" :list="groupDetails.techniques" />
59 </div>
60 </n-tab-pane>
frontend/src/components/mitre/Mitigation/MitigationCard.vue
+2 -2
@@ -36,7 +36,7 @@
36 >
37 <n-tabs type="line" animated :tabs-padding="24">
38 <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy">
39 - <div class="px-7 pt-4 pb-7">
39 + <div class="px-6 pt-3 pb-6">
40 <MitigationDetails :entity="mitigationDetails" />
41 </div>
42 </n-tab-pane>
@@ -45,7 +45,7 @@
45 :tab="`Techniques (${mitigationDetails?.techniques?.length || 0})`"
46 display-directive="show:lazy"
47 >
48 - <div class="px-7 pt-4 pb-7">
48 + <div class="px-6 pt-3 pb-6">
49 <TechniquesList v-if="mitigationDetails" :list="mitigationDetails.techniques" />
50 </div>
51 </n-tab-pane>
frontend/src/components/mitre/Software/SoftwareCard.vue
+3 -3
@@ -36,7 +36,7 @@
36 >
37 <n-tabs type="line" animated :tabs-padding="24">
38 <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy">
39 - <div class="px-7 pt-4 pb-7">
39 + <div class="px-6 pt-3 pb-6">
40 <SoftwareDetails :entity="softwareDetails" />
41 </div>
42 </n-tab-pane>
@@ -45,7 +45,7 @@
45 :tab="`Groups (${softwareDetails?.groups?.length || 0})`"
46 display-directive="show:lazy"
47 >
48 - <div class="px-7 pt-4 pb-7">
48 + <div class="px-6 pt-3 pb-6">
49 <GroupsList v-if="softwareDetails" :list="softwareDetails.groups" />
50 </div>
51 </n-tab-pane>
@@ -54,7 +54,7 @@
54 :tab="`Techniques (${softwareDetails?.techniques?.length || 0})`"
55 display-directive="show:lazy"
56 >
57 - <div class="px-7 pt-4 pb-7">
57 + <div class="px-6 pt-3 pb-6">
58 <TechniquesList v-if="softwareDetails" :list="softwareDetails.techniques" />
59 </div>
60 </n-tab-pane>
frontend/src/components/mitre/Tactic/TacticCard.vue
+2 -2
@@ -36,7 +36,7 @@
36 >
37 <n-tabs type="line" animated :tabs-padding="24">
38 <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy">
39 - <div class="px-7 pt-4 pb-7">
39 + <div class="px-6 pt-3 pb-6">
40 <TacticDetails :entity="tacticDetails" />
41 </div>
42 </n-tab-pane>
@@ -45,7 +45,7 @@
45 :tab="`Techniques (${tacticDetails?.techniques?.length || 0})`"
46 display-directive="show:lazy"
47 >
48 - <div class="px-7 pt-4 pb-7">
48 + <div class="px-6 pt-3 pb-6">
49 <TechniquesList v-if="tacticDetails" :list="tacticDetails.techniques" />
50 </div>
51 </n-tab-pane>
frontend/src/components/mitre/Technique/TechniqueCard.vue
+6 -6
@@ -36,7 +36,7 @@
36 >
37 <n-tabs type="line" animated :tabs-padding="24">
38 <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy">
39 - <div class="px-7 pt-4 pb-7">
39 + <div class="px-6 pt-3 pb-6">
40 <TechniqueAlertDetails :entity="techniqueDetails" />
41 </div>
42 </n-tab-pane>
@@ -45,7 +45,7 @@
45 :tab="`Tactics (${techniqueDetails?.tactics?.length || 0})`"
46 display-directive="show:lazy"
47 >
48 - <div class="px-7 pt-4 pb-7">
48 + <div class="px-6 pt-3 pb-6">
49 <TacticsList v-if="techniqueDetails" :list="techniqueDetails.tactics" />
50 </div>
51 </n-tab-pane>
@@ -54,7 +54,7 @@
54 :tab="`Mitigations (${techniqueDetails?.mitigations?.length || 0})`"
55 display-directive="show:lazy"
56 >
57 - <div class="px-7 pt-4 pb-7">
57 + <div class="px-6 pt-3 pb-6">
58 <MitigationsList v-if="techniqueDetails" :list="techniqueDetails.mitigations" />
59 </div>
60 </n-tab-pane>
@@ -64,7 +64,7 @@
64 :tab="`Techniques (${techniqueDetails?.techniques?.length || 0})`"
65 display-directive="show:lazy"
66 >
67 - <div class="px-7 pt-4 pb-7">
67 + <div class="px-6 pt-3 pb-6">
68 <TechniquesList :list="techniqueDetails.techniques" />
69 </div>
70 </n-tab-pane>
@@ -73,7 +73,7 @@
73 :tab="`Groups (${techniqueDetails?.groups?.length || 0})`"
74 display-directive="show:lazy"
75 >
76 - <div class="px-7 pt-4 pb-7">
76 + <div class="px-6 pt-3 pb-6">
77 <GroupsList v-if="techniqueDetails" :list="techniqueDetails.groups" />
78 </div>
79 </n-tab-pane>
@@ -82,7 +82,7 @@
82 :tab="`Software (${techniqueDetails?.software?.length || 0})`"
83 display-directive="show:lazy"
84 >
85 - <div class="px-7 pt-4 pb-7">
85 + <div class="px-6 pt-3 pb-6">
86 <SoftwareList v-if="techniqueDetails" :list="techniqueDetails.software" />
87 </div>
88 </n-tab-pane>
frontend/src/components/mitre/TechniqueAlert/TechniqueAlertOverview.vue
+7 -7
@@ -2,7 +2,7 @@
2 <n-spin :show="loadingDetails">
3 <n-tabs type="line" animated :tabs-padding="24">
4 <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy">
5 - <div class="px-7 pt-4 pb-7">
5 + <div class="px-6 pt-3 pb-6">
6 <TechniqueAlertDetails v-if="techniqueDetails" :entity="techniqueDetails" />
7 </div>
8 </n-tab-pane>
@@ -11,7 +11,7 @@
11 :tab="`Groups (${techniqueDetails?.groups?.length || 0})`"
12 display-directive="show:lazy"
13 >
14 - <div class="px-7 pt-4 pb-7">
14 + <div class="px-6 pt-3 pb-6">
15 <GroupsList v-if="techniqueDetails" :list="techniqueDetails.groups" />
16 </div>
17 </n-tab-pane>
@@ -20,7 +20,7 @@
20 :tab="`Mitigations (${techniqueDetails?.mitigations?.length || 0})`"
21 display-directive="show:lazy"
22 >
23 - <div class="px-7 pt-4 pb-7">
23 + <div class="px-6 pt-3 pb-6">
24 <MitigationsList v-if="techniqueDetails" :list="techniqueDetails.mitigations" />
25 </div>
26 </n-tab-pane>
@@ -29,7 +29,7 @@
29 :tab="`Software (${techniqueDetails?.software?.length || 0})`"
30 display-directive="show:lazy"
31 >
32 - <div class="px-7 pt-4 pb-7">
32 + <div class="px-6 pt-3 pb-6">
33 <SoftwareList v-if="techniqueDetails" :list="techniqueDetails.software" />
34 </div>
35 </n-tab-pane>
@@ -38,17 +38,17 @@
38 :tab="`Tactics (${techniqueDetails?.tactics?.length || 0})`"
39 display-directive="show:lazy"
40 >
41 - <div class="px-7 pt-4 pb-7">
41 + <div class="px-6 pt-3 pb-6">
42 <TacticsList v-if="techniqueDetails" :list="techniqueDetails.tactics" />
43 </div>
44 </n-tab-pane>
45 <n-tab-pane name="Alerts" tab="Alerts" display-directive="show:lazy">
46 - <div class="px-7 pt-4 pb-7">
46 + <div class="px-6 pt-3 pb-6">
47 <TechniqueEventsList v-if="techniqueDetails" :external-id />
48 </div>
49 </n-tab-pane>
50 <n-tab-pane name="Atomic test" tab="Atomic test" display-directive="show:lazy">
51 - <div class="px-7 pt-4 pb-7">
51 + <div class="px-6 pt-3 pb-6">
52 <TechniqueCardContent :technique-id="externalId" />
53 </div>
54 </n-tab-pane>
frontend/src/components/mitre/TechniqueEvents/TechniqueEventCard.vue
+10 -10
@@ -108,7 +108,7 @@
108 >
109 <n-tabs type="line" animated :tabs-padding="24">
110 <n-tab-pane name="Agent" tab="Agent" display-directive="show">
111 - <div v-if="agentProperties" class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
111 + <div v-if="agentProperties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
112 <CardKV v-for="(value, key) of agentProperties" :key>
113 <template #key>
114 {{ key }}
@@ -150,7 +150,7 @@
150 :tab="tabCard.tab"
151 display-directive="show"
152 >
153 - <div v-if="tabCard.properties" class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
153 + <div v-if="tabCard.properties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
154 <CardKV v-for="(value, key) of tabCard.properties" :key>
155 <template #key>
156 {{ key }}
@@ -163,7 +163,7 @@
163 </n-tab-pane>
164
165 <n-tab-pane name="DNS" tab="DNS" display-directive="show">
166 - <div class="px-7 pt-4">
166 + <div class="px-6 pt-3">
167 <CardKV v-if="alert.data_dns_answers">
168 <template #key>data_dns_answers</template>
169 <template #value>
@@ -171,7 +171,7 @@
171 </template>
172 </CardKV>
173 </div>
174 - <div v-if="dnsProperties" class="grid-auto-fit-200 grid gap-2 p-7 pt-2">
174 + <div v-if="dnsProperties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
175 <CardKV v-for="(value, key) of dnsProperties" :key>
176 <template #key>
177 {{ key }}
@@ -184,7 +184,7 @@
184 </n-tab-pane>
185
186 <n-tab-pane name="GL2" tab="GL2" display-directive="show">
187 - <div class="px-7 pt-4">
187 + <div class="px-6 pt-3">
188 <CardKV v-if="alert.gl2_processing_error">
189 <template #key>gl2_processing_error</template>
190 <template #value>
@@ -192,7 +192,7 @@
192 </template>
193 </CardKV>
194 </div>
195 - <div v-if="gl2Properties" class="grid-auto-fit-200 grid gap-2 p-7 pt-2">
195 + <div v-if="gl2Properties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
196 <CardKV v-for="(value, key) of gl2Properties" :key>
197 <template #key>
198 {{ key }}
@@ -205,19 +205,19 @@
205 </n-tab-pane>
206
207 <n-tab-pane v-if="alert.message" name="Message" tab="Message" display-directive="show">
208 - <div class="p-7 pt-4">
208 + <div class="p-6 pt-3">
209 <CodeSource :code="alert.message" :decode="false" />
210 </div>
211 </n-tab-pane>
212
213 <n-tab-pane v-if="alert.location" name="Location" tab="Location" display-directive="show">
214 - <div class="p-7 pt-4">
214 + <div class="p-6 pt-3">
215 <CodeSource :code="alert.location" :decode="false" />
216 </div>
217 </n-tab-pane>
218
219 <n-tab-pane v-if="alert.streams?.length" name="Streams" tab="Streams" display-directive="show">
220 - <div class="flex flex-wrap gap-3 p-7 pt-4">
220 + <div class="flex flex-wrap gap-3 p-6 pt-3">
221 <ul>
222 <li v-for="stream of alert.streams" :key="stream">
223 <code>{{ stream }}</code>
@@ -228,7 +228,7 @@
228 </template>
229
230 <n-tab-pane name="Details" tab="Details" display-directive="show:lazy">
231 - <div class="p-7 pt-4">
231 + <div class="p-6 pt-3">
232 <CodeSource :code="alert" lang="json" :decode="false" />
233 </div>
234 </n-tab-pane>
frontend/src/components/sigma/QueryDetails.vue
+1 -1
@@ -6,7 +6,7 @@
6 </div>
7 </n-tab-pane>
8 <n-tab-pane name="Rule Query" tab="Rule Query" display-directive="show:lazy">
9 - <div class="p-7 pt-4">
9 + <div class="p-6 pt-3">
10 <CodeSource :code="query.rule_query" lang="sql" />
11 </div>
12 </n-tab-pane>
frontend/src/components/soc/SocAlerts/SocAlertAssets/SocAlertAssetsItem.vue
+3 -3
@@ -70,7 +70,7 @@
70 >
71 <n-tabs type="line" animated :tabs-padding="24">
72 <n-tab-pane name="Info" tab="Info" display-directive="show">
73 - <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
73 + <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
74 <CardKV v-for="(value, key) of properties" :key>
75 <template #key>
76 {{ key }}
@@ -95,7 +95,7 @@
95 </div>
96 </n-tab-pane>
97 <n-tab-pane name="Type" tab="Type" display-directive="show">
98 - <div v-if="assetType" class="grid-auto-fit-250 grid gap-2 p-7 pt-4">
98 + <div v-if="assetType" class="grid-auto-fit-250 grid gap-2 p-6 pt-3">
99 <CardKV v-for="(value, key) of assetType" :key>
100 <template #key>
101 {{ key }}
@@ -107,7 +107,7 @@
107 </div>
108 </n-tab-pane>
109 <n-tab-pane name="Description" tab="Description" display-directive="show">
110 - <div class="p-7 pt-4">
110 + <div class="p-6 pt-3">
111 <template v-if="isUrlLike(asset.asset_description)">
112 <a
113 :href="asset.asset_description"
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemDetails.vue
+7 -7
@@ -1,15 +1,15 @@
1 <template>
2 <n-tabs v-if="alert" type="line" animated :tabs-padding="24">
3 <n-tab-pane name="Context" tab="Context" display-directive="show:lazy">
4 - <SocAlertItemContext :alert class="p-7 pt-4" />
4 + <SocAlertItemContext :alert class="p-6 pt-3" />
5 </n-tab-pane>
6 <n-tab-pane name="Note" tab="Note" display-directive="show:lazy">
7 - <div class="p-7 pt-4">
7 + <div class="p-6 pt-3">
8 {{ alert.alert_note ?? "No notes for this alert" }}
9 </div>
10 </n-tab-pane>
11 <n-tab-pane name="Customer" tab="Customer" display-directive="show:lazy">
12 - <div class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
12 + <div class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
13 <CardKV v-for="(value, key) of alert.customer" :key>
14 <template #key>
15 {{ key }}
@@ -32,7 +32,7 @@
32 </div>
33 </n-tab-pane>
34 <n-tab-pane name="Owner" tab="Owner" display-directive="show:lazy">
35 - <div class="grid gap-2 px-7 pt-4">
35 + <div class="grid gap-2 px-6 pt-3">
36 <Badge
37 type="active"
38 style="max-width: 145px"
@@ -45,7 +45,7 @@
45 <template #label>Go to users page</template>
46 </Badge>
47 </div>
48 - <div class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
48 + <div class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
49 <CardKV>
50 <template #key>user_login</template>
51 <template #value>
@@ -75,12 +75,12 @@
75 </div>
76 </n-tab-pane>
77 <n-tab-pane name="History" tab="History" display-directive="show:lazy">
78 - <div class="p-7 pt-4">
78 + <div class="p-6 pt-3">
79 <SocAlertItemTimeline :alert />
80 </div>
81 </n-tab-pane>
82 <n-tab-pane name="Details" tab="Details" display-directive="show:lazy">
83 - <div class="p-7 pt-4">
83 + <div class="p-6 pt-3">
84 <SimpleJsonViewer class="vuesjv-override" :model-value="socAlertDetail" :initial-expanded-depth="1" />
85 </div>
86 </n-tab-pane>
frontend/src/components/soc/SocCases/SocCaseAssetsItem.vue
+2 -2
@@ -65,7 +65,7 @@
65 >
66 <n-tabs type="line" animated :tabs-padding="24">
67 <n-tab-pane name="Info" tab="Info" display-directive="show">
68 - <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
68 + <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
69 <CardKV v-for="(value, key) of properties" :key>
70 <template #key>
71 {{ key }}
@@ -77,7 +77,7 @@
77 </div>
78 </n-tab-pane>
79 <n-tab-pane name="Description" tab="Description" display-directive="show">
80 - <div class="p-7 pt-4">
80 + <div class="p-6 pt-3">
81 <template v-if="isUrlLike(asset.asset_description)">
82 <a
83 :href="asset.asset_description"
frontend/src/components/soc/SocCases/SocCaseItem.vue
+3 -3
@@ -153,7 +153,7 @@
153 </code>
154 </div>
155 </div>
156 - <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
156 + <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
157 <CardKV v-for="(value, key) of properties" :key>
158 <template #key>
159 {{ key }}
@@ -177,7 +177,7 @@
177 </n-spin>
178 </n-tab-pane>
179 <n-tab-pane name="Description" tab="Description" display-directive="show">
180 - <div v-if="baseInfo" class="p-7 pt-4">
180 + <div v-if="baseInfo" class="p-6 pt-3">
181 <n-input
182 :value="baseInfo.case_description"
183 type="textarea"
@@ -192,7 +192,7 @@
192 </n-tab-pane>
193 <n-tab-pane name="History" tab="History" display-directive="show:lazy">
194 <n-spin :show="loadingDetails">
195 - <div class="p-7 pt-4">
195 + <div class="p-6 pt-3">
196 <SocCaseTimeline v-if="extendedInfo" :case-data="extendedInfo" />
197 </div>
198 </n-spin>
frontend/src/components/soc/SocCases/SocCaseNote.vue
+3 -3
@@ -58,7 +58,7 @@
58 >
59 <n-tabs type="line" animated :tabs-padding="24">
60 <n-tab-pane name="Info" tab="Info" display-directive="show">
61 - <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
61 + <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
62 <CardKV v-for="(value, key) of properties" :key>
63 <template #key>
64 {{ key }}
@@ -70,7 +70,7 @@
70 </div>
71 </n-tab-pane>
72 <n-tab-pane name="Content" tab="Content" display-directive="show">
73 - <div class="p-7 pt-4">
73 + <div class="p-6 pt-3">
74 <n-input
75 :value="note.note_details.note_content"
76 type="textarea"
@@ -85,7 +85,7 @@
85 </div>
86 </n-tab-pane>
87 <n-tab-pane name="History" tab="History" display-directive="show:lazy">
88 - <div class="p-7 pt-4">
88 + <div class="p-6 pt-3">
89 <SocCaseNoteTimeline :note />
90 </div>
91 </n-tab-pane>
frontend/src/components/soc/SocCases/SocCaseNotesList.vue
+2 -2
@@ -1,10 +1,10 @@
1 <template>
2 <div class="soc-notes-list">
3 - <div class="px-7 pt-4">
3 + <div class="px-6 pt-3">
4 <n-input v-model:value="notesFilter" placeholder="Search notes..." clearable />
5 </div>
6 <n-spin :show="loadingNotes" class="min-h-28">
7 - <div v-if="notesList?.length" class="flex flex-col gap-2 p-7 pt-3" style="container-type: inline-size">
7 + <div v-if="notesList?.length" class="flex flex-col gap-2 p-6 pt-3" style="container-type: inline-size">
8 <SocCaseNote v-for="note of notesList" :key="note.note_id" :note />
9 </div>
10 <template v-else>
frontend/src/components/threatIntel/AIAnalystButton.vue
+3 -3
@@ -64,7 +64,7 @@
64 </template>
65 <n-tabs type="line" animated :tabs-padding="24">
66 <n-tab-pane v-if="analysisResponse?.analysis" name="Analysis" tab="Analysis" display-directive="show">
67 - <div class="p-7 pt-4">
67 + <div class="p-6 pt-3">
68 <Markdown :source="analysisResponse.analysis" />
69 </div>
70 </n-tab-pane>
@@ -74,7 +74,7 @@
74 tab="Base64 Decoded"
75 display-directive="show"
76 >
77 - <div class="p-7 pt-4">
77 + <div class="p-6 pt-3">
78 <CodeSource :code="analysisResponse.base64_decoded" decode />
79 </div>
80 </n-tab-pane>
@@ -84,7 +84,7 @@
84 tab="Threat Indicators"
85 display-directive="show"
86 >
87 - <div class="p-7 pt-4">
87 + <div class="p-6 pt-3">
88 <Markdown :source="analysisResponse.threat_indicators" />
89 </div>
90 </n-tab-pane>
frontend/src/components/threatIntel/ThreatIntelButton.vue
+9 -11
@@ -15,17 +15,15 @@
15 :mask-closable="false"
16 display-directive="show"
17 >
18 - <n-drawer-content title="Threat Intel" closable :native-scrollbar="false">
19 - <div class="py-3">
20 - <n-collapse default-expanded-names="1" accordion display-directive="show">
21 - <n-collapse-item title="SOCFortress Threat Intel" name="1">
22 - <ThreatIntelForm @mounted="threatIntelCTX = $event" />
23 - </n-collapse-item>
24 - <n-collapse-item title="Virus Total" name="2">
25 - <VirusTotalForm @mounted="virusTotalCTX = $event" />
26 - </n-collapse-item>
27 - </n-collapse>
28 - </div>
18 + <n-drawer-content title="Threat Intel" closable :native-scrollbar="false" body-content-class="px-0! py-4!">
19 + <n-collapse default-expanded-names="1" accordion display-directive="show">
20 + <n-collapse-item title="SOCFortress Threat Intel" name="1" class="px-6">
21 + <ThreatIntelForm @mounted="threatIntelCTX = $event" />
22 + </n-collapse-item>
23 + <n-collapse-item title="Virus Total" name="2" class="px-6">
24 + <VirusTotalForm @mounted="virusTotalCTX = $event" />
25 + </n-collapse-item>
26 + </n-collapse>
27 </n-drawer-content>
28 </n-drawer>
29 </template>
frontend/src/components/threatIntel/ThreatIntelEpssScore.vue
+5 -24
@@ -8,29 +8,17 @@
8 </template>
9 </CardKV>
10 </div>
11 - <div v-if="epssList.length" class="flex flex-col gap-3">
11 + <div v-if="epssList.length" class="@container flex flex-col gap-3">
12 <n-card
13 v-for="item of epssList"
14 :key="item.___id"
15 embedded
16 class="item-appear item-appear-bottom item-appear-005 overflow-hidden"
17 >
18 - <div class="xs:flex!-row flex flex-col justify-between gap-8">
19 - <n-statistic class="grow" label="Date">
20 - <span class="stats-value whitespace-nowrap">
21 - {{ formatDate(item.date, dFormats.date) }}
22 - </span>
23 - </n-statistic>
24 - <n-statistic class="grow" label="EPSS">
25 - <span class="stats-value">
26 - {{ item.epss }}
27 - </span>
28 - </n-statistic>
29 - <n-statistic class="grow" label="Percentile">
30 - <span class="stats-value">
31 - {{ item.percentile }}
32 - </span>
33 - </n-statistic>
18 + <div class="flex flex-col justify-between gap-8 @md:flex-row">
19 + <n-statistic class="grow" label="Date" :value="`${formatDate(item.date, dFormats.date)}`" />
20 + <n-statistic class="grow" label="EPSS" :value="item.epss" tabular-nums />
21 + <n-statistic class="grow" label="Percentile" :value="item.percentile" tabular-nums />
22 </div>
23 </n-card>
24 <p v-if="epssModelLink" class="w-full text-right">
@@ -96,10 +84,3 @@ onBeforeMount(() => {
84 getData()
85 })
86 </script>
99 -
100 -<style lang="scss" scoped>
101 -.stats-value {
102 - font-family: var(--font-family-mono);
103 - font-size: clamp(18px, 2.3vw, var(--n-value-font-size));
104 -}
105 -</style>
frontend/src/components/threatIntel/ThreatIntelForm.vue
+3 -4
@@ -1,10 +1,9 @@
1 <template>
2 <n-spin :show="loading">
3 <div class="flex flex-col gap-3">
4 - <div class="flex flex-col gap-1">
5 - <small class="ml-2">IOC Value:</small>
4 + <n-form-item label="IOC Value" :show-feedback="false">
5 <n-input v-model:value.trim="iocValue" placeholder="IPv4, domain, or SHA256 hash" clearable />
7 - </div>
6 + </n-form-item>
7 <div class="flex justify-end">
8 <n-button type="primary" :disabled="!isValid" @click="create()">Submit</n-button>
9 </div>
@@ -76,7 +75,7 @@
75 <script setup lang="ts">
76 import type { ThreatIntelResponse } from "@/types/threatIntel.d"
77 import _trim from "lodash/trim"
79 -import { NButton, NInput, NSpin, useMessage } from "naive-ui"
78 +import { NButton, NFormItem, NInput, NSpin, useMessage } from "naive-ui"
79 import { computed, onMounted, ref } from "vue"
80 import Api from "@/api"
81 import { useSettingsStore } from "@/stores/settings"
frontend/src/components/threatIntel/ThreatIntelProcessEvaluationProvider.vue
+7 -7
@@ -18,7 +18,7 @@
18 tab="Analysis"
19 display-directive="show"
20 >
21 - <div class="p-7 pt-4">
21 + <div class="p-6 pt-3">
22 <Markdown :source="mcpResponse.structured_result.response" />
23 </div>
24 </n-tab-pane>
@@ -28,7 +28,7 @@
28 tab="Thinking Process"
29 display-directive="show:lazy"
30 >
31 - <div class="p-7 pt-4">
31 + <div class="p-6 pt-3">
32 <Markdown :source="mcpResponse.structured_result.thinking_process" />
33 </div>
34 </n-tab-pane>
@@ -55,7 +55,7 @@
55 </div>
56 </n-tab-pane>
57 <n-tab-pane v-if="evaluation" name="Intel" tab="Intel" display-directive="show:lazy">
58 - <div class="p-7 pt-4">
58 + <div class="p-6 pt-3">
59 <n-input
60 :value="evaluation.intel"
61 type="textarea"
@@ -71,7 +71,7 @@
71 </n-tab-pane>
72 <n-tab-pane v-if="evaluation?.hashes?.length" name="Hashes" tab="Hashes" display-directive="show:lazy">
73 <ListPercentage
74 - class="p-7 pt-4"
74 + class="p-6 pt-3"
75 :list="evaluation.hashes"
76 label-key="hash"
77 percentage-key="percentage"
@@ -84,7 +84,7 @@
84 display-directive="show:lazy"
85 >
86 <ListPercentage
87 - class="p-7 pt-4"
87 + class="p-6 pt-3"
88 :list="evaluation.network"
89 label-key="port"
90 percentage-key="usage"
@@ -97,7 +97,7 @@
97 display-directive="show:lazy"
98 >
99 <ListPercentage
100 - class="p-7 pt-4"
100 + class="p-6 pt-3"
101 :list="evaluation.parents"
102 label-key="name"
103 percentage-key="percentage"
@@ -105,7 +105,7 @@
105 </n-tab-pane>
106 <n-tab-pane v-if="evaluation?.paths?.length" name="Paths" tab="Paths" display-directive="show:lazy">
107 <ListPercentage
108 - class="p-7 pt-4"
108 + class="p-6 pt-3"
109 :list="evaluation.paths"
110 label-key="directory"
111 percentage-key="percentage"
frontend/src/components/threatIntel/VirusTotalEnrichmentButton.vue
+2 -2
@@ -112,7 +112,7 @@
112 </div>
113 </n-tab-pane>
114 <n-tab-pane name="Details" tab="Details" display-directive="show">
115 - <div class="p-7 pt-4">
115 + <div class="p-6 pt-3">
116 <CodeSource :code="properties" decode />
117 </div>
118 </n-tab-pane>
@@ -122,7 +122,7 @@
122 tab="Whois"
123 display-directive="show"
124 >
125 - <div class="p-7 pt-4">
125 + <div class="p-6 pt-3">
126 <div
127 v-if="virusTotalDataResponse.attributes.whois_date"
128 class="mb-4 text-right font-mono text-sm"
frontend/src/components/vulnerabilities/VulnerabilityCardContent.vue
+1 -1
@@ -75,7 +75,7 @@
75 </div>
76 </n-tab-pane>
77 <n-tab-pane name="Timeline" tab="Timeline" display-directive="show:lazy">
78 - <div class="p-7 pt-4">
78 + <div class="p-6 pt-3">
79 <n-timeline>
80 <n-timeline-item
81 v-if="vulnerability.published_at"
frontend/src/theme/index.ts
+9
@@ -114,6 +114,7 @@ export function getThemeOverrides(state: ThemeState): GlobalThemeOverrides {
114 Card: {
115 color: background,
116 colorEmbedded: backgroundSecondary,
117 + colorEmbeddedModal: backgroundSecondary,
118 titleFontSizeSmall: state.fontSize.cardTitle,
119 titleFontSizeMedium: state.fontSize.cardTitle,
120 titleFontSizeLarge: state.fontSize.cardTitle,
@@ -129,6 +130,14 @@ export function getThemeOverrides(state: ThemeState): GlobalThemeOverrides {
130 LoadingBar: {
131 colorLoading: primary
132 },
133 + Modal: {
134 + peers: {
135 + Card: {
136 + color: background,
137 + colorEmbedded: backgroundSecondary
138 + }
139 + }
140 + },
141 Tag: {
142 colorBordered: "rgba(0, 0, 0, 0.1)"
143 },
frontend/src/utils/format.ts
+9 -2
@@ -6,13 +6,20 @@ export function formatBytes(val: string | number) {
6 return bytes(_toNumber(val))
7 }
8
9 -export function formatDate(date: Date | string | number, format: string) {
9 +export function formatDate(date: Date | string | number, format: string, opts?: { utc?: boolean; tz?: boolean }) {
10 let parsedDate = date
11 if (typeof date === "number" && date.toString().length >= 15) {
12 parsedDate = date / 1000
13 }
14 - const datejs = dayjs(parsedDate)
14 +
15 + let datejs = opts?.tz ? dayjs.utc(parsedDate) : dayjs(parsedDate)
16 if (!datejs.isValid()) return date
17
18 + if (opts?.tz) {
19 + datejs = datejs.tz(dayjs.tz.guess())
20 + } else if (opts?.utc) {
21 + datejs = datejs.utc()
22 + }
23 +
24 return datejs.format(format)
25 }
frontend/src/utils/index.ts
+1 -1
@@ -132,7 +132,7 @@ export function getNameInitials(name: string, cap?: number) {
132 initials = name
133 .split(" ")
134 .map(chunk => chunk[0])
135 - .join()
135 + .join("")
136 }
137
138 return (cap ? initials.slice(0, cap) : initials).toUpperCase()
frontend/src/views/agents/Agents.vue
+10 -15
@@ -1,7 +1,7 @@
1 <template>
2 <div class="page page-wrapped page-without-footer flex flex-col">
3 <div class="wrapper flex grow gap-4">
4 - <div class="sidebar">
4 + <div class="sidebar p-px">
5 <AgentToolbar
6 v-model="textFilter"
7 :syncing="loadingSync"
@@ -9,6 +9,7 @@
9 :agents-filtered-length="agentsFiltered.length"
10 :agents-critical
11 :agents-online
12 + hide-selection-switch
13 :selection-mode
14 :selected-count="selectedAgents.length"
15 @run="runCommand($event)"
@@ -21,15 +22,15 @@
22 <div class="main flex grow flex-col overflow-hidden">
23 <n-spin class="flex h-full w-full flex-col overflow-hidden" :show="loadingAgents">
24 <n-scrollbar class="grow">
24 - <div class="agents-list flex grow flex-col gap-3">
25 + <div class="agents-list flex grow flex-col gap-3 p-px">
26 <template v-if="agentsFiltered.length">
27 <AgentCard
28 v-for="agent in itemsPaginated"
29 :key="agent.agent_id"
30 :agent
30 - :show-actions="!selectionMode"
31 :selectable="selectionMode"
32 :selected="isAgentSelected(agent)"
33 + show-actions
34 hoverable
35 clickable
36 class="item-appear item-appear-bottom item-appear-005"
@@ -89,7 +90,7 @@ const page = ref(1)
90 const pageSize = ref(20)
91
92 // Selection mode state
92 -const selectionMode = ref(false)
93 +const selectionMode = ref(true)
94 const selectedAgents = ref<Agent[]>([])
95 const showBulkDeleteModal = ref(false)
96
@@ -161,16 +162,11 @@ function clearSelection() {
162 }
163
164 function handleAgentClick(agent: Agent) {
164 - if (selectionMode.value) {
165 - toggleAgentSelection(agent)
166 - } else {
167 - routeAgent(agent.agent_id).navigate()
168 - }
165 + routeAgent(agent.agent_id).navigate()
166 }
167
168 function onBulkDeleteComplete() {
169 clearSelection()
173 - selectionMode.value = false
170 getAgents()
171 }
172
@@ -266,6 +262,10 @@ onBeforeMount(() => {
262 height: 100%;
263 overflow: hidden;
264
265 + @media (max-width: 1023px) {
266 + flex-direction: column;
267 + }
268 +
269 .sidebar {
270 .agent-toolbar {
271 height: 100%;
@@ -336,10 +336,5 @@ onBeforeMount(() => {
336 }
337 }
338 }
339 - @container (max-width: 770px) {
340 - .wrapper {
341 - flex-direction: column;
342 - }
343 - }
339 }
340 </style>
frontend/src/views/agents/Overview.vue
+66 -149
@@ -1,107 +1,90 @@
1 <template>
2 <div class="page">
3 - <div class="agent-toolbar">
4 - <div class="back-btn" @click="routeAgent().navigate()">
5 - <Icon :name="ArrowIcon" :size="16" />
6 - <span>Agents list</span>
7 - </div>
8 - <div v-if="agent" class="delete-btn" @click.stop="handleDelete">Delete Agent</div>
3 + <div class="flex items-center justify-between">
4 + <n-button text size="small" :focusable="false" @click="routeAgent().navigate()">
5 + <template #icon>
6 + <Icon :name="ArrowIcon" :size="16" />
7 + </template>
8 + Back to agents list
9 + </n-button>
10 + <n-button v-if="agent" text size="small" :focusable="false" @click.stop="handleDelete">
11 + <template #icon>
12 + <Icon :name="DeleteIcon" :size="16" />
13 + </template>
14 + Delete Agent
15 + </n-button>
16 </div>
10 - <CardEntity
11 - class="agent-header my-4"
12 - :class="{ critical: agent?.critical_asset, online: isOnline }"
13 - :loading="loadingAgent"
14 - >
15 - <div class="flex flex-wrap items-start justify-between gap-x-6 gap-y-1">
16 - <div class="info grow">
17 - <div class="title">
18 - <div v-if="agent" class="critical" :class="{ active: agent?.critical_asset }">
19 - <n-tooltip>
20 - Toggle Critical Assets
21 - <template #trigger>
22 - <n-button
23 - text
24 - :type="agent?.critical_asset ? 'warning' : 'default'"
25 - circle
26 - @click.stop="toggleCritical(agent.agent_id, agent.critical_asset)"
27 - >
28 - <template #icon>
29 - <Icon :name="StarIcon" />
30 - </template>
31 - </n-button>
17 +
18 + <CardEntity class="my-4" :highlighted="agent?.critical_asset" :loading="loadingAgent">
19 + <template #headerMain>
20 + <div v-if="agent" class="text-default text-xl font-bold">
21 + {{ agent.hostname }}
22 + </div>
23 + </template>
24 + <template #headerExtra>
25 + <div class="flex items-center gap-2">
26 + <n-tag :type="isOnline ? 'success' : 'default'" round class="rounded-lg!" :bordered="false">
27 + {{ isOnline ? "ONLINE" : "OFFLINE" }}
28 + </n-tag>
29 +
30 + <n-tooltip v-if="agent">
31 + <span class="text-sm">Toggle Critical Assets</span>
32 + <template #trigger>
33 + <n-button
34 + :type="agent.critical_asset ? 'error' : 'default'"
35 + ghost
36 + size="small"
37 + @click.stop="toggleCritical(agent.agent_id, agent.critical_asset)"
38 + >
39 + <template #icon>
40 + <Icon :name="agent.critical_asset ? 'carbon:warning-alt' : 'carbon:checkmark'" />
41 </template>
33 - </n-tooltip>
34 - </div>
42 + {{ agent.critical_asset ? "Critical Asset" : "Non-Critical Asset" }}
43 + </n-button>
44 + </template>
45 + </n-tooltip>
46
36 - <h1 v-if="agent?.hostname">
37 - {{ agent?.hostname }}
38 - </h1>
47 + <n-tag v-if="agent?.quarantined" type="warning" round class="rounded-lg!" :bordered="false">
48 + Quarantined
49 + </n-tag>
50
40 - <n-tag v-if="isOnline" type="success" round :bordered="false">ONLINE</n-tag>
41 - <n-tag v-if="isQuarantined" type="warning" round :bordered="false">
42 - <template #icon>
43 - <Icon :name="QuarantinedIcon" />
44 - </template>
45 - <span>QUARANTINED</span>
46 - </n-tag>
47 - </div>
48 - <div class="label text-secondary mt-2">Agent #{{ agent?.agent_id }}</div>
49 - </div>
50 - <div class="actions flex grow items-center justify-end">
51 - <n-button size="small" ghost type="primary" :loading="upgradingAgent" @click="upgradeWazuhAgent()">
51 + <n-button size="small" :loading="upgradingAgent" @click="upgradeWazuhAgent()">
52 Upgrade Wazuh Agent
53 </n-button>
54 </div>
55 - </div>
55 + </template>
56 </CardEntity>
57 - <n-card class="px-4 py-1 pb-4" content-style="padding:0">
57 +
58 + <n-card content-class="p-4! pt-1!">
59 <n-spin :show="loadingAgent">
59 - <n-tabs type="line" animated default-value="Overview">
60 + <n-tabs type="line" animated default-value="Overview" pane-wrapper-class="min-h-100">
61 <n-tab-pane name="Overview" tab="Overview" display-directive="show">
61 - <div class="section">
62 - <OverviewSection v-if="agent" :agent @updated="getAgent()" />
63 - </div>
62 + <OverviewSection v-if="agent" :agent @updated="getAgent()" />
63 </n-tab-pane>
64 <n-tab-pane name="Vulnerabilities" tab="Vulnerabilities" display-directive="show:lazy">
66 - <div class="section">
67 - <VulnerabilitiesGrid v-if="agent" :agent />
68 - </div>
65 + <VulnerabilitiesGrid v-if="agent" :agent />
66 </n-tab-pane>
67 <n-tab-pane name="SCA" tab="SCA" display-directive="show:lazy">
71 - <div class="section">
72 - <ScaTable v-if="agent" :agent />
73 - </div>
68 + <ScaTable v-if="agent" :agent />
69 </n-tab-pane>
70 <n-tab-pane name="Cases" tab="Cases" display-directive="show:lazy">
76 - <div class="section">
77 - <!--
78 - <AgentCases v-if="agent" :agent />
79 - -->
80 - <CasesList
81 - v-if="agent"
82 - class="px-1"
83 - :preset="{ type: 'hostname', value: agent.hostname }"
84 - hide-filters
85 - />
86 - </div>
71 + <!--
72 + <AgentCases v-if="agent" :agent />
73 + -->
74 + <CasesList v-if="agent" :preset="{ type: 'hostname', value: agent.hostname }" hide-filters />
75 </n-tab-pane>
76 <n-tab-pane name="Artifacts" tab="Artifacts" display-directive="show:lazy">
89 - <div class="section">
90 - <AgentFlowList v-if="agent" :agent />
91 - </div>
77 + <AgentFlowList v-if="agent" :agent />
78 </n-tab-pane>
79 <n-tab-pane name="Alerts" tab="Alerts" display-directive="show:lazy">
94 - <div class="section">
95 - <!--
96 - <AlertsList v-if="agent" :agent-hostname="agent.hostname" />
97 - -->
98 - <AlertsList
99 - v-if="agent"
100 - class="px-1"
101 - :preset="[{ type: 'assetName', value: agent.hostname }]"
102 - :show-filters="false"
103 - />
104 - </div>
80 + <!--
81 + <AlertsList v-if="agent" :agent-hostname="agent.hostname" />
82 + -->
83 + <AlertsList
84 + v-if="agent"
85 + :preset="[{ type: 'assetName', value: agent.hostname }]"
86 + :show-filters="false"
87 + />
88 </n-tab-pane>
89 <n-tab-pane name="collect" tab="Collect" display-directive="show:lazy">
90 <ArtifactsCollect
@@ -182,9 +165,8 @@ const FileCollectionForm = defineAsyncComponent(
165 () => import("@/components/agents/fileCollection/FileCollectionForm.vue")
166 )
167
185 -const StarIcon = "carbon:star"
186 -const QuarantinedIcon = "ph:seal-warning-light"
168 const ArrowIcon = "carbon:arrow-left"
169 +const DeleteIcon = "ph:trash"
170
171 const { routeAgent } = useNavigation()
172 const message = useMessage()
@@ -202,10 +184,6 @@ const isOnline = computed(() => {
184 return agent.value?.wazuh_agent_status === AgentStatus.Active
185 })
186
205 -const isQuarantined = computed(() => {
206 - return !!agent.value?.quarantined
207 -})
208 -
187 function getAgent() {
188 if (agentId.value) {
189 loadingAgent.value = true
@@ -302,64 +280,3 @@ onBeforeMount(() => {
280 }
281 })
282 </script>
305 -
306 -<style lang="scss" scoped>
307 -.page {
308 - .agent-toolbar {
309 - display: flex;
310 - justify-content: space-between;
311 - align-items: center;
312 -
313 - .back-btn {
314 - cursor: pointer;
315 - opacity: 0.8;
316 - font-size: 14px;
317 -
318 - i {
319 - font-size: 20px;
320 - }
321 -
322 - span {
323 - position: relative;
324 - top: -3px;
325 - margin-left: 4px;
326 - }
327 - }
328 -
329 - .delete-btn {
330 - opacity: 0.8;
331 - cursor: pointer;
332 - font-size: 14px;
333 - }
334 - }
335 -
336 - .agent-header {
337 - .title {
338 - display: flex;
339 - align-items: center;
340 - line-height: 1;
341 - gap: calc(var(--spacing) * 4);
342 -
343 - h1 {
344 - margin: 0;
345 - font-size: var(--text-2xl);
346 - word-break: break-all;
347 - }
348 -
349 - .critical {
350 - display: flex;
351 - align-items: center;
352 - }
353 - }
354 -
355 - &.critical {
356 - border-color: var(--warning-color);
357 - }
358 - }
359 -
360 - .section {
361 - margin-top: calc(var(--spacing) * 2);
362 - min-height: 200px;
363 - }
364 -}
365 -</style>