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