@cccsaurora/howler-ui 2.18.0-dev.700 → 2.18.0-dev.704

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/api/index.d.ts +0 -2
  2. package/api/index.js +2 -4
  3. package/api/search/index.d.ts +1 -2
  4. package/api/search/index.js +1 -2
  5. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  6. package/components/app/App.js +7 -34
  7. package/components/app/hooks/useMatchers.js +2 -2
  8. package/components/app/hooks/useMatchers.test.js +22 -22
  9. package/components/app/hooks/useTitle.js +3 -3
  10. package/components/app/providers/FavouritesProvider.js +2 -2
  11. package/components/app/providers/HitProvider.d.ts +22 -0
  12. package/components/app/providers/{RecordProvider.js → HitProvider.js} +41 -41
  13. package/components/app/providers/{RecordSearchProvider.d.ts → HitSearchProvider.d.ts} +6 -6
  14. package/components/app/providers/{RecordSearchProvider.js → HitSearchProvider.js} +17 -12
  15. package/components/app/providers/{RecordSearchProvider.test.js → HitSearchProvider.test.js} +70 -51
  16. package/components/app/providers/ModalProvider.d.ts +0 -1
  17. package/components/app/providers/ParameterProvider.d.ts +2 -9
  18. package/components/app/providers/ParameterProvider.js +240 -165
  19. package/components/app/providers/ParameterProvider.test.js +14 -307
  20. package/components/elements/PluginTypography.d.ts +1 -2
  21. package/components/elements/PluginTypography.js +2 -3
  22. package/components/elements/UserList.d.ts +2 -5
  23. package/components/elements/UserList.js +5 -14
  24. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  25. package/components/elements/display/ChipPopper.d.ts +1 -1
  26. package/components/elements/display/HowlerCard.js +1 -1
  27. package/components/elements/display/Modal.js +0 -2
  28. package/components/elements/display/icons/BundleButton.d.ts +6 -0
  29. package/components/elements/display/icons/BundleButton.js +32 -0
  30. package/components/elements/hit/HitActions.js +4 -4
  31. package/components/elements/hit/HitBanner.js +48 -28
  32. package/components/elements/hit/HitCard.js +5 -5
  33. package/components/elements/{record/RecordComments.d.ts → hit/HitComments.d.ts} +4 -5
  34. package/components/elements/{record/RecordComments.js → hit/HitComments.js} +28 -29
  35. package/components/elements/{ObjectDetails.js → hit/HitDetails.js} +17 -17
  36. package/components/elements/hit/HitLabels.js +2 -2
  37. package/components/elements/hit/{HitPreview.d.ts → HitQuickSearch.d.ts} +3 -3
  38. package/components/elements/hit/{HitPreview.js → HitQuickSearch.js} +4 -10
  39. package/components/elements/hit/HitRelated.d.ts +6 -0
  40. package/components/elements/hit/HitRelated.js +7 -0
  41. package/components/elements/hit/HitSummary.d.ts +1 -2
  42. package/components/elements/hit/HitSummary.js +5 -6
  43. package/components/elements/{record/RecordWorklog.d.ts → hit/HitWorklog.d.ts} +3 -4
  44. package/components/elements/{record/RecordWorklog.js → hit/HitWorklog.js} +13 -15
  45. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  46. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  47. package/components/elements/view/ViewTitle.d.ts +0 -1
  48. package/components/elements/view/ViewTitle.js +2 -9
  49. package/components/hooks/useHitActions.d.ts +1 -1
  50. package/components/hooks/useHitActions.js +4 -4
  51. package/components/hooks/{useRecordSelection.d.ts → useHitSelection.d.ts} +2 -2
  52. package/components/hooks/{useRecordSelection.js → useHitSelection.js} +33 -12
  53. package/components/hooks/useMyPreferences.js +1 -10
  54. package/components/hooks/useMySearch.js +2 -2
  55. package/components/hooks/useMySitemap.js +1 -4
  56. package/components/hooks/useMyTheme.js +2 -9
  57. package/components/hooks/useParamState.test.js +4 -3
  58. package/components/routes/action/edit/ActionEditor.js +2 -2
  59. package/components/routes/action/view/ActionSearch.js +1 -1
  60. package/components/routes/advanced/QueryBuilder.js +1 -1
  61. package/components/routes/advanced/QueryEditor.js +3 -3
  62. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  63. package/components/routes/analytics/AnalyticDetails.js +2 -2
  64. package/components/routes/analytics/AnalyticSearch.js +1 -1
  65. package/components/routes/dossiers/DossierEditor.js +2 -2
  66. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  67. package/components/routes/help/ApiDocumentation.js +1 -1
  68. package/components/routes/help/BundleDocumentation.d.ts +3 -0
  69. package/components/routes/help/BundleDocumentation.js +12 -0
  70. package/components/routes/help/HitBannerDocumentation.js +0 -1
  71. package/components/routes/help/HitDocumentation.js +3 -1
  72. package/components/routes/help/markdown/en/bundles.md.js +1 -0
  73. package/components/routes/help/markdown/fr/bundles.md.js +1 -0
  74. package/components/routes/hits/search/BundleParentMenu.d.ts +6 -0
  75. package/components/routes/hits/search/BundleParentMenu.js +32 -0
  76. package/components/routes/hits/search/BundleScroller.d.ts +2 -0
  77. package/components/routes/hits/search/BundleScroller.js +6 -0
  78. package/components/routes/hits/search/{RecordBrowser.js → HitBrowser.js} +9 -9
  79. package/components/{elements/record/RecordContextMenu.d.ts → routes/hits/search/HitContextMenu.d.ts} +3 -3
  80. package/components/routes/hits/search/HitContextMenu.js +227 -0
  81. package/components/{elements/record/RecordContextMenu.test.js → routes/hits/search/HitContextMenu.test.js} +39 -94
  82. package/components/routes/hits/search/{RecordQuery.d.ts → HitQuery.d.ts} +2 -2
  83. package/components/routes/hits/search/{RecordQuery.js → HitQuery.js} +6 -6
  84. package/components/routes/hits/search/InformationPane.d.ts +0 -1
  85. package/components/routes/hits/search/InformationPane.js +60 -47
  86. package/components/routes/hits/search/LayoutSettings.js +3 -3
  87. package/components/routes/hits/search/QuerySettings.js +1 -2
  88. package/components/routes/hits/search/QuerySettings.test.js +9 -14
  89. package/components/routes/hits/search/SearchPane.js +49 -26
  90. package/components/routes/hits/search/ViewLink.js +3 -3
  91. package/components/routes/hits/search/ViewLink.test.js +8 -8
  92. package/components/routes/hits/search/grid/AddColumnModal.js +4 -5
  93. package/components/routes/hits/search/grid/EnhancedCell.d.ts +1 -2
  94. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  95. package/components/routes/hits/search/grid/HitGrid.js +18 -20
  96. package/components/routes/hits/search/grid/{RecordRow.d.ts → HitRow.d.ts} +2 -3
  97. package/components/routes/hits/search/grid/{RecordRow.js → HitRow.js} +8 -10
  98. package/components/routes/hits/view/HitViewer.js +13 -12
  99. package/components/routes/home/ViewCard.js +41 -47
  100. package/components/{elements/MarkdownEditor.js → routes/overviews/OverviewEditor.js} +3 -3
  101. package/components/routes/overviews/OverviewViewer.js +2 -2
  102. package/components/routes/views/ViewComposer.js +19 -46
  103. package/locales/en/translation.json +3 -80
  104. package/locales/fr/translation.json +3 -78
  105. package/models/WithMetadata.d.ts +1 -2
  106. package/models/entities/generated/{ThreatEnrichment.d.ts → Enrichment.d.ts} +1 -1
  107. package/models/entities/generated/Hit.d.ts +0 -1
  108. package/models/entities/generated/Howler.d.ts +4 -0
  109. package/models/entities/generated/Rule.d.ts +10 -2
  110. package/models/entities/generated/Threat.d.ts +2 -2
  111. package/models/entities/generated/View.d.ts +0 -1
  112. package/package.json +106 -123
  113. package/plugins/clue/components/ClueTypography.js +2 -2
  114. package/plugins/clue/helpers.js +1 -1
  115. package/plugins/clue/utils.d.ts +1 -2
  116. package/tests/server-handlers.js +1 -6
  117. package/tests/utils.d.ts +0 -4
  118. package/tests/utils.js +0 -20
  119. package/utils/constants.d.ts +3 -3
  120. package/utils/hitFunctions.d.ts +1 -2
  121. package/utils/hitFunctions.js +4 -4
  122. package/utils/viewUtils.js +0 -3
  123. package/api/search/case.d.ts +0 -4
  124. package/api/search/case.js +0 -8
  125. package/api/v2/case/index.d.ts +0 -8
  126. package/api/v2/case/index.js +0 -20
  127. package/api/v2/case/items.d.ts +0 -6
  128. package/api/v2/case/items.js +0 -18
  129. package/api/v2/index.d.ts +0 -4
  130. package/api/v2/index.js +0 -6
  131. package/api/v2/search/facet.d.ts +0 -3
  132. package/api/v2/search/facet.js +0 -12
  133. package/api/v2/search/index.d.ts +0 -5
  134. package/api/v2/search/index.js +0 -24
  135. package/components/app/providers/RecordProvider.d.ts +0 -23
  136. package/components/elements/ContextMenu.d.ts +0 -56
  137. package/components/elements/ContextMenu.js +0 -109
  138. package/components/elements/ContextMenu.test.js +0 -215
  139. package/components/elements/ObjectDetails.d.ts +0 -6
  140. package/components/elements/case/CaseCard.d.ts +0 -12
  141. package/components/elements/case/CaseCard.js +0 -42
  142. package/components/elements/case/CasePreview.d.ts +0 -6
  143. package/components/elements/case/CasePreview.js +0 -17
  144. package/components/elements/case/StatusIcon.d.ts +0 -5
  145. package/components/elements/case/StatusIcon.js +0 -13
  146. package/components/elements/hit/elements/AnalyticLink.d.ts +0 -8
  147. package/components/elements/hit/elements/AnalyticLink.js +0 -22
  148. package/components/elements/hit/related/RelatedRecords.js +0 -63
  149. package/components/elements/observable/ObservableCard.d.ts +0 -6
  150. package/components/elements/observable/ObservableCard.js +0 -23
  151. package/components/elements/observable/ObservablePreview.d.ts +0 -6
  152. package/components/elements/observable/ObservablePreview.js +0 -12
  153. package/components/elements/record/RecordContextMenu.js +0 -247
  154. package/components/elements/record/RecordContextMenu.test.d.ts +0 -1
  155. package/components/elements/record/RecordRelated.d.ts +0 -7
  156. package/components/elements/record/RecordRelated.js +0 -34
  157. package/components/hooks/useRelatedRecords.d.ts +0 -13
  158. package/components/hooks/useRelatedRecords.js +0 -32
  159. package/components/routes/cases/CaseViewer.d.ts +0 -2
  160. package/components/routes/cases/CaseViewer.js +0 -22
  161. package/components/routes/cases/Cases.d.ts +0 -2
  162. package/components/routes/cases/Cases.js +0 -101
  163. package/components/routes/cases/constants.d.ts +0 -5
  164. package/components/routes/cases/constants.js +0 -5
  165. package/components/routes/cases/detail/AlertPanel.d.ts +0 -6
  166. package/components/routes/cases/detail/AlertPanel.js +0 -33
  167. package/components/routes/cases/detail/CaseAssets.d.ts +0 -12
  168. package/components/routes/cases/detail/CaseAssets.js +0 -104
  169. package/components/routes/cases/detail/CaseAssets.test.d.ts +0 -1
  170. package/components/routes/cases/detail/CaseAssets.test.js +0 -167
  171. package/components/routes/cases/detail/CaseDashboard.d.ts +0 -7
  172. package/components/routes/cases/detail/CaseDashboard.js +0 -54
  173. package/components/routes/cases/detail/CaseDetails.d.ts +0 -6
  174. package/components/routes/cases/detail/CaseDetails.js +0 -61
  175. package/components/routes/cases/detail/CaseOverview.d.ts +0 -7
  176. package/components/routes/cases/detail/CaseOverview.js +0 -43
  177. package/components/routes/cases/detail/CaseSidebar.d.ts +0 -8
  178. package/components/routes/cases/detail/CaseSidebar.js +0 -61
  179. package/components/routes/cases/detail/CaseTask.d.ts +0 -11
  180. package/components/routes/cases/detail/CaseTask.js +0 -57
  181. package/components/routes/cases/detail/ItemPage.d.ts +0 -6
  182. package/components/routes/cases/detail/ItemPage.js +0 -99
  183. package/components/routes/cases/detail/RelatedCasePanel.d.ts +0 -6
  184. package/components/routes/cases/detail/RelatedCasePanel.js +0 -31
  185. package/components/routes/cases/detail/TaskPanel.d.ts +0 -7
  186. package/components/routes/cases/detail/TaskPanel.js +0 -52
  187. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +0 -12
  188. package/components/routes/cases/detail/aggregates/CaseAggregate.js +0 -19
  189. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +0 -6
  190. package/components/routes/cases/detail/aggregates/SourceAggregate.js +0 -30
  191. package/components/routes/cases/detail/assets/Asset.d.ts +0 -14
  192. package/components/routes/cases/detail/assets/Asset.js +0 -12
  193. package/components/routes/cases/detail/assets/Asset.test.d.ts +0 -1
  194. package/components/routes/cases/detail/assets/Asset.test.js +0 -72
  195. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +0 -14
  196. package/components/routes/cases/detail/sidebar/CaseFolder.js +0 -133
  197. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +0 -34
  198. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +0 -105
  199. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +0 -1
  200. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +0 -351
  201. package/components/routes/cases/detail/sidebar/types.d.ts +0 -3
  202. package/components/routes/cases/detail/sidebar/utils.d.ts +0 -3
  203. package/components/routes/cases/detail/sidebar/utils.js +0 -25
  204. package/components/routes/cases/hooks/useCase.d.ts +0 -13
  205. package/components/routes/cases/hooks/useCase.js +0 -51
  206. package/components/routes/cases/modals/AddToCaseModal.d.ts +0 -7
  207. package/components/routes/cases/modals/AddToCaseModal.js +0 -62
  208. package/components/routes/cases/modals/RenameItemModal.d.ts +0 -9
  209. package/components/routes/cases/modals/RenameItemModal.js +0 -48
  210. package/components/routes/cases/modals/ResolveModal.d.ts +0 -7
  211. package/components/routes/cases/modals/ResolveModal.js +0 -62
  212. package/components/routes/hits/search/shared/IndexPicker.d.ts +0 -2
  213. package/components/routes/hits/search/shared/IndexPicker.js +0 -20
  214. package/components/routes/observables/ObservableViewer.d.ts +0 -7
  215. package/components/routes/observables/ObservableViewer.js +0 -27
  216. package/models/entities/generated/AttachmentsFile.d.ts +0 -12
  217. package/models/entities/generated/Case.d.ts +0 -28
  218. package/models/entities/generated/DestinationOriginal.d.ts +0 -19
  219. package/models/entities/generated/EmailAttachment.d.ts +0 -8
  220. package/models/entities/generated/EmailParent.d.ts +0 -19
  221. package/models/entities/generated/Enrichments.d.ts +0 -7
  222. package/models/entities/generated/EnrichmentsIndicator.d.ts +0 -21
  223. package/models/entities/generated/HttpResponse.d.ts +0 -11
  224. package/models/entities/generated/Item.d.ts +0 -9
  225. package/models/entities/generated/Observable.d.ts +0 -85
  226. package/models/entities/generated/ObservableCloud.d.ts +0 -20
  227. package/models/entities/generated/ObservableDestination.d.ts +0 -23
  228. package/models/entities/generated/ObservableEmail.d.ts +0 -30
  229. package/models/entities/generated/ObservableFile.d.ts +0 -36
  230. package/models/entities/generated/ObservableHowler.d.ts +0 -43
  231. package/models/entities/generated/ObservableHttp.d.ts +0 -11
  232. package/models/entities/generated/ObservableObserver.d.ts +0 -21
  233. package/models/entities/generated/ObservableOrganization.d.ts +0 -7
  234. package/models/entities/generated/ObservableProcess.d.ts +0 -34
  235. package/models/entities/generated/ObservableSource.d.ts +0 -23
  236. package/models/entities/generated/ObservableThreat.d.ts +0 -21
  237. package/models/entities/generated/ObservableTls.d.ts +0 -12
  238. package/models/entities/generated/ObserverIngress.d.ts +0 -9
  239. package/models/entities/generated/Task.d.ts +0 -10
  240. package/utils/typeUtils.d.ts +0 -7
  241. package/utils/typeUtils.js +0 -27
  242. /package/components/app/providers/{RecordSearchProvider.test.d.ts → HitSearchProvider.test.d.ts} +0 -0
  243. /package/components/elements/hit/{related/RelatedRecords.d.ts → HitDetails.d.ts} +0 -0
  244. /package/components/routes/hits/search/{RecordBrowser.d.ts → HitBrowser.d.ts} +0 -0
  245. /package/components/{elements/ContextMenu.test.d.ts → routes/hits/search/HitContextMenu.test.d.ts} +0 -0
  246. /package/components/{elements/MarkdownEditor.d.ts → routes/overviews/OverviewEditor.d.ts} +0 -0
@@ -1,57 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Check, Close, Delete, Edit } from '@mui/icons-material';
3
- import { Autocomplete, Card, Checkbox, Chip, IconButton, LinearProgress, Stack, TextField, Tooltip, Typography } from '@mui/material';
4
- import UserList from '@cccsaurora/howler-ui/components/elements/UserList';
5
- import { useEffect, useState } from 'react';
6
- import { useTranslation } from 'react-i18next';
7
- import { Link } from 'react-router-dom';
8
- const CaseTask = ({ task, onEdit, onDelete, paths, newTask = false }) => {
9
- const { t } = useTranslation();
10
- const [loading, setLoading] = useState(false);
11
- const [editing, setEditing] = useState(newTask);
12
- const [summary, setSummary] = useState(task?.summary || '');
13
- const [path, setPath] = useState(task?.path ?? null);
14
- const [assignment, setAssignment] = useState(task?.assignment);
15
- const [complete, setComplete] = useState(task?.complete ?? false);
16
- const dirty = summary !== task?.summary || path !== task?.path || complete !== task?.complete || assignment !== task?.assignment;
17
- const onSubmit = async () => {
18
- if (dirty && editing) {
19
- setLoading(true);
20
- await onEdit({ summary, path: !path ? null : path, assignment, complete });
21
- setLoading(false);
22
- }
23
- };
24
- useEffect(() => {
25
- if (!editing && task?.assignment !== assignment) {
26
- setLoading(true);
27
- onEdit({ assignment }).finally(() => setLoading(false));
28
- }
29
- // eslint-disable-next-line react-hooks/exhaustive-deps
30
- }, [assignment]);
31
- useEffect(() => {
32
- if (!editing && task?.complete !== complete) {
33
- setLoading(true);
34
- onEdit({ complete }).finally(() => setLoading(false));
35
- }
36
- // eslint-disable-next-line react-hooks/exhaustive-deps
37
- }, [complete]);
38
- return (_jsxs(Card, { sx: { pl: 0.5, pr: 1, py: 0.5, position: 'relative' }, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Checkbox, { disabled: loading, color: "success", checked: complete, size: "small", onChange: (_ev, _complete) => setComplete(_complete) }), editing ? (_jsx(TextField, { disabled: loading, value: summary, onChange: e => setSummary(e.target.value), size: "small", fullWidth: true, sx: { minWidth: '40%' } })) : (_jsx(Typography, { sx: [complete && { textDecoration: 'line-through' }], children: task?.summary || summary })), !editing && path && _jsx(Chip, { clickable: true, component: Link, to: path, label: path }), editing && (_jsx(Autocomplete, { disabled: loading, value: path, options: paths, onChange: (_ev, value) => setPath(value), fullWidth: true, renderInput: params => _jsx(TextField, { ...params, size: "small" }) })), _jsx(UserList, { disabled: loading, userIds: [assignment], onChange: ([_assigment]) => setAssignment(_assigment), i18nLabel: "route.cases.task.set.assignment", avatarHeight: 24 }), _jsx("div", { style: { flex: 1 } }), editing && !newTask && (_jsx(Tooltip, { title: t('route.cases.task.delete'), children: _jsx(IconButton, { size: "small", color: "error", onClick: () => {
39
- setLoading(true);
40
- onDelete().then(() => setLoading(false));
41
- }, disabled: loading, children: _jsx(Delete, { fontSize: "small" }) }) })), _jsx(Tooltip, { title: t(editing ? 'route.cases.task.edit.save' : 'route.cases.task.edit'), children: _jsx("span", { children: _jsx(IconButton, { size: "small", color: editing ? 'success' : 'default', onClick: async () => {
42
- if (!editing) {
43
- setEditing(true);
44
- return;
45
- }
46
- await onSubmit();
47
- setEditing(false);
48
- }, disabled: (!dirty && editing) || loading || !summary, children: editing ? _jsx(Check, { fontSize: "small" }) : _jsx(Edit, { fontSize: "small" }) }) }) }), editing && (_jsx(Tooltip, { title: t('route.cases.task.edit.cancel'), children: _jsx(IconButton, { size: "small", onClick: () => {
49
- if (newTask) {
50
- onDelete();
51
- }
52
- else {
53
- setEditing(false);
54
- }
55
- }, disabled: loading, children: _jsx(Close, { fontSize: "small" }) }) }))] }), loading && _jsx(LinearProgress, { sx: { left: 0, bottom: 0, right: 0, position: 'absolute' } })] }));
56
- };
57
- export default CaseTask;
@@ -1,6 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import { type FC } from 'react';
3
- declare const ItemPage: FC<{
4
- case?: Case;
5
- }>;
6
- export default ItemPage;
@@ -1,99 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import api from '@cccsaurora/howler-ui/api';
3
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
4
- import NotFoundPage from '@cccsaurora/howler-ui/components/routes/404';
5
- import InformationPane from '@cccsaurora/howler-ui/components/routes/hits/search/InformationPane';
6
- import ObservableViewer from '@cccsaurora/howler-ui/components/routes/observables/ObservableViewer';
7
- import { useEffect, useMemo, useState } from 'react';
8
- import { useOutletContext, useParams } from 'react-router-dom';
9
- import useCase from '../hooks/useCase';
10
- import CaseDashboard from './CaseDashboard';
11
- const ItemPage = ({ case: providedCase }) => {
12
- const params = useParams();
13
- const routeCase = useOutletContext();
14
- const { case: fetchedCase } = useCase({ caseId: !providedCase && !routeCase ? params.id : undefined });
15
- const _case = providedCase ?? routeCase ?? fetchedCase;
16
- const { dispatchApi } = useMyApi();
17
- const [item, setItem] = useState(null);
18
- const [loading, setLoading] = useState(true);
19
- // When rendered as a child route, the wildcard segment is in params['*'].
20
- // When rendered directly with a case prop, fall back to parsing the pathname.
21
- const subPath = params['*'] ?? '';
22
- const normalizedSubPath = useMemo(() => subPath.replace(/^\/+|\/+$/g, ''), [subPath]);
23
- useEffect(() => {
24
- let cancelled = false;
25
- const resolveItem = async () => {
26
- setLoading(true);
27
- if (!normalizedSubPath) {
28
- if (!cancelled) {
29
- setItem(null);
30
- setLoading(false);
31
- }
32
- return;
33
- }
34
- let currentCase = _case;
35
- let remainingPath = normalizedSubPath;
36
- while (currentCase && remainingPath) {
37
- const currentRemainingPath = remainingPath;
38
- const matchedNestedCase = currentCase.items
39
- .filter(_item => _item?.path &&
40
- _item?.type?.toLowerCase() === 'case' &&
41
- (currentRemainingPath === _item.path || currentRemainingPath.startsWith(`${_item.path}/`)))
42
- .sort((a, b) => (b.path?.length || 0) - (a.path?.length || 0))[0];
43
- if (!matchedNestedCase) {
44
- break;
45
- }
46
- if (currentRemainingPath === matchedNestedCase.path) {
47
- if (!cancelled) {
48
- setItem(matchedNestedCase);
49
- setLoading(false);
50
- }
51
- return;
52
- }
53
- if (!matchedNestedCase.value) {
54
- if (!cancelled) {
55
- setItem(null);
56
- setLoading(false);
57
- }
58
- return;
59
- }
60
- const nextCase = await dispatchApi(api.v2.case.get(matchedNestedCase.value), { throwError: false });
61
- if (!nextCase) {
62
- if (!cancelled) {
63
- setItem(null);
64
- setLoading(false);
65
- }
66
- return;
67
- }
68
- remainingPath = currentRemainingPath.slice((matchedNestedCase.path?.length || 0) + 1);
69
- currentCase = nextCase;
70
- }
71
- const resolvedItem = currentCase?.items?.find(_item => _item.path === remainingPath);
72
- if (!cancelled) {
73
- setItem(resolvedItem || null);
74
- setLoading(false);
75
- }
76
- };
77
- resolveItem();
78
- return () => {
79
- cancelled = true;
80
- };
81
- }, [_case, dispatchApi, normalizedSubPath]);
82
- if (loading) {
83
- return null;
84
- }
85
- if (!item) {
86
- return _jsx(NotFoundPage, {});
87
- }
88
- if (item.type === 'hit') {
89
- return _jsx(InformationPane, { selected: item.value });
90
- }
91
- if (item.type === 'observable') {
92
- return _jsx(ObservableViewer, { observableId: item.value });
93
- }
94
- if (item.type === 'case') {
95
- return _jsx(CaseDashboard, { caseId: item.value });
96
- }
97
- return _jsx("h1", { children: JSON.stringify(item) });
98
- };
99
- export default ItemPage;
@@ -1,6 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import { type FC } from 'react';
3
- declare const RelatedCasePanel: FC<{
4
- case: Case;
5
- }>;
6
- export default RelatedCasePanel;
@@ -1,31 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Divider, Pagination, Skeleton, Stack, Typography, useTheme } from '@mui/material';
3
- import { chunk, uniq } from 'lodash-es';
4
- import { useMemo, useState } from 'react';
5
- import { useTranslation } from 'react-i18next';
6
- import { Link } from 'react-router-dom';
7
- import CaseCard from '../../../elements/case/CaseCard';
8
- const RelatedCasePanel = ({ case: _case }) => {
9
- const { t } = useTranslation();
10
- const theme = useTheme();
11
- const [casePage, setCasePage] = useState(1);
12
- const casePages = useMemo(() => chunk(uniq((_case?.items ?? []).filter(item => item.type === 'case')), 5), [_case?.items]);
13
- if (!_case) {
14
- return _jsx(Skeleton, { height: 240 });
15
- }
16
- return (_jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.cases') }), _jsx(Pagination, { count: casePages.length, page: casePage, onChange: (_, page) => setCasePage(page) })] }), _jsx(Divider, {}), casePages[casePage - 1]?.map(item => (_jsxs(Box, { position: "relative", children: [_jsx(CaseCard, { caseId: item.value }), _jsx(Box, { component: Link, to: item.path, sx: {
17
- position: 'absolute',
18
- top: 0,
19
- left: 0,
20
- width: '100%',
21
- height: '100%',
22
- cursor: 'pointer',
23
- zIndex: 100,
24
- borderRadius: '4px',
25
- '&:hover': {
26
- background: theme.palette.divider,
27
- border: `thin solid ${theme.palette.primary.light}`
28
- }
29
- } })] }, item.path)))] }));
30
- };
31
- export default RelatedCasePanel;
@@ -1,7 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import { type FC } from 'react';
3
- declare const TaskPanel: FC<{
4
- case: Case;
5
- updateCase: (_case: Partial<Case>) => Promise<void>;
6
- }>;
7
- export default TaskPanel;
@@ -1,52 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Add } from '@mui/icons-material';
3
- import { Divider, Skeleton, Stack, Typography } from '@mui/material';
4
- import { useState } from 'react';
5
- import { useTranslation } from 'react-i18next';
6
- import CaseTask from './CaseTask';
7
- const TaskPanel = ({ case: _case, updateCase }) => {
8
- const { t } = useTranslation();
9
- const [addingTask, setAddingTask] = useState(false);
10
- const onEdit = (task) => async (newTask) => {
11
- if (task) {
12
- await updateCase({
13
- tasks: _case.tasks.map(_task => {
14
- if (_task.id !== task.id) {
15
- return _task;
16
- }
17
- return {
18
- ..._task,
19
- ...newTask
20
- };
21
- })
22
- });
23
- }
24
- else {
25
- await updateCase({
26
- tasks: [..._case.tasks, newTask]
27
- });
28
- }
29
- };
30
- if (!_case) {
31
- return _jsx(Skeleton, { height: 240 });
32
- }
33
- return (_jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.tasks') }), _jsx(Divider, {}), _case.tasks.map(task => (_jsx(CaseTask, { task: task, paths: _case.items.map(item => item.path), onEdit: onEdit(task), onDelete: () => updateCase({ tasks: _case.tasks.filter(_task => _task.id !== task.id) }) }, task.id))), addingTask && (_jsx(CaseTask, { newTask: true, paths: _case.items.map(item => item.path), onEdit: async (task) => {
34
- await onEdit()(task);
35
- setAddingTask(false);
36
- }, onDelete: async () => setAddingTask(false) })), _jsxs(Stack, { onClick: () => setAddingTask(true), direction: "row", spacing: 2, sx: theme => ({
37
- borderStyle: 'dashed',
38
- borderColor: theme.palette.text.secondary,
39
- borderWidth: '0.15rem',
40
- borderRadius: '0.15rem',
41
- opacity: 0.3,
42
- justifyContent: 'center',
43
- alignItems: 'center',
44
- padding: 1,
45
- transition: theme.transitions.create('opacity'),
46
- '&:hover': {
47
- opacity: 1,
48
- cursor: 'pointer'
49
- }
50
- }), children: [_jsx(Add, {}), _jsx(Typography, { children: t('page.cases.dashboard.tasks.add') })] })] }));
51
- };
52
- export default TaskPanel;
@@ -1,12 +0,0 @@
1
- import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
- import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
3
- import { type FC } from 'react';
4
- declare const CaseAggregate: FC<{
5
- icon?: string;
6
- iconColor?: string;
7
- field?: string;
8
- records?: Partial<Hit | Observable>[];
9
- title?: string;
10
- subtitle?: string;
11
- }>;
12
- export default CaseAggregate;
@@ -1,19 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Icon } from '@iconify/react';
3
- import { Card, CardContent, Skeleton, Stack, styled, Tooltip, tooltipClasses, Typography, useTheme } from '@mui/material';
4
- import { get, isEmpty, uniq } from 'lodash-es';
5
- import {} from 'react';
6
- const NoMaxWidthTooltip = styled(({ className, ...props }) => (_jsx(Tooltip, { ...props, classes: { popper: className } })))({
7
- [`& .${tooltipClasses.tooltip}`]: {
8
- maxWidth: 'none'
9
- }
10
- });
11
- const CaseAggregate = ({ icon, iconColor, field, records, title, subtitle }) => {
12
- const theme = useTheme();
13
- if (!title && (!records || !field)) {
14
- return _jsx(Skeleton, { height: 120 });
15
- }
16
- const values = uniq(records?.map(_record => get(_record, field)).flat());
17
- return (_jsx(Card, { sx: { height: '100%' }, children: _jsx(CardContent, { children: _jsxs(Stack, { alignItems: "center", spacing: 1, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [icon && _jsx(Icon, { fontSize: "96px", icon: icon, color: iconColor || theme.palette.grey[700] }), _jsx(NoMaxWidthTooltip, { title: !isEmpty(values) && (_jsx(Stack, { spacing: 0.5, children: values.map(value => (_jsx("span", { children: value }, value))) })), children: _jsxs(Typography, { variant: "h3", children: [values.length, !isEmpty(values) && !!title && ' - ', title] }) })] }), _jsx(Typography, { color: "textSecondary", children: subtitle })] }) }) }));
18
- };
19
- export default CaseAggregate;
@@ -1,6 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import { type FC } from 'react';
3
- declare const SourceAggregate: FC<{
4
- case: Case;
5
- }>;
6
- export default SourceAggregate;
@@ -1,30 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Chip, Grid, Skeleton } from '@mui/material';
3
- import api from '@cccsaurora/howler-ui/api';
4
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
5
- import { uniq } from 'lodash-es';
6
- import { useEffect, useMemo, useState } from 'react';
7
- import useCase from '../../hooks/useCase';
8
- const SourceAggregate = ({ case: providedCase }) => {
9
- const { dispatchApi } = useMyApi();
10
- const { case: _case } = useCase({ case: providedCase });
11
- const [analytics, setAnalytics] = useState([]);
12
- const hitIds = useMemo(() => _case?.items
13
- .filter(item => item.type === 'hit')
14
- .map(item => item.value)
15
- .filter(value => !!value), [_case?.items]);
16
- useEffect(() => {
17
- dispatchApi(api.v2.search.post('hit', { query: `howler.id:(${hitIds.join(' OR ')})`, fl: 'howler.analytic' }))
18
- .then(response => response?.items.map(hit => hit.howler.analytic) ?? [])
19
- .then(_analytics => setAnalytics(uniq(_analytics)));
20
- api.v2.search.facet.post(['hit', 'observable'], {
21
- query: `howler.id:(${hitIds.join(' OR ')})`,
22
- fields: ['howler.analytic']
23
- });
24
- }, [dispatchApi, hitIds]);
25
- if (!_case) {
26
- return _jsx(Skeleton, { height: 12, variant: "rounded" });
27
- }
28
- return (_jsx(Grid, { container: true, spacing: 1, children: analytics.map(_analytic => (_jsx(Grid, { item: true, children: _jsx(Chip, { size: "small", label: _analytic }) }, _analytic))) }));
29
- };
30
- export default SourceAggregate;
@@ -1,14 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import type { FC } from 'react';
3
- export type AssetType = 'hash' | 'hosts' | 'ip' | 'user' | 'ids' | 'id' | 'uri' | 'signature';
4
- export interface AssetEntry {
5
- type: AssetType;
6
- value: string;
7
- /** IDs of the hits/observables this asset was seen in */
8
- seenIn: string[];
9
- }
10
- declare const Asset: FC<{
11
- asset: AssetEntry;
12
- case: Case;
13
- }>;
14
- export default Asset;
@@ -1,12 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Card, CardContent, Chip, Stack, Typography } from '@mui/material';
3
- import { useTranslation } from 'react-i18next';
4
- import { Link } from 'react-router-dom';
5
- const Asset = ({ asset, case: _case }) => {
6
- const { t } = useTranslation();
7
- return (_jsx(Card, { sx: { height: '100%' }, children: _jsx(CardContent, { children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Chip, { size: "small", label: t(`page.cases.assets.type.${asset.type}`), color: "primary", variant: "outlined" }), _jsx(Typography, { variant: "body2", sx: { wordBreak: 'break-all', fontFamily: 'monospace' }, children: asset.value })] }), asset.seenIn.length > 0 && (_jsxs(Stack, { spacing: 0.5, children: [_jsx(Typography, { variant: "caption", color: "text.secondary", children: t('page.cases.assets.seen_in') }), _jsx(Stack, { direction: "row", flexWrap: "wrap", gap: 0.5, children: asset.seenIn.map(id => {
8
- const entry = _case.items.find(item => item.value === id);
9
- return (_jsx(Chip, { clickable: true, size: "small", label: entry.path, variant: "outlined", component: Link, to: `/cases/${_case.case_id}/${entry.path}` }, id));
10
- }) })] }))] }) }) }));
11
- };
12
- export default Asset;
@@ -1 +0,0 @@
1
- export {};
@@ -1,72 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- /// <reference types="vitest" />
3
- import { render, screen } from '@testing-library/react';
4
- import { MemoryRouter } from 'react-router-dom';
5
- import { createMockCase } from '@cccsaurora/howler-ui/tests/utils';
6
- import { describe, expect, it } from 'vitest';
7
- import Asset, {} from './Asset';
8
- const makeAsset = (overrides = {}) => ({
9
- type: 'ip',
10
- value: '192.168.1.1',
11
- seenIn: [],
12
- ...overrides
13
- });
14
- describe('Asset', () => {
15
- describe('type chip', () => {
16
- it('renders the correct label for each type', () => {
17
- const cases = ['hash', 'hosts', 'ip', 'user', 'ids', 'id', 'uri', 'signature'];
18
- for (const type of cases) {
19
- const { unmount } = render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ type, value: 'x' }), case: createMockCase() }) }));
20
- expect(screen.getByText(`page.cases.assets.type.${type}`)).toBeTruthy();
21
- unmount();
22
- }
23
- });
24
- });
25
- describe('value display', () => {
26
- it('renders the asset value', () => {
27
- render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ value: '10.0.0.1' }), case: createMockCase() }) }));
28
- expect(screen.getByText('10.0.0.1')).toBeTruthy();
29
- });
30
- it('renders long hash values without truncation', () => {
31
- const hash = 'a'.repeat(64);
32
- render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ type: 'hash', value: hash }), case: createMockCase() }) }));
33
- expect(screen.getByText(hash)).toBeTruthy();
34
- });
35
- });
36
- describe('seen-in chips', () => {
37
- it('renders nothing when seenIn is empty', () => {
38
- render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ seenIn: [] }), case: createMockCase() }) }));
39
- expect(screen.queryByText('page.cases.assets.seen_in')).toBeNull();
40
- });
41
- it('renders "Seen in" label when seenIn has entries', () => {
42
- const _case = createMockCase({
43
- items: [{ path: 'alerts/test-analytic (hit-001)', type: 'hit', value: 'hit-001' }]
44
- });
45
- render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ seenIn: ['hit-001'] }), case: _case }) }));
46
- expect(screen.getByText('page.cases.assets.seen_in')).toBeTruthy();
47
- });
48
- it('renders a chip labelled with entry.path for each seenIn id', () => {
49
- const _case = createMockCase({
50
- items: [
51
- { path: 'alerts/my-analytic (hit-001)', type: 'hit', value: 'hit-001' },
52
- { path: 'observables/obs-002', type: 'observable', value: 'obs-002' },
53
- { path: 'alerts/other-analytic (hit-003)', type: 'hit', value: 'hit-003' }
54
- ]
55
- });
56
- render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ seenIn: ['hit-001', 'obs-002', 'hit-003'] }), case: _case }) }));
57
- expect(screen.getByText('alerts/my-analytic (hit-001)')).toBeTruthy();
58
- expect(screen.getByText('observables/obs-002')).toBeTruthy();
59
- expect(screen.getByText('alerts/other-analytic (hit-003)')).toBeTruthy();
60
- });
61
- it('links each chip to /cases/:case_id/:path', () => {
62
- const _case = createMockCase({
63
- case_id: 'case-abc',
64
- items: [{ path: 'alerts/my-analytic (hit-001)', type: 'hit', value: 'hit-001' }]
65
- });
66
- render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ seenIn: ['hit-001'] }), case: _case }) }));
67
- const link = screen.getByText('alerts/my-analytic (hit-001)').closest('a');
68
- expect(link).not.toBeNull();
69
- expect(link?.getAttribute('href')).toBe('/cases/case-abc/alerts/my-analytic (hit-001)');
70
- });
71
- });
72
- });
@@ -1,14 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import { type FC } from 'react';
3
- import type { Tree } from './types';
4
- interface CaseFolderProps {
5
- case: Case;
6
- folder?: Tree;
7
- name?: string;
8
- step?: number;
9
- rootCaseId?: string;
10
- pathPrefix?: string;
11
- onItemUpdated?: (newCase: Case) => void;
12
- }
13
- declare const CaseFolder: FC<CaseFolderProps>;
14
- export default CaseFolder;
@@ -1,133 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Article, BookRounded, CheckCircle, ChevronRight, Folder as FolderIcon, Lightbulb, Link as LinkIcon, TableChart, Visibility } from '@mui/icons-material';
3
- import { alpha, Skeleton, Stack, Typography, useTheme } from '@mui/material';
4
- import api from '@cccsaurora/howler-ui/api';
5
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
6
- import { omit } from 'lodash-es';
7
- import { useCallback, useEffect, useMemo, useState } from 'react';
8
- import { Link, useLocation } from 'react-router-dom';
9
- import { ESCALATION_COLORS } from '@cccsaurora/howler-ui/utils/constants';
10
- import CaseFolderContextMenu from './CaseFolderContextMenu';
11
- import { buildTree } from './utils';
12
- // Static map: item type → MUI icon component (avoids re-creating closures on each render)
13
- const ICON_FOR_TYPE = {
14
- case: BookRounded,
15
- observable: Visibility,
16
- hit: CheckCircle,
17
- table: TableChart,
18
- lead: Lightbulb,
19
- reference: LinkIcon
20
- };
21
- const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPrefix = '', onItemUpdated }) => {
22
- const theme = useTheme();
23
- const location = useLocation();
24
- const { dispatchApi } = useMyApi();
25
- const [open, setOpen] = useState(true);
26
- const [caseStates, setCaseStates] = useState({});
27
- const [hitMetadata, setHitMetadata] = useState({});
28
- const tree = useMemo(() => folder || buildTree(_case?.items), [folder, _case?.items]);
29
- const currentRootCaseId = rootCaseId || _case?.case_id;
30
- const hitIds = useMemo(() => _case?.items
31
- .filter(item => item.type === 'hit')
32
- .map(item => item.value)
33
- .filter(value => !!value), [_case?.items]);
34
- useEffect(() => {
35
- if (hitIds.length < 1) {
36
- return;
37
- }
38
- dispatchApi(api.search.hit.post({ query: `howler.id:(${hitIds.join(' OR ')})` }), { throwError: false }).then(result => {
39
- if ((result?.items?.length ?? 0) < 1)
40
- return;
41
- setHitMetadata(Object.fromEntries(result.items.map(hit => [hit.howler.id, hit.howler])));
42
- });
43
- }, [hitIds, dispatchApi]);
44
- // Returns the MUI colour token for the item's escalation, or undefined if none.
45
- const getEscalationColor = (itemType, itemKey, leafId) => {
46
- if (itemType === 'hit' && leafId) {
47
- const color = ESCALATION_COLORS[hitMetadata[leafId]?.escalation];
48
- if (color)
49
- return color;
50
- }
51
- if (itemType === 'case' && itemKey) {
52
- const color = ESCALATION_COLORS[caseStates[itemKey]?.data?.escalation];
53
- if (color)
54
- return color;
55
- }
56
- return undefined;
57
- };
58
- const toggleCase = useCallback((item, itemKey) => {
59
- const resolvedKey = itemKey || item.path || item.value;
60
- if (!resolvedKey) {
61
- return;
62
- }
63
- const prev = caseStates[resolvedKey] ?? { open: false, loading: false, data: null };
64
- const shouldOpen = !prev.open;
65
- const shouldFetch = shouldOpen && !!item.value && !prev.data && !prev.loading;
66
- setCaseStates(current => ({ ...current, [resolvedKey]: { ...prev, open: shouldOpen, loading: shouldFetch } }));
67
- if (!shouldFetch)
68
- return;
69
- dispatchApi(api.v2.case.get(item.value), { throwError: false })
70
- .then(caseResponse => {
71
- if (!caseResponse)
72
- return;
73
- setCaseStates(current => ({ ...current, [resolvedKey]: { ...current[resolvedKey], data: caseResponse } }));
74
- })
75
- .finally(() => {
76
- setCaseStates(current => ({ ...current, [resolvedKey]: { ...current[resolvedKey], loading: false } }));
77
- });
78
- }, [caseStates, dispatchApi]);
79
- return (_jsxs(Stack, { sx: { overflow: 'visible' }, children: [name && (_jsx(CaseFolderContextMenu, { _case: _case, tree: tree, onUpdate: onItemUpdated, children: _jsxs(Stack, { direction: "row", pl: step * 1.5, py: 0.25, sx: {
80
- cursor: 'pointer',
81
- transition: theme.transitions.create('background', { duration: 50 }),
82
- background: 'transparent',
83
- '&:hover': {
84
- background: theme.palette.grey[800]
85
- }
86
- }, onClick: () => setOpen(_open => !_open), children: [_jsx(ChevronRight, { fontSize: "small", color: "disabled", sx: [
87
- { transition: theme.transitions.create('transform', { duration: 100 }), transform: 'rotate(0deg)' },
88
- open && { transform: 'rotate(90deg)' }
89
- ] }), _jsx(FolderIcon, { fontSize: "small", color: "disabled" }), _jsx(Typography, { variant: "caption", color: "textSecondary", sx: { userSelect: 'none', pl: 0.5, textWrap: 'nowrap' }, children: name })] }) })), open && (_jsxs(_Fragment, { children: [Object.entries(omit(tree, 'leaves')).map(([path, subfolder]) => (_jsx(CaseFolder, { name: path, case: _case, folder: subfolder, step: step + 1, rootCaseId: currentRootCaseId, pathPrefix: pathPrefix, onItemUpdated: onItemUpdated }, `${_case?.case_id}-${path}`))), tree.leaves?.map(leaf => {
90
- const itemType = leaf.type?.toLowerCase();
91
- const isCase = itemType === 'case';
92
- const fullRelativePath = [pathPrefix, leaf.path].filter(Boolean).join('/');
93
- const itemKey = fullRelativePath || leaf.value;
94
- const nodeState = itemKey ? caseStates[itemKey] : null;
95
- const isCaseOpen = !!nodeState?.open;
96
- const isCaseLoading = !!nodeState?.loading;
97
- const nestedCase = nodeState?.data ?? null;
98
- const itemPath = itemType !== 'reference'
99
- ? fullRelativePath
100
- ? `/cases/${currentRootCaseId}/${fullRelativePath}`
101
- : `/cases/${currentRootCaseId}`
102
- : leaf.value;
103
- const escalationColor = getEscalationColor(itemType, itemKey, leaf.value);
104
- const iconColor = escalationColor ?? 'inherit';
105
- const leafColor = escalationColor ? `${escalationColor}.light` : 'text.secondary';
106
- const Icon = ICON_FOR_TYPE[itemType ?? ''] ?? Article;
107
- return (_jsx(CaseFolderContextMenu, { _case: _case, leaf: leaf, onUpdate: onItemUpdated, children: _jsxs(Stack, { children: [_jsxs(Stack, { direction: "row", pl: step * 1.5 + 1, py: 0.25, sx: [
108
- {
109
- cursor: 'pointer',
110
- overflow: 'visible',
111
- color: `${theme.palette.text.secondary} !important`,
112
- textDecoration: 'none',
113
- transition: theme.transitions.create('background', { duration: 100 }),
114
- background: 'transparent',
115
- '&:hover': {
116
- background: theme.palette.grey[800]
117
- },
118
- borderRight: '3px solid transparent'
119
- },
120
- decodeURIComponent(location.pathname) === itemPath && {
121
- background: alpha(theme.palette.grey[600], 0.15),
122
- borderRightColor: theme.palette.primary.main
123
- }
124
- ], onClick: () => isCase && toggleCase(leaf, itemKey), component: Link, to: itemPath, target: itemType === 'reference' ? '_blank' : undefined, rel: itemType === 'reference' ? 'noopener noreferrer' : undefined, children: [_jsx(ChevronRight, { fontSize: "small", sx: [
125
- !isCase && { opacity: 0 },
126
- isCase && {
127
- transition: theme.transitions.create('transform', { duration: 100 }),
128
- transform: isCaseOpen ? 'rotate(90deg)' : 'rotate(0deg)'
129
- }
130
- ] }), _jsx(Icon, { fontSize: "small", color: iconColor }), _jsx(Typography, { variant: "caption", color: leafColor, sx: { userSelect: 'none', pl: 0.5, textWrap: 'nowrap' }, children: leaf.path?.split('/').pop() || leaf.value })] }), isCase && isCaseOpen && isCaseLoading && (_jsx(Stack, { pl: step * 1.5 + 4, py: 0.25, children: _jsx(Skeleton, { width: 140, height: 16 }) })), isCase && isCaseOpen && nestedCase && (_jsx(CaseFolder, { case: nestedCase, step: step + 1, rootCaseId: currentRootCaseId, pathPrefix: fullRelativePath, onItemUpdated: onItemUpdated }))] }) }, `${_case?.case_id}-${leaf.value}-${leaf.path}`));
131
- })] }))] }));
132
- };
133
- export default CaseFolder;
@@ -1,34 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import type { Item } from '@cccsaurora/howler-ui/models/entities/generated/Item';
3
- import { type FC, type PropsWithChildren } from 'react';
4
- import type { Tree } from './types';
5
- /**
6
- * Recursively collects all leaf items from a folder tree.
7
- */
8
- export declare const collectAllLeaves: (tree: Tree) => Item[];
9
- /**
10
- * Returns the URL to open for a given leaf item, or null if no URL applies.
11
- * - reference: the item's value (an external URL)
12
- * - hit: /hits/<id>
13
- * - observable: /observables/<id>
14
- * - case: /cases/<id>
15
- * - table / lead: null (no dedicated detail page)
16
- */
17
- export declare const getOpenUrl: (leaf: Item) => string | null;
18
- export interface CaseFolderContextMenuProps extends PropsWithChildren {
19
- /** The case that owns the item(s). */
20
- _case: Case;
21
- /** Present when the context menu is for a single leaf item. */
22
- leaf?: Item;
23
- /** Present when the context menu is for a folder (all leaves within it will be removed). */
24
- tree?: Tree;
25
- /** Called after item(s) have been updated (renamed, removed). */
26
- onUpdate?: (updatedCase: Case) => void;
27
- }
28
- /**
29
- * Wraps its children with a right-click context menu providing:
30
- * - **Open item** – opens the item in a new tab (only for leaf items with a navigable URL).
31
- * - **Remove item / Remove folder** – deletes the leaf item or all items under a folder.
32
- */
33
- declare const CaseFolderContextMenu: FC<CaseFolderContextMenuProps>;
34
- export default CaseFolderContextMenu;