@cryptotaxi247 / CoPilot / commits / e40f4472

365 ability to merge incident alerts (#382)

* Add bulk creation endpoint for case alert links * chore: update dependencies in frontend * feat: add multi alert link * refactor: lint * feat: add case alert unlink functionality * feat: update alerts endpoint to include total filtered count and adjust counts to not include the filtered values * feat: include linked cases in alerts output for multiple filters * refactor: improve alerts list * fix: mobile navbar * feat: add unlink case feature * refactor: lint * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Dec 19, 2024 at 11:40 UTC e40f4472cec2419f8ed2dbb93c4c44f9580fb414
16 files changed +493 -223
backend/app/incidents/routes/db_operations.py
+62 -37
@@ -49,6 +49,10 @@ from app.incidents.schema.db_operations import AvailableSourcesResponse
49 from app.incidents.schema.db_operations import AvailableUsersResponse
50 from app.incidents.schema.db_operations import CaseAlertLinkCreate
51 from app.incidents.schema.db_operations import CaseAlertLinkResponse
52 +from app.incidents.schema.db_operations import CaseAlertLinksCreate
53 +from app.incidents.schema.db_operations import CaseAlertLinksResponse
54 +from app.incidents.schema.db_operations import CaseAlertUnLink
55 +from app.incidents.schema.db_operations import CaseAlertUnLinkResponse
56 from app.incidents.schema.db_operations import CaseCreate
57 from app.incidents.schema.db_operations import CaseCreateFromAlert
58 from app.incidents.schema.db_operations import CaseDataStoreResponse
@@ -81,6 +85,10 @@ from app.incidents.schema.db_operations import UpdateAlertStatus
85 from app.incidents.schema.db_operations import UpdateCaseStatus
86 from app.incidents.schema.incident_alert import CreatedAlertPayload
87 from app.incidents.schema.incident_alert import CreatedCaseNotificationPayload
88 +
89 +# from app.incidents.services.db_operations import alerts_open_multiple_filters
90 +# from app.incidents.services.db_operations import alerts_in_progress_multiple_filters
91 +# from app.incidents.services.db_operations import alerts_closed_multiple_filters
92 from app.incidents.services.db_operations import add_alert_title_name
93 from app.incidents.services.db_operations import add_asset_name
94 from app.incidents.services.db_operations import add_field_name
@@ -97,7 +105,6 @@ from app.incidents.services.db_operations import alerts_closed_by_customer_code
105 from app.incidents.services.db_operations import alerts_closed_by_ioc
106 from app.incidents.services.db_operations import alerts_closed_by_source
107 from app.incidents.services.db_operations import alerts_closed_by_tag
100 -from app.incidents.services.db_operations import alerts_closed_multiple_filters
108 from app.incidents.services.db_operations import alerts_in_progress
109 from app.incidents.services.db_operations import alerts_in_progress_by_alert_title
110 from app.incidents.services.db_operations import alerts_in_progress_by_assest_name
@@ -106,7 +113,6 @@ from app.incidents.services.db_operations import alerts_in_progress_by_customer_
113 from app.incidents.services.db_operations import alerts_in_progress_by_ioc
114 from app.incidents.services.db_operations import alerts_in_progress_by_source
115 from app.incidents.services.db_operations import alerts_in_progress_by_tag
109 -from app.incidents.services.db_operations import alerts_in_progress_multiple_filters
116 from app.incidents.services.db_operations import alerts_open
117 from app.incidents.services.db_operations import alerts_open_by_alert_title
118 from app.incidents.services.db_operations import alerts_open_by_assest_name
@@ -115,13 +121,13 @@ from app.incidents.services.db_operations import alerts_open_by_customer_code
121 from app.incidents.services.db_operations import alerts_open_by_ioc
122 from app.incidents.services.db_operations import alerts_open_by_source
123 from app.incidents.services.db_operations import alerts_open_by_tag
118 -from app.incidents.services.db_operations import alerts_open_multiple_filters
124 from app.incidents.services.db_operations import alerts_total_by_assigned_to
125 from app.incidents.services.db_operations import alerts_total_by_customer_code
126 from app.incidents.services.db_operations import alerts_total_by_ioc
127 from app.incidents.services.db_operations import alerts_total_by_source
128 from app.incidents.services.db_operations import alerts_total_by_tag
129 from app.incidents.services.db_operations import alerts_total_multiple_filters
130 +from app.incidents.services.db_operations import case_alert_unlink
131 from app.incidents.services.db_operations import create_alert
132 from app.incidents.services.db_operations import create_alert_context
133 from app.incidents.services.db_operations import create_alert_ioc
@@ -129,6 +135,7 @@ from app.incidents.services.db_operations import create_alert_tag
135 from app.incidents.services.db_operations import create_asset
136 from app.incidents.services.db_operations import create_case
137 from app.incidents.services.db_operations import create_case_alert_link
138 +from app.incidents.services.db_operations import create_case_alert_links_bulk
139 from app.incidents.services.db_operations import create_case_from_alert
140 from app.incidents.services.db_operations import create_comment
141 from app.incidents.services.db_operations import delete_alert
@@ -540,6 +547,20 @@ async def create_case_alert_link_endpoint(case_alert_link: CaseAlertLinkCreate,
547 )
548
549
550 +@incidents_db_operations_router.post("/case/alert-links", response_model=CaseAlertLinksResponse)
551 +async def create_case_alert_links_endpoint(case_alert_links: CaseAlertLinksCreate, db: AsyncSession = Depends(get_db)):
552 + return CaseAlertLinksResponse(
553 + case_alert_links=await create_case_alert_links_bulk(case_alert_links, db),
554 + success=True,
555 + message="Case alert links created successfully",
556 + )
557 +
558 +
559 +@incidents_db_operations_router.post("/case/alert-unlink", response_model=CaseAlertUnLinkResponse)
560 +async def case_alert_unlink_endpoint(case_alert_link: CaseAlertUnLink, db: AsyncSession = Depends(get_db)):
561 + return await case_alert_unlink(case_alert_link, db)
562 +
563 +
564 @incidents_db_operations_router.post("/case/from-alert", response_model=CaseAlertLinkResponse)
565 async def create_case_from_alert_endpoint(alert_id: CaseCreateFromAlert, db: AsyncSession = Depends(get_db)):
566 case = await create_case_from_alert(alert_id.alert_id, db)
@@ -792,40 +813,7 @@ async def list_alerts_multiple_filters_endpoint(
813 page_size=page_size,
814 order=order,
815 ),
795 - total=await alerts_total_multiple_filters(
796 - assigned_to=assigned_to,
797 - alert_title=alert_title,
798 - customer_code=customer_code,
799 - source=source,
800 - asset_name=asset_name,
801 - status=status,
802 - tags=tags,
803 - ioc_value=ioc_value,
804 - db=db,
805 - ),
806 - open=await alerts_open_multiple_filters(
807 - assigned_to=assigned_to,
808 - alert_title=alert_title,
809 - customer_code=customer_code,
810 - source=source,
811 - asset_name=asset_name,
812 - status=status,
813 - tags=tags,
814 - ioc_value=ioc_value,
815 - db=db,
816 - ),
817 - in_progress=await alerts_in_progress_multiple_filters(
818 - assigned_to=assigned_to,
819 - alert_title=alert_title,
820 - customer_code=customer_code,
821 - source=source,
822 - asset_name=asset_name,
823 - status=status,
824 - tags=tags,
825 - ioc_value=ioc_value,
826 - db=db,
827 - ),
828 - closed=await alerts_closed_multiple_filters(
816 + total_filtered=await alerts_total_multiple_filters(
817 assigned_to=assigned_to,
818 alert_title=alert_title,
819 customer_code=customer_code,
@@ -836,6 +824,43 @@ async def list_alerts_multiple_filters_endpoint(
824 ioc_value=ioc_value,
825 db=db,
826 ),
827 + # open=await alerts_open_multiple_filters(
828 + # assigned_to=assigned_to,
829 + # alert_title=alert_title,
830 + # customer_code=customer_code,
831 + # source=source,
832 + # asset_name=asset_name,
833 + # status=status,
834 + # tags=tags,
835 + # ioc_value=ioc_value,
836 + # db=db,
837 + # ),
838 + open=await alerts_open(db),
839 + # in_progress=await alerts_in_progress_multiple_filters(
840 + # assigned_to=assigned_to,
841 + # alert_title=alert_title,
842 + # customer_code=customer_code,
843 + # source=source,
844 + # asset_name=asset_name,
845 + # status=status,
846 + # tags=tags,
847 + # ioc_value=ioc_value,
848 + # db=db,
849 + # ),
850 + in_progress=await alerts_in_progress(db),
851 + # closed=await alerts_closed_multiple_filters(
852 + # assigned_to=assigned_to,
853 + # alert_title=alert_title,
854 + # customer_code=customer_code,
855 + # source=source,
856 + # asset_name=asset_name,
857 + # status=status,
858 + # tags=tags,
859 + # ioc_value=ioc_value,
860 + # db=db,
861 + # ),
862 + closed=await alerts_closed(db),
863 + total=await alert_total(db),
864 success=True,
865 message="Alerts retrieved successfully",
866 )
backend/app/incidents/schema/db_operations.py
+22
@@ -195,6 +195,17 @@ class CaseAlertLinkResponse(BaseModel):
195 message: str
196
197
198 +class CaseAlertUnLinkResponse(BaseModel):
199 + success: bool
200 + message: str
201 +
202 +
203 +class CaseAlertLinksResponse(BaseModel):
204 + case_alert_links: List[CaseAlertLink]
205 + success: bool
206 + message: str
207 +
208 +
209 class AvailableUsersResponse(BaseModel):
210 available_users: List[str]
211 success: bool
@@ -289,6 +300,16 @@ class CaseAlertLinkCreate(BaseModel):
300 alert_id: int
301
302
303 +class CaseAlertLinksCreate(BaseModel):
304 + case_id: int
305 + alert_ids: List[int]
306 +
307 +
308 +class CaseAlertUnLink(BaseModel):
309 + case_id: int
310 + alert_id: int
311 +
312 +
313 class AssetCreate(BaseModel):
314 alert_linked: int
315 asset_name: str
@@ -370,6 +391,7 @@ class AlertOutResponse(BaseModel):
391 open: Optional[int] = None
392 in_progress: Optional[int] = None
393 closed: Optional[int] = None
394 + total_filtered: Optional[int] = None
395 success: bool
396 message: str
397
backend/app/incidents/services/db_operations.py
+34 -1
@@ -55,6 +55,9 @@ from app.incidents.schema.db_operations import AlertTagCreate
55 from app.incidents.schema.db_operations import AssetBase
56 from app.incidents.schema.db_operations import AssetCreate
57 from app.incidents.schema.db_operations import CaseAlertLinkCreate
58 +from app.incidents.schema.db_operations import CaseAlertLinksCreate
59 +from app.incidents.schema.db_operations import CaseAlertUnLink
60 +from app.incidents.schema.db_operations import CaseAlertUnLinkResponse
61 from app.incidents.schema.db_operations import CaseCreate
62 from app.incidents.schema.db_operations import CaseOut
63 from app.incidents.schema.db_operations import CaseReportTemplateDataStoreListResponse
@@ -1119,6 +1122,34 @@ async def create_case_alert_link(case_alert_link: CaseAlertLinkCreate, db: Async
1122 return db_case_alert_link
1123
1124
1125 +async def case_alert_unlink(case_alert_unlink: CaseAlertUnLink, db: AsyncSession) -> CaseAlertUnLinkResponse:
1126 + result = await db.execute(
1127 + select(CaseAlertLink).where(
1128 + (CaseAlertLink.case_id == case_alert_unlink.case_id) & (CaseAlertLink.alert_id == case_alert_unlink.alert_id),
1129 + ),
1130 + )
1131 + case_alert_link = result.scalars().first()
1132 + if not case_alert_link:
1133 + raise HTTPException(status_code=404, detail="Case alert link not found")
1134 + await db.execute(
1135 + delete(CaseAlertLink).where(
1136 + (CaseAlertLink.case_id == case_alert_unlink.case_id) & (CaseAlertLink.alert_id == case_alert_unlink.alert_id),
1137 + ),
1138 + )
1139 + await db.commit()
1140 + return CaseAlertUnLinkResponse(success=True, message="Case alert link deleted successfully")
1141 +
1142 +
1143 +async def create_case_alert_links_bulk(case_alert_links: CaseAlertLinksCreate, db: AsyncSession) -> List[CaseAlertLink]:
1144 + db_case_alert_links = [CaseAlertLink(case_id=case_alert_links.case_id, alert_id=alert_id) for alert_id in case_alert_links.alert_ids]
1145 + db.add_all(db_case_alert_links)
1146 + try:
1147 + await db.commit()
1148 + except IntegrityError:
1149 + raise HTTPException(status_code=400, detail="Case alert links already exist")
1150 + return db_case_alert_links
1151 +
1152 +
1153 async def get_case_by_id(case_id: int, db: AsyncSession) -> CaseOut:
1154 result = await db.execute(
1155 select(Case)
@@ -1850,7 +1881,7 @@ async def list_alerts_multiple_filters(
1881 .options(
1882 selectinload(Alert.comments),
1883 selectinload(Alert.assets),
1853 - selectinload(Alert.cases),
1884 + selectinload(Alert.cases).selectinload(CaseAlertLink.case),
1885 selectinload(Alert.tags).selectinload(AlertToTag.tag),
1886 selectinload(Alert.iocs).selectinload(AlertToIoC.ioc),
1887 )
@@ -1868,6 +1899,7 @@ async def list_alerts_multiple_filters(
1899 assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
1900 tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
1901 iocs = [IoCBase(**alert_to_ioc.ioc.__dict__) for alert_to_ioc in alert.iocs]
1902 + linked_cases = [LinkedCaseCreate(**case_alert_link.case.__dict__) for case_alert_link in alert.cases]
1903 alert_out = AlertOut(
1904 id=alert.id,
1905 alert_creation_time=alert.alert_creation_time,
@@ -1882,6 +1914,7 @@ async def list_alerts_multiple_filters(
1914 assets=assets,
1915 tags=tags,
1916 iocs=iocs,
1917 + linked_cases=linked_cases,
1918 )
1919 alerts_out.append(alert_out)
1920
frontend/package-lock.json
+148 -102
@@ -13,7 +13,7 @@
13 "@fontsource/jetbrains-mono": "^5.1.1",
14 "@fontsource/lexend": "^5.1.1",
15 "@fontsource/public-sans": "^5.1.1",
16 - "@shikijs/markdown-it": "^1.24.2",
16 + "@shikijs/markdown-it": "^1.24.3",
17 "@tailwindcss/container-queries": "^0.1.1",
18 "@vueuse/core": "^12.0.0",
19 "axios": "^1.7.9",
@@ -32,9 +32,9 @@
32 "nanoid": "^5.0.9",
33 "password-validator": "^5.3.0",
34 "pinia": "^2.3.0",
35 - "pinia-plugin-persistedstate": "^4.1.3",
35 + "pinia-plugin-persistedstate": "^4.2.0",
36 "secure-ls": "^2.0.0",
37 - "shiki": "^1.24.2",
37 + "shiki": "^1.24.3",
38 "validator": "^13.12.0",
39 "vue": "^3.5.13",
40 "vue-advanced-cropper": "^2.8.9",
@@ -63,24 +63,24 @@
63 "@vue/test-utils": "^2.4.6",
64 "@vue/tsconfig": "^0.7.0",
65 "autoprefixer": "^10.4.20",
66 - "cypress": "^13.16.1",
66 + "cypress": "^13.17.0",
67 "depcheck": "^1.4.7",
68 - "eslint": "^9.16.0",
68 + "eslint": "^9.17.0",
69 "flourite": "^1.3.0",
70 "fs-extra": "^11.2.0",
71 "jsdom": "^25.0.1",
72 - "npm-run-all2": "^7.0.1",
72 + "npm-run-all2": "^7.0.2",
73 "postcss": "^8.4.49",
74 "prettier": "^3.4.2",
75 "prettier-plugin-tailwindcss": "^0.6.9",
76 "sass": "^1.83.0",
77 "start-server-and-test": "^2.0.9",
78 "tailwind-config-viewer": "^2.0.4",
79 - "tailwindcss": "^3.4.16",
79 + "tailwindcss": "^3.4.17",
80 "taze": "^0.18.0",
81 - "type-fest": "^4.30.1",
81 + "type-fest": "^4.30.2",
82 "typescript": "~5.6.3",
83 - "unplugin-vue-components": "^0.27.5",
83 + "unplugin-vue-components": "^0.28.0",
84 "vite": "^5.4.11",
85 "vite-bundle-visualizer": "^1.2.1",
86 "vite-plugin-vue-devtools": "^7.6.8",
@@ -1512,9 +1512,9 @@
1512 }
1513 },
1514 "node_modules/@eslint/js": {
1515 - "version": "9.16.0",
1516 - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz",
1517 - "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==",
1515 + "version": "9.17.0",
1516 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
1517 + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
1518 "dev": true,
1519 "license": "MIT",
1520 "engines": {
@@ -2325,9 +2325,9 @@
2325 "license": "MIT"
2326 },
2327 "node_modules/@rollup/pluginutils": {
2328 - "version": "5.1.3",
2329 - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz",
2330 - "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==",
2328 + "version": "5.1.4",
2329 + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
2330 + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
2331 "license": "MIT",
2332 "dependencies": {
2333 "@types/estree": "^1.0.0",
@@ -2605,64 +2605,64 @@
2605 "license": "MIT"
2606 },
2607 "node_modules/@shikijs/core": {
2608 - "version": "1.24.2",
2609 - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.24.2.tgz",
2610 - "integrity": "sha512-BpbNUSKIwbKrRRA+BQj0BEWSw+8kOPKDJevWeSE/xIqGX7K0xrCZQ9kK0nnEQyrzsUoka1l81ZtJ2mGaCA32HQ==",
2608 + "version": "1.24.3",
2609 + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.24.3.tgz",
2610 + "integrity": "sha512-VRcf4GYUIkxIchGM9DrapRcxtgojg4IWKUtX5EtW+4PJiGzF2xQqZSv27PJt+WLc18KT3CNLpNWow9JYV5n+Rg==",
2611 "license": "MIT",
2612 "dependencies": {
2613 - "@shikijs/engine-javascript": "1.24.2",
2614 - "@shikijs/engine-oniguruma": "1.24.2",
2615 - "@shikijs/types": "1.24.2",
2616 - "@shikijs/vscode-textmate": "^9.3.0",
2613 + "@shikijs/engine-javascript": "1.24.3",
2614 + "@shikijs/engine-oniguruma": "1.24.3",
2615 + "@shikijs/types": "1.24.3",
2616 + "@shikijs/vscode-textmate": "^9.3.1",
2617 "@types/hast": "^3.0.4",
2618 - "hast-util-to-html": "^9.0.3"
2618 + "hast-util-to-html": "^9.0.4"
2619 }
2620 },
2621 "node_modules/@shikijs/engine-javascript": {
2622 - "version": "1.24.2",
2623 - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.24.2.tgz",
2624 - "integrity": "sha512-EqsmYBJdLEwEiO4H+oExz34a5GhhnVp+jH9Q/XjPjmBPc6TE/x4/gD0X3i0EbkKKNqXYHHJTJUpOLRQNkEzS9Q==",
2622 + "version": "1.24.3",
2623 + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.24.3.tgz",
2624 + "integrity": "sha512-De8tNLvYjeK6V0Gb47jIH2M+OKkw+lWnSV1j3HVDFMlNIglmVcTMG2fASc29W0zuFbfEEwKjO8Fe4KYSO6Ce3w==",
2625 "license": "MIT",
2626 "dependencies": {
2627 - "@shikijs/types": "1.24.2",
2628 - "@shikijs/vscode-textmate": "^9.3.0",
2629 - "oniguruma-to-es": "0.7.0"
2627 + "@shikijs/types": "1.24.3",
2628 + "@shikijs/vscode-textmate": "^9.3.1",
2629 + "oniguruma-to-es": "0.8.0"
2630 }
2631 },
2632 "node_modules/@shikijs/engine-oniguruma": {
2633 - "version": "1.24.2",
2634 - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.2.tgz",
2635 - "integrity": "sha512-ZN6k//aDNWRJs1uKB12pturKHh7GejKugowOFGAuG7TxDRLod1Bd5JhpOikOiFqPmKjKEPtEA6mRCf7q3ulDyQ==",
2633 + "version": "1.24.3",
2634 + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.3.tgz",
2635 + "integrity": "sha512-iNnx950gs/5Nk+zrp1LuF+S+L7SKEhn8k9eXgFYPGhVshKppsYwRmW8tpmAMvILIMSDfrgqZ0w+3xWVQB//1Xw==",
2636 "license": "MIT",
2637 "dependencies": {
2638 - "@shikijs/types": "1.24.2",
2639 - "@shikijs/vscode-textmate": "^9.3.0"
2638 + "@shikijs/types": "1.24.3",
2639 + "@shikijs/vscode-textmate": "^9.3.1"
2640 }
2641 },
2642 "node_modules/@shikijs/markdown-it": {
2643 - "version": "1.24.2",
2644 - "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.24.2.tgz",
2645 - "integrity": "sha512-vLFRZYudSkrWWrtfBBZy7hM5mZjpC54zdxSNDn25nV6uVSilySmbdt70LyfiuTOtrKQ3p7fjuxojxqM/n6qVCg==",
2643 + "version": "1.24.3",
2644 + "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.24.3.tgz",
2645 + "integrity": "sha512-9Eeo6bcoXntHS0Mi6Vw6v8WAzRDT2JZTJXsH0rAo+3zDYdywUjwdsEl+YskOJL925ARikAMn1q5De8IC6nEwVg==",
2646 "license": "MIT",
2647 "dependencies": {
2648 "markdown-it": "^14.1.0",
2649 - "shiki": "1.24.2"
2649 + "shiki": "1.24.3"
2650 }
2651 },
2652 "node_modules/@shikijs/types": {
2653 - "version": "1.24.2",
2654 - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.24.2.tgz",
2655 - "integrity": "sha512-bdeWZiDtajGLG9BudI0AHet0b6e7FbR0EsE4jpGaI0YwHm/XJunI9+3uZnzFtX65gsyJ6ngCIWUfA4NWRPnBkQ==",
2653 + "version": "1.24.3",
2654 + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.24.3.tgz",
2655 + "integrity": "sha512-FPMrJ69MNxhRtldRk69CghvaGlbbN3pKRuvko0zvbfa2dXp4pAngByToqS5OY5jvN8D7LKR4RJE8UvzlCOuViw==",
2656 "license": "MIT",
2657 "dependencies": {
2658 - "@shikijs/vscode-textmate": "^9.3.0",
2658 + "@shikijs/vscode-textmate": "^9.3.1",
2659 "@types/hast": "^3.0.4"
2660 }
2661 },
2662 "node_modules/@shikijs/vscode-textmate": {
2663 - "version": "9.3.0",
2664 - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz",
2665 - "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==",
2663 + "version": "9.3.1",
2664 + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.1.tgz",
2665 + "integrity": "sha512-79QfK1393x9Ho60QFyLti+QfdJzRQCVLFb97kOIV7Eo9vQU/roINgk7m24uv0a7AUvN//RDH36FLjjK48v0s9g==",
2666 "license": "MIT"
2667 },
2668 "node_modules/@sideway/address": {
@@ -3118,14 +3118,14 @@
3118 }
3119 },
3120 "node_modules/@typescript-eslint/typescript-estree": {
3121 - "version": "8.18.0",
3122 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz",
3123 - "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==",
3121 + "version": "8.18.1",
3122 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz",
3123 + "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==",
3124 "dev": true,
3125 "license": "MIT",
3126 "dependencies": {
3127 - "@typescript-eslint/types": "8.18.0",
3128 - "@typescript-eslint/visitor-keys": "8.18.0",
3127 + "@typescript-eslint/types": "8.18.1",
3128 + "@typescript-eslint/visitor-keys": "8.18.1",
3129 "debug": "^4.3.4",
3130 "fast-glob": "^3.3.2",
3131 "is-glob": "^4.0.3",
@@ -3144,6 +3144,38 @@
3144 "typescript": ">=4.8.4 <5.8.0"
3145 }
3146 },
3147 + "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/types": {
3148 + "version": "8.18.1",
3149 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz",
3150 + "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==",
3151 + "dev": true,
3152 + "license": "MIT",
3153 + "engines": {
3154 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3155 + },
3156 + "funding": {
3157 + "type": "opencollective",
3158 + "url": "https://opencollective.com/typescript-eslint"
3159 + }
3160 + },
3161 + "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": {
3162 + "version": "8.18.1",
3163 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz",
3164 + "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==",
3165 + "dev": true,
3166 + "license": "MIT",
3167 + "dependencies": {
3168 + "@typescript-eslint/types": "8.18.1",
3169 + "eslint-visitor-keys": "^4.2.0"
3170 + },
3171 + "engines": {
3172 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3173 + },
3174 + "funding": {
3175 + "type": "opencollective",
3176 + "url": "https://opencollective.com/typescript-eslint"
3177 + }
3178 + },
3179 "node_modules/@typescript-eslint/utils": {
3180 "version": "8.18.0",
3181 "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz",
@@ -5171,9 +5203,9 @@
5203 "license": "MIT"
5204 },
5205 "node_modules/cypress": {
5174 - "version": "13.16.1",
5175 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.16.1.tgz",
5176 - "integrity": "sha512-17FtCaz0cx7ssWYKXzGB0Vub8xHwpVPr+iPt2fHhLMDhVAPVrplD+rTQsZUsfb19LVBn5iwkEUFjQ1yVVJXsLA==",
5206 + "version": "13.17.0",
5207 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz",
5208 + "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==",
5209 "dev": true,
5210 "hasInstallScript": true,
5211 "license": "MIT",
@@ -6050,9 +6082,9 @@
6082 }
6083 },
6084 "node_modules/eslint": {
6053 - "version": "9.16.0",
6054 - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz",
6055 - "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==",
6085 + "version": "9.17.0",
6086 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz",
6087 + "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==",
6088 "dev": true,
6089 "license": "MIT",
6090 "dependencies": {
@@ -6061,7 +6093,7 @@
6093 "@eslint/config-array": "^0.19.0",
6094 "@eslint/core": "^0.9.0",
6095 "@eslint/eslintrc": "^3.2.0",
6064 - "@eslint/js": "9.16.0",
6096 + "@eslint/js": "9.17.0",
6097 "@eslint/plugin-kit": "^0.2.3",
6098 "@humanfs/node": "^0.16.6",
6099 "@humanwhocodes/module-importer": "^1.0.1",
@@ -6070,7 +6102,7 @@
6102 "@types/json-schema": "^7.0.15",
6103 "ajv": "^6.12.4",
6104 "chalk": "^4.0.0",
6073 - "cross-spawn": "^7.0.5",
6105 + "cross-spawn": "^7.0.6",
6106 "debug": "^4.3.2",
6107 "escape-string-regexp": "^4.0.0",
6108 "eslint-scope": "^8.2.0",
@@ -7688,9 +7720,9 @@
7720 }
7721 },
7722 "node_modules/hast-util-to-html": {
7691 - "version": "9.0.3",
7692 - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz",
7693 - "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==",
7723 + "version": "9.0.4",
7724 + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.4.tgz",
7725 + "integrity": "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==",
7726 "license": "MIT",
7727 "dependencies": {
7728 "@types/hast": "^3.0.0",
@@ -9204,9 +9236,9 @@
9236 }
9237 },
9238 "node_modules/magic-string": {
9207 - "version": "0.30.14",
9208 - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz",
9209 - "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==",
9239 + "version": "0.30.17",
9240 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
9241 + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
9242 "license": "MIT",
9243 "dependencies": {
9244 "@jridgewell/sourcemap-codec": "^1.5.0"
@@ -10542,14 +10574,14 @@
10574 }
10575 },
10576 "node_modules/npm-run-all2": {
10545 - "version": "7.0.1",
10546 - "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-7.0.1.tgz",
10547 - "integrity": "sha512-Adbv+bJQ8UTAM03rRODqrO5cx0YU5KCG2CvHtSURiadvdTjjgGJXdbc1oQ9CXBh9dnGfHSoSB1Web/0Dzp6kOQ==",
10577 + "version": "7.0.2",
10578 + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-7.0.2.tgz",
10579 + "integrity": "sha512-7tXR+r9hzRNOPNTvXegM+QzCuMjzUIIq66VDunL6j60O4RrExx32XUhlrS7UK4VcdGw5/Wxzb3kfNcFix9JKDA==",
10580 "dev": true,
10581 "license": "MIT",
10582 "dependencies": {
10583 "ansi-styles": "^6.2.1",
10552 - "cross-spawn": "^7.0.3",
10584 + "cross-spawn": "^7.0.6",
10585 "memorystream": "^0.3.1",
10586 "minimatch": "^9.0.0",
10587 "pidtree": "^0.6.0",
@@ -10883,14 +10915,14 @@
10915 }
10916 },
10917 "node_modules/oniguruma-to-es": {
10886 - "version": "0.7.0",
10887 - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.7.0.tgz",
10888 - "integrity": "sha512-HRaRh09cE0gRS3+wi2zxekB+I5L8C/gN60S+vb11eADHUaB/q4u8wGGOX3GvwvitG8ixaeycZfeoyruKQzUgNg==",
10918 + "version": "0.8.0",
10919 + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.8.0.tgz",
10920 + "integrity": "sha512-rY+/a6b+uCgoYIL9itjY0x99UUDHXmGaw7Jjk5ZvM/3cxDJifyxFr/Zm4tTmF6Tre18gAakJo7AzhKUeMNLgHA==",
10921 "license": "MIT",
10922 "dependencies": {
10923 "emoji-regex-xs": "^1.0.0",
10924 "regex": "^5.0.2",
10893 - "regex-recursion": "^4.3.0"
10925 + "regex-recursion": "^5.0.0"
10926 }
10927 },
10928 "node_modules/only": {
@@ -11316,19 +11348,19 @@
11348 }
11349 },
11350 "node_modules/pinia-plugin-persistedstate": {
11319 - "version": "4.1.3",
11320 - "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.1.3.tgz",
11321 - "integrity": "sha512-5Rad7oSoEh0na+j4jEViVQMtAYE38KjU7ixKc+am33QX/clJ1mb19zTZqMJXvq+PnQW7uuEuL33Q6NL0GbkfWw==",
11351 + "version": "4.2.0",
11352 + "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.2.0.tgz",
11353 + "integrity": "sha512-3buhA7ac+ssbOIx3VRCC8oHkoFwhDM9oHRCjo7nj+O8WUqnW+jRqh7eYT5eS/DNa3H28zp3dYf/nd/Vc8zj8eQ==",
11354 "license": "MIT",
11355 "dependencies": {
11324 - "@nuxt/kit": "^3.13.2",
11356 + "@nuxt/kit": "^3.14.1592",
11357 "deep-pick-omit": "^1.2.1",
11358 "defu": "^6.1.4",
11359 "destr": "^2.0.3"
11360 },
11361 "peerDependencies": {
11330 - "@pinia/nuxt": ">=0.5.0",
11331 - "pinia": ">=2.0.0"
11362 + "@pinia/nuxt": ">=0.9.0",
11363 + "pinia": ">=2.3.0"
11364 },
11365 "peerDependenciesMeta": {
11366 "@pinia/nuxt": {
@@ -12062,9 +12094,9 @@
12094 }
12095 },
12096 "node_modules/regex-recursion": {
12065 - "version": "4.3.0",
12066 - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-4.3.0.tgz",
12067 - "integrity": "sha512-5LcLnizwjcQ2ALfOj95MjcatxyqF5RPySx9yT+PaXu3Gox2vyAtLDjHB8NTJLtMGkvyau6nI3CfpwFCjPUIs/A==",
12097 + "version": "5.0.0",
12098 + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.0.0.tgz",
12099 + "integrity": "sha512-UwyOqeobrCCqTXPcsSqH4gDhOjD5cI/b8kjngWgSZbxYh5yVjAwTjO5+hAuPRNiuR70+5RlWSs+U9PVcVcW9Lw==",
12100 "license": "MIT",
12101 "dependencies": {
12102 "regex-utilities": "^2.3.0"
@@ -12700,16 +12732,16 @@
12732 }
12733 },
12734 "node_modules/shiki": {
12703 - "version": "1.24.2",
12704 - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.24.2.tgz",
12705 - "integrity": "sha512-TR1fi6mkRrzW+SKT5G6uKuc32Dj2EEa7Kj0k8kGqiBINb+C1TiflVOiT9ta6GqOJtC4fraxO5SLUaKBcSY38Fg==",
12735 + "version": "1.24.3",
12736 + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.24.3.tgz",
12737 + "integrity": "sha512-eMeX/ehE2IDKVs71kB4zVcDHjutNcOtm+yIRuR4sA6ThBbdFI0DffGJiyoKCodj0xRGxIoWC3pk/Anmm5mzHmA==",
12738 "license": "MIT",
12739 "dependencies": {
12708 - "@shikijs/core": "1.24.2",
12709 - "@shikijs/engine-javascript": "1.24.2",
12710 - "@shikijs/engine-oniguruma": "1.24.2",
12711 - "@shikijs/types": "1.24.2",
12712 - "@shikijs/vscode-textmate": "^9.3.0",
12740 + "@shikijs/core": "1.24.3",
12741 + "@shikijs/engine-javascript": "1.24.3",
12742 + "@shikijs/engine-oniguruma": "1.24.3",
12743 + "@shikijs/types": "1.24.3",
12744 + "@shikijs/vscode-textmate": "^9.3.1",
12745 "@types/hast": "^3.0.4"
12746 }
12747 },
@@ -13363,9 +13395,9 @@
13395 }
13396 },
13397 "node_modules/tailwindcss": {
13366 - "version": "3.4.16",
13367 - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz",
13368 - "integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==",
13398 + "version": "3.4.17",
13399 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
13400 + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
13401 "license": "MIT",
13402 "dependencies": {
13403 "@alloc/quick-lru": "^5.2.0",
@@ -13854,9 +13886,9 @@
13886 }
13887 },
13888 "node_modules/type-fest": {
13857 - "version": "4.30.1",
13858 - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.1.tgz",
13859 - "integrity": "sha512-ojFL7eDMX2NF0xMbDwPZJ8sb7ckqtlAi1GsmgsFXvErT9kFTk1r0DuQKvrCh73M6D4nngeHJmvogF9OluXs7Hw==",
13889 + "version": "4.30.2",
13890 + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.2.tgz",
13891 + "integrity": "sha512-UJShLPYi1aWqCdq9HycOL/gwsuqda1OISdBO3t8RlXQC4QvtuIz4b5FCfe2dQIWEpmlRExKmcTBfP1r9bhY7ig==",
13892 "dev": true,
13893 "license": "(MIT OR CC0-1.0)",
13894 "engines": {
@@ -14101,22 +14133,22 @@
14133 }
14134 },
14135 "node_modules/unplugin-vue-components": {
14104 - "version": "0.27.5",
14105 - "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.27.5.tgz",
14106 - "integrity": "sha512-m9j4goBeNwXyNN8oZHHxvIIYiG8FQ9UfmKWeNllpDvhU7btKNNELGPt+o3mckQKuPwrE7e0PvCsx+IWuDSD9Vg==",
14136 + "version": "0.28.0",
14137 + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.28.0.tgz",
14138 + "integrity": "sha512-jiTGtJ3JsRFBjgvyilfrX7yUoGKScFgbdNw+6p6kEXU+Spf/rhxzgvdfuMcvhCcLmflB/dY3pGQshYBVGOUx7Q==",
14139 "dev": true,
14140 "license": "MIT",
14141 "dependencies": {
14142 "@antfu/utils": "^0.7.10",
14111 - "@rollup/pluginutils": "^5.1.3",
14143 + "@rollup/pluginutils": "^5.1.4",
14144 "chokidar": "^3.6.0",
14113 - "debug": "^4.3.7",
14145 + "debug": "^4.4.0",
14146 "fast-glob": "^3.3.2",
14147 "local-pkg": "^0.5.1",
14116 - "magic-string": "^0.30.14",
14148 + "magic-string": "^0.30.15",
14149 "minimatch": "^9.0.5",
14150 "mlly": "^1.7.3",
14119 - "unplugin": "^1.16.0"
14151 + "unplugin": "^2.1.0"
14152 },
14153 "engines": {
14154 "node": ">=14"
@@ -14176,6 +14208,20 @@
14208 "node": ">= 6"
14209 }
14210 },
14211 + "node_modules/unplugin-vue-components/node_modules/unplugin": {
14212 + "version": "2.1.0",
14213 + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.1.0.tgz",
14214 + "integrity": "sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==",
14215 + "dev": true,
14216 + "license": "MIT",
14217 + "dependencies": {
14218 + "acorn": "^8.14.0",
14219 + "webpack-virtual-modules": "^0.6.2"
14220 + },
14221 + "engines": {
14222 + "node": ">=18.12.0"
14223 + }
14224 + },
14225 "node_modules/untildify": {
14226 "version": "4.0.0",
14227 "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
frontend/package.json
+14 -14
@@ -41,7 +41,7 @@
41 "@fontsource/jetbrains-mono": "^5.1.1",
42 "@fontsource/lexend": "^5.1.1",
43 "@fontsource/public-sans": "^5.1.1",
44 - "@shikijs/markdown-it": "^1.24.2",
44 + "@shikijs/markdown-it": "^1.24.3",
45 "@tailwindcss/container-queries": "^0.1.1",
46 "@vueuse/core": "^12.0.0",
47 "axios": "^1.7.9",
@@ -60,9 +60,9 @@
60 "nanoid": "^5.0.9",
61 "password-validator": "^5.3.0",
62 "pinia": "^2.3.0",
63 - "pinia-plugin-persistedstate": "^4.1.3",
63 + "pinia-plugin-persistedstate": "^4.2.0",
64 "secure-ls": "^2.0.0",
65 - "shiki": "^1.24.2",
65 + "shiki": "^1.24.3",
66 "validator": "^13.12.0",
67 "vue": "^3.5.13",
68 "vue-advanced-cropper": "^2.8.9",
@@ -94,24 +94,24 @@
94 "@vue/test-utils": "^2.4.6",
95 "@vue/tsconfig": "^0.7.0",
96 "autoprefixer": "^10.4.20",
97 - "cypress": "^13.16.1",
97 + "cypress": "^13.17.0",
98 "depcheck": "^1.4.7",
99 - "eslint": "^9.16.0",
99 + "eslint": "^9.17.0",
100 "flourite": "^1.3.0",
101 "fs-extra": "^11.2.0",
102 "jsdom": "^25.0.1",
103 - "npm-run-all2": "^7.0.1",
103 + "npm-run-all2": "^7.0.2",
104 "postcss": "^8.4.49",
105 "prettier": "^3.4.2",
106 "prettier-plugin-tailwindcss": "^0.6.9",
107 "sass": "^1.83.0",
108 "start-server-and-test": "^2.0.9",
109 "tailwind-config-viewer": "^2.0.4",
110 - "tailwindcss": "^3.4.16",
110 + "tailwindcss": "^3.4.17",
111 "taze": "^0.18.0",
112 - "type-fest": "^4.30.1",
112 + "type-fest": "^4.30.2",
113 "typescript": "~5.6.3",
114 - "unplugin-vue-components": "^0.27.5",
114 + "unplugin-vue-components": "^0.28.0",
115 "vite": "^5.4.11",
116 "vite-bundle-visualizer": "^1.2.1",
117 "vite-plugin-vue-devtools": "^7.6.8",
@@ -121,20 +121,20 @@
121 },
122 "pnpm": {
123 "overrides": {
124 - "@typescript-eslint/eslint-plugin": "^8.18.0",
124 + "@typescript-eslint/eslint-plugin": "^8.18.1",
125 "@typescript-eslint/eslint-plugin>eslint": "$eslint",
126 - "@typescript-eslint/parser": "^8.18.0",
126 + "@typescript-eslint/parser": "^8.18.1",
127 "@typescript-eslint/parser>eslint": "$eslint",
128 "eslint": "$eslint"
129 }
130 },
131 "overrides": {
132 "@typescript-eslint/eslint-plugin": {
133 - "eslint": "^9.16.0"
133 + "eslint": "^9.17.0"
134 },
135 "@typescript-eslint/parser": {
136 - "eslint": "^9.16.0"
136 + "eslint": "^9.17.0"
137 },
138 - "@typescript-eslint/typescript-estree": "^8.18.0"
138 + "@typescript-eslint/typescript-estree": "^8.18.1"
139 }
140 }
frontend/src/api/endpoints/incidentManagement.ts
+24 -1
@@ -181,7 +181,14 @@ export default {
181 }
182
183 return HttpClient.get<
184 - FlaskBaseResponse & { alerts: Alert[]; closed: number; in_progress: number; open: number; total: number }
184 + FlaskBaseResponse & {
185 + alerts: Alert[]
186 + closed: number
187 + in_progress: number
188 + open: number
189 + total: number
190 + total_filtered: number
191 + }
192 >(url, {
193 params,
194 paramsSerializer: {
@@ -313,6 +320,7 @@ export default {
320 }
321 )
322 },
323 + /** @deprecated in favor of multiLinkCase */
324 linkCase(alertId: number, caseId: number) {
325 return HttpClient.post<FlaskBaseResponse & { case_alert_link: { case_id: number; alert_id: number } }>(
326 `/incidents/db_operations/case/alert-link`,
@@ -322,6 +330,21 @@ export default {
330 }
331 )
332 },
333 + multiLinkCase(alertIds: number[], caseId: number) {
334 + return HttpClient.post<FlaskBaseResponse & { case_alert_links: { case_id: number; alert_id: number }[] }>(
335 + `/incidents/db_operations/case/alert-links`,
336 + {
337 + alert_ids: alertIds,
338 + case_id: caseId
339 + }
340 + )
341 + },
342 + unlinkCase(alertId: number, caseId: number) {
343 + return HttpClient.post<FlaskBaseResponse>(`/incidents/db_operations/case/alert-unlink`, {
344 + alert_id: alertId,
345 + case_id: caseId
346 + })
347 + },
348 updateCaseStatus(caseId: number, status: CaseStatus) {
349 return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/case/status`, {
350 case_id: caseId,
frontend/src/app-layouts/HorizontalNav/Sidebar.vue
+1 -1
@@ -7,7 +7,7 @@
7 <div ref="sidebar" class="sidebar-wrap flex grow flex-col">
8 <SidebarHeader :logo-mini="sidebarClosed" />
9 <n-scrollbar>
10 - <Navbar :collapsed="sidebarClosed" />
10 + <Navbar :collapsed="sidebarClosed" mode="vertical" />
11 </n-scrollbar>
12 <SidebarFooter :collapsed="sidebarClosed" />
13 </div>
frontend/src/app-layouts/common/Navbar/index.vue
+1 -1
@@ -5,7 +5,7 @@
5 v-model:value="selectedKey"
6 :options="menuOptions"
7 :collapsed="collapsed"
8 - :mode="mode"
8 + :mode
9 :accordion="true"
10 :collapsed-width="collapsedWidth"
11 :dropdown-props="{
frontend/src/components/alerts/Alert.vue
+4 -4
@@ -17,7 +17,7 @@
17 <div class="flex flex-wrap items-center gap-3">
18 <Badge type="splitted" color="primary">
19 <template #iconLeft>
20 - <Icon :name="TargetIcon" :size="13" class="!opacity-80"></Icon>
20 + <Icon :name="TargetIcon" :size="13" class="!opacity-80" />
21 </template>
22 <template #label>Fired times</template>
23 <template #value>
@@ -27,7 +27,7 @@
27
28 <Badge :type="alert._source.rule_mail ? 'active' : 'muted'">
29 <template #iconRight>
30 - <Icon :name="alert._source.rule_mail ? MailIcon : DisabledIcon" :size="14"></Icon>
30 + <Icon :name="alert._source.rule_mail ? MailIcon : DisabledIcon" :size="14" />
31 </template>
32 <template #label>Rule mail</template>
33 </Badge>
@@ -36,13 +36,13 @@
36 <template #trigger>
37 <Badge type="splitted" color="primary" hint-cursor>
38 <template #iconLeft>
39 - <Icon :name="AgentIcon" :size="13" class="!opacity-80"></Icon>
39 + <Icon :name="AgentIcon" :size="13" class="!opacity-80" />
40 </template>
41 <template #label>Agent</template>
42 <template #value>
43 <div class="flex flex-wrap items-center gap-2">
44 {{ alert._source.agent_name }} / {{ alert._source.agent_labels_customer }}
45 - <Icon :name="InfoIcon" :size="13" class="!opacity-80"></Icon>
45 + <Icon :name="InfoIcon" :size="13" class="!opacity-80" />
46 </div>
47 </template>
48 </Badge>
frontend/src/components/customers/CustomerCreationButton.vue
-1
@@ -43,7 +43,6 @@
43 </template>
44
45 <script setup lang="ts">
46 -import type { LicenseFeatures } from "@/types/license.d"
46 import type { Size } from "naive-ui/es/button/src/interface"
47 import Icon from "@/components/common/Icon.vue"
48 import LicenseFeatureCheck from "@/components/license/LicenseFeatureCheck.vue"
frontend/src/components/incidentManagement/alerts/AlertItem.vue
+25 -10
@@ -24,6 +24,7 @@
24 </div>
25 </div>
26 </template>
27 +
28 <template v-if="alert?.alert_creation_time" #headerExtra>
29 <div :class="{ 'hidden sm:block': !compact }">
30 <n-popover
@@ -168,6 +169,14 @@
169 </div>
170 </template>
171 </Badge>
172 +
173 + <Badge type="splitted" class="!hidden sm:!flex">
174 + <template #label>Linked Cases</template>
175 + <template #value>
176 + <AlertLinkedCases v-if="alert.linked_cases?.length" :alert @updated="updateAlert($event)" />
177 + <span v-else>n/d</span>
178 + </template>
179 + </Badge>
180 </div>
181 </template>
182
@@ -234,6 +243,7 @@
243 </span>
244 </div>
245 </template>
246 +
247 <template v-if="alert && !compact" #footerExtra>
248 <n-button quaternary size="tiny" @click.stop="handleDelete()">Delete</n-button>
249 </template>
@@ -271,7 +281,7 @@ import { formatDate } from "@/utils"
281 import _clone from "lodash/cloneDeep"
282 import _truncate from "lodash/truncate"
283 import { NButton, NCard, NCheckbox, NModal, NPopover, NSpin, NTooltip, useDialog, useMessage } from "naive-ui"
274 -import { computed, defineAsyncComponent, onBeforeMount, onMounted, ref, toRefs } from "vue"
284 +import { computed, defineAsyncComponent, onMounted, ref, toRefs, watch } from "vue"
285 import AssigneeIcon from "../common/AssigneeIcon.vue"
286 import StatusIcon from "../common/StatusIcon.vue"
287 import AlertAssignUser from "./AlertAssignUser.vue"
@@ -299,6 +309,7 @@ const emit = defineEmits<{
309 }>()
310
311 const AlertAssetItem = defineAsyncComponent(() => import("./AlertAsset.vue"))
312 +const AlertLinkedCases = defineAsyncComponent(() => import("./AlertLinkedCases.vue"))
313
314 const { alertData, alertId, compact, embedded, detailsOnMounted, highlight, selectable, checked } = toRefs(props)
315
@@ -377,15 +388,19 @@ function closeDetails() {
388 showDetails.value = false
389 }
390
380 -onBeforeMount(() => {
381 - if (alertId.value) {
382 - getAlert(alertId.value)
383 - } else if (alertData.value?.id && alertData.value?.linked_cases === undefined) {
384 - getAlert(alertData.value.id)
385 - } else if (alertData.value) {
386 - alert.value = _clone(alertData.value)
387 - }
388 -})
391 +watch(
392 + [alertId, alertData],
393 + () => {
394 + if (alertId.value) {
395 + getAlert(alertId.value)
396 + } else if (alertData.value?.id && alertData.value?.linked_cases === undefined) {
397 + getAlert(alertData.value.id)
398 + } else if (alertData.value) {
399 + alert.value = _clone(alertData.value)
400 + }
401 + },
402 + { immediate: true }
403 +)
404
405 onMounted(() => {
406 if (detailsOnMounted.value) {
frontend/src/components/incidentManagement/alerts/AlertLinkedCases.vue
+88 -13
@@ -1,26 +1,101 @@
1 <template>
2 - <code
3 - v-for="linkedCase of linkedCases"
4 - :key="linkedCase.id"
5 - class="text-primary cursor-pointer"
6 - @click="gotoIncidentManagementCases(linkedCase.id)"
7 - >
8 - #{{ linkedCase.id }}
9 - <Icon :name="LinkIcon" :size="14" class="relative top-0.5" />
10 - </code>
2 + <n-popover v-for="linkedCase of linkedCases" :key="linkedCase.id" placement="top" to="body" trigger="click">
3 + <template #trigger>
4 + <code class="text-primary cursor-pointer">
5 + #{{ linkedCase.id }}
6 + <Icon :name="MenuIcon" :size="14" class="relative top-0.5" />
7 + </code>
8 + </template>
9 + <div class="flex flex-col gap-3 px-1 py-2">
10 + <CaseItem
11 + :case-data="{ ...linkedCase, alerts: alert ? [alert] : [] }"
12 + compact
13 + embedded
14 + @click="gotoIncidentManagementCases(linkedCase.id)"
15 + />
16 + <div class="flex items-center justify-end gap-3">
17 + <n-button
18 + size="small"
19 + secondary
20 + type="warning"
21 + :loading="loadingId === linkedCase.id"
22 + @click="unlink(linkedCase.id)"
23 + >
24 + <template #icon>
25 + <Icon :name="UnlinkIcon" />
26 + </template>
27 + Unlink Case
28 + </n-button>
29 + </div>
30 + </div>
31 + </n-popover>
32 </template>
33
34 <script setup lang="ts">
35 import type { Alert } from "@/types/incidentManagement/alerts.d"
36 +import Api from "@/api"
37 import Icon from "@/components/common/Icon.vue"
38 import { useGoto } from "@/composables/useGoto"
17 -import { computed, toRefs } from "vue"
39 +import { NButton, NPopover, useMessage } from "naive-ui"
40 +import { computed, defineAsyncComponent, ref, watch } from "vue"
41
42 const props = defineProps<{ alert: Alert }>()
20 -const { alert } = toRefs(props)
43
22 -const LinkIcon = "carbon:launch"
44 +const emit = defineEmits<{
45 + (e: "updated", value: Alert): void
46 + (e: "unlinked"): void
47 +}>()
48
49 +const CaseItem = defineAsyncComponent(() => import("../cases/CaseItem.vue"))
50 +
51 +const UnlinkIcon = "carbon:unlink"
52 +const MenuIcon = "carbon:overflow-menu-horizontal"
53 +const loadingId = ref<number | false>(false)
54 +const alert = ref<Alert>(props.alert)
55 +const message = useMessage()
56 const { gotoIncidentManagementCases } = useGoto()
25 -const linkedCases = computed(() => alert.value.linked_cases)
57 +const linkedCases = computed(() => alert.value?.linked_cases || [])
58 +
59 +function updateAlert(updatedAlert: Alert) {
60 + alert.value = updatedAlert
61 + emit("updated", updatedAlert)
62 +}
63 +
64 +function unlink(caseId: number) {
65 + if (!alert.value) return
66 +
67 + loadingId.value = caseId
68 +
69 + Api.incidentManagement
70 + .unlinkCase(alert.value.id, caseId)
71 + .then(res => {
72 + if (res.data.success) {
73 + updateAlert({
74 + ...alert.value,
75 + linked_cases: alert.value.linked_cases.filter(c => c.id !== caseId)
76 + })
77 +
78 + emit("unlinked")
79 + message.success(res.data?.message || "Case unlinked successfully")
80 + } else {
81 + message.warning(res.data?.message || "An error occurred. Please try again later.")
82 + }
83 + })
84 + .catch(err => {
85 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
86 + })
87 + .finally(() => {
88 + loadingId.value = false
89 + })
90 +}
91 +
92 +watch(
93 + () => props.alert,
94 + () => {
95 + alert.value = props.alert
96 + },
97 + {
98 + immediate: true
99 + }
100 +)
101 </script>
frontend/src/components/incidentManagement/alerts/AlertMergeCaseButton.vue
+34 -30
@@ -1,5 +1,5 @@
1 <template>
2 - <n-button secondary :loading="merging" @click="openDialog()">
2 + <n-button secondary :loading="merging" :size @click="openDialog()">
3 <template #icon>
4 <Icon :name="MergeIcon" />
5 </template>
@@ -10,9 +10,9 @@
10 v-model:show="showMergeBox"
11 display-directive="show"
12 preset="card"
13 - title="Select the case you want to merge it with"
13 + :title="`Select the case you want to merge ${alerts.length > 1 ? 'them' : 'it'} with :`"
14 :style="{ maxWidth: 'min(850px, 90vw)', minHeight: 'min(540px, 90vh)', maxHeight: '80vh' }"
15 - content-class="overflow-hidden flex flex-col !px-2"
15 + content-class="overflow-hidden flex flex-col !px-2 !py-0"
16 segmented
17 >
18 <n-spin
@@ -21,7 +21,7 @@
21 content-class="grow overflow-hidden flex flex-col"
22 >
23 <n-scrollbar class="flex grow flex-col" content-class="grow" trigger="none">
24 - <div class="flex flex-col gap-2 px-5 py-2">
24 + <div class="flex flex-col gap-2 px-5 py-5">
25 <template v-if="linkableCases.length">
26 <CaseItem
27 v-for="item of linkableCases"
@@ -56,22 +56,22 @@
56 <script setup lang="ts">
57 import type { Alert } from "@/types/incidentManagement/alerts.d"
58 import type { Case } from "@/types/incidentManagement/cases.d"
59 +import type { Size } from "naive-ui/es/button/src/interface"
60 import Api from "@/api"
61 import Icon from "@/components/common/Icon.vue"
62 import _orderBy from "lodash/orderBy"
63 import { NButton, NEmpty, NModal, NScrollbar, NSpin, useMessage } from "naive-ui"
63 -import { inject, ref, type Ref, toRefs, watch } from "vue"
64 +import { inject, ref, type Ref, watch } from "vue"
65 import CaseItem from "../cases/CaseItem.vue"
66
66 -const props = defineProps<{ alert: Alert }>()
67 +const { alerts, size } = defineProps<{ alerts: Alert[]; size?: Size }>()
68 +
69 const emit = defineEmits<{
70 (e: "updated", value: Alert): void
71 + (e: "merged"): void
72 }>()
73
71 -const { alert } = toRefs(props)
72 -
74 const MergeIcon = "carbon:ibm-cloud-direct-link-1-connect"
74 -
75 const message = useMessage()
76 const merging = ref(false)
77 const showMergeBox = ref(false)
@@ -130,24 +130,34 @@ function linkCase() {
130 merging.value = true
131
132 Api.incidentManagement
133 - .linkCase(alert.value.id, selectedCase.value.id)
133 + .multiLinkCase(
134 + alerts.map(o => o.id),
135 + selectedCase.value.id
136 + )
137 .then(res => {
138 if (res.data.success) {
139 closeDialog()
137 - updateAlert({
138 - ...alert.value,
139 - linked_cases: [
140 - {
141 - id: res.data.case_alert_link.case_id,
142 - case_name: "",
143 - case_description: "",
144 - case_creation_time: new Date(),
145 - assigned_to: null,
146 - case_status: null,
147 - customer_code: null
148 - }
149 - ]
150 - })
140 +
141 + for (const alert of alerts) {
142 + const caseId = res.data.case_alert_links.find(o => o.alert_id === alert.id)?.case_id || 0
143 + const caseData = linkableCases.value.find(o => o.id === caseId) || null
144 + updateAlert({
145 + ...alert,
146 + linked_cases: [
147 + {
148 + id: caseId,
149 + case_name: caseData?.case_name || "",
150 + case_description: caseData?.case_description || "",
151 + case_creation_time: caseData?.case_creation_time || new Date(),
152 + assigned_to: caseData?.assigned_to || null,
153 + case_status: caseData?.case_status || null,
154 + customer_code: caseData?.customer_code || null
155 + }
156 + ]
157 + })
158 + }
159 +
160 + emit("merged")
161 message.success(res.data?.message || "Case linked successfully")
162 } else {
163 message.warning(res.data?.message || "An error occurred. Please try again later.")
@@ -162,9 +172,3 @@ function linkCase() {
172 }
173 }
174 </script>
165 -
166 -<style lang="scss" scoped>
167 -.active {
168 - box-shadow: 0px 0px 0px 2px var(--success-color) !important;
169 -}
170 -</style>
frontend/src/components/incidentManagement/alerts/AlertOverview.vue
+2 -2
@@ -141,11 +141,11 @@
141 <div class="footer-box bg-secondary flex items-center gap-2 px-7 py-4">
142 <AlertCreateCaseButton v-if="!linkedCases.length" :alert @updated="updateAlert" />
143
144 - <AlertMergeCaseButton v-if="!linkedCases.length" :alert @updated="updateAlert" />
144 + <AlertMergeCaseButton v-if="!linkedCases.length" :alerts="[alert]" @updated="updateAlert" />
145
146 <div v-if="linkedCases.length" class="flex flex-wrap items-center gap-3">
147 <span>Linked Cases:</span>
148 - <AlertLinkedCases :alert />
148 + <AlertLinkedCases :alert @updated="updateAlert($event)" />
149 </div>
150
151 <div class="grow"></div>
frontend/src/components/incidentManagement/alerts/AlertsFilters.vue
+1
@@ -231,6 +231,7 @@ function addFilter(key: AlertsFilterTypes) {
231
232 function delFilter(key: AlertsFilterTypes) {
233 filters.value = filters.value.filter(o => o.type !== key)
234 + submit()
235 }
236
237 function setFilter(newFilters: AlertsListFilter[]) {
frontend/src/components/incidentManagement/alerts/AlertsList.vue
+33 -6
@@ -15,7 +15,14 @@
15 <div class="flex flex-col gap-2">
16 <div class="box">
17 Total :
18 - <code>{{ total }}</code>
18 + <code>
19 + <span v-if="totalFiltered === total">{{ total }}</span>
20 + <span v-else>
21 + <span>{{ totalFiltered }}</span>
22 + <span class="opacity-50">/</span>
23 + <span class="opacity-50">{{ total }}</span>
24 + </span>
25 + </code>
26 </div>
27 <div class="box text-error">
28 Open :
@@ -36,7 +43,14 @@
43 <n-button quaternary size="small" @click="filtersCTX?.setFilter([{ type: 'status', value: null }])">
44 <div class="flex items-center gap-2">
45 <span>Total</span>
39 - <code class="py-1">{{ total }}</code>
46 + <code class="py-1">
47 + <span v-if="totalFiltered === total">{{ total }}</span>
48 + <span v-else class="flex gap-1">
49 + <span>{{ totalFiltered }}</span>
50 + <span class="opacity-50">/</span>
51 + <span class="opacity-50">{{ total }}</span>
52 + </span>
53 + </code>
54 </div>
55 </n-button>
56 <span>/</span>
@@ -71,7 +85,7 @@
85 :page-slot="pageSlot"
86 :show-size-picker="showSizePicker"
87 :page-sizes="pageSizes"
74 - :item-count="total"
88 + :item-count="totalFiltered"
89 :simple="simpleMode"
90 />
91 <n-select
@@ -148,6 +162,14 @@
162 </template>
163 </n-popover>
164
165 + <AlertMergeCaseButton
166 + v-if="checkedNoLinkedAlerts.length"
167 + :alerts="checkedNoLinkedAlerts"
168 + size="small"
169 + @updated="updateAlert"
170 + @merged="resetChecked()"
171 + />
172 +
173 <n-popconfirm to="body" @positive-click="deleteAlerts()">
174 <template #trigger>
175 <n-button size="small" type="error" secondary :loading="deleting">
@@ -190,7 +212,7 @@
212 v-if="alertsList.length > 3"
213 v-model:page="currentPage"
214 :page-size="pageSize"
193 - :item-count="total"
215 + :item-count="totalFiltered"
216 :page-slot="6"
217 />
218 </div>
@@ -222,7 +244,7 @@ import {
244 NSpin,
245 useMessage
246 } from "naive-ui"
225 -import { computed, nextTick, onBeforeMount, provide, ref, watch } from "vue"
247 +import { computed, defineAsyncComponent, nextTick, onBeforeMount, provide, ref, watch } from "vue"
248 import AlertItem from "./AlertItem.vue"
249 import AlertsFilters from "./AlertsFilters.vue"
250
@@ -236,11 +258,14 @@ const {
258 showFilters?: boolean
259 }>()
260
261 +const AlertMergeCaseButton = defineAsyncComponent(() => import("./AlertMergeCaseButton.vue"))
262 +
263 const FilterIcon = "carbon:filter-edit"
264 const TrashIcon = "carbon:trash-can"
265 const InfoIcon = "carbon:information"
266
267 const checkedAlerts = ref<Alert[]>([])
268 +const checkedNoLinkedAlerts = computed(() => checkedAlerts.value.filter(alert => !alert.linked_cases.length))
269 const message = useMessage()
270 const loading = ref(false)
271 const deleting = ref(false)
@@ -263,6 +288,7 @@ const sortOptions = [
288 { label: "Asc", value: "asc" }
289 ]
290
291 +const totalFiltered = ref(0)
292 const total = ref(0)
293 const statusOpenTotal = ref(0)
294 const statusInProgressTotal = ref(0)
@@ -296,7 +322,7 @@ watch(
322 if (
323 alertsList.value.length &&
324 !alertsList.value.find(o => o.id.toString() === highlight) &&
299 - currentPage.value < total.value &&
325 + currentPage.value < totalFiltered.value &&
326 !highlightedItemFound.value
327 ) {
328 nextTick(() => {
@@ -388,6 +414,7 @@ function getData() {
414 if (res.data.success) {
415 alertsList.value = res.data?.alerts || []
416 total.value = res.data.total || 0
417 + totalFiltered.value = res.data.total_filtered ?? total.value ?? 0
418 statusCloseTotal.value = res.data.closed || 0
419 statusInProgressTotal.value = res.data.in_progress || 0
420 statusOpenTotal.value = res.data.open || 0