@cccsaurora/howler-ui 2.18.0-dev.648 → 2.18.0-dev.674

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 (235) hide show
  1. package/api/index.d.ts +2 -0
  2. package/api/index.js +4 -2
  3. package/api/search/case.d.ts +4 -0
  4. package/api/search/case.js +8 -0
  5. package/api/search/index.d.ts +2 -1
  6. package/api/search/index.js +2 -1
  7. package/api/v2/case/index.d.ts +6 -0
  8. package/api/v2/case/index.js +18 -0
  9. package/api/v2/index.d.ts +4 -0
  10. package/api/v2/index.js +6 -0
  11. package/api/v2/search/facet.d.ts +3 -0
  12. package/api/v2/search/facet.js +12 -0
  13. package/api/v2/search/index.d.ts +5 -0
  14. package/api/v2/search/index.js +24 -0
  15. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  16. package/components/app/App.js +34 -7
  17. package/components/app/hooks/useMatchers.js +2 -2
  18. package/components/app/hooks/useMatchers.test.js +22 -22
  19. package/components/app/hooks/useTitle.js +3 -3
  20. package/components/app/providers/FavouritesProvider.js +2 -2
  21. package/components/app/providers/ParameterProvider.d.ts +9 -2
  22. package/components/app/providers/ParameterProvider.js +165 -240
  23. package/components/app/providers/ParameterProvider.test.js +307 -14
  24. package/components/app/providers/RecordProvider.d.ts +23 -0
  25. package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
  26. package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
  27. package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
  28. package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +51 -70
  29. package/components/elements/ContextMenu.d.ts +56 -0
  30. package/components/elements/ContextMenu.js +109 -0
  31. package/components/elements/ContextMenu.test.js +215 -0
  32. package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
  33. package/components/elements/ObjectDetails.d.ts +6 -0
  34. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
  35. package/components/elements/PluginTypography.d.ts +2 -1
  36. package/components/elements/PluginTypography.js +3 -2
  37. package/components/elements/UserList.d.ts +5 -2
  38. package/components/elements/UserList.js +14 -5
  39. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  40. package/components/elements/case/CaseCard.d.ts +8 -0
  41. package/components/elements/case/CaseCard.js +39 -0
  42. package/components/elements/case/CasePreview.d.ts +6 -0
  43. package/components/elements/case/CasePreview.js +17 -0
  44. package/components/elements/case/StatusIcon.d.ts +5 -0
  45. package/components/elements/case/StatusIcon.js +13 -0
  46. package/components/elements/display/ChipPopper.d.ts +1 -1
  47. package/components/elements/display/HowlerCard.js +1 -1
  48. package/components/elements/display/Modal.js +1 -0
  49. package/components/elements/hit/HitActions.js +4 -4
  50. package/components/elements/hit/HitBanner.js +28 -48
  51. package/components/elements/hit/HitCard.js +5 -5
  52. package/components/elements/hit/HitLabels.js +2 -2
  53. package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
  54. package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
  55. package/components/elements/hit/HitSummary.d.ts +2 -1
  56. package/components/elements/hit/HitSummary.js +6 -5
  57. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  58. package/components/elements/hit/elements/AnalyticLink.d.ts +8 -0
  59. package/components/elements/hit/elements/AnalyticLink.js +22 -0
  60. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  61. package/components/elements/hit/related/RelatedRecords.js +63 -0
  62. package/components/elements/observable/ObservableCard.d.ts +6 -0
  63. package/components/elements/observable/ObservableCard.js +23 -0
  64. package/components/elements/observable/ObservablePreview.d.ts +6 -0
  65. package/components/elements/observable/ObservablePreview.js +12 -0
  66. package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
  67. package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
  68. package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
  69. package/components/elements/record/RecordContextMenu.js +235 -0
  70. package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
  71. package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +39 -39
  72. package/components/elements/record/RecordRelated.d.ts +7 -0
  73. package/components/elements/record/RecordRelated.js +34 -0
  74. package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
  75. package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
  76. package/components/elements/view/ViewTitle.js +1 -1
  77. package/components/hooks/useHitActions.d.ts +1 -1
  78. package/components/hooks/useHitActions.js +4 -4
  79. package/components/hooks/useLocalStorage.test.d.ts +1 -0
  80. package/components/hooks/useLocalStorage.test.js +137 -0
  81. package/components/hooks/useMyPreferences.js +10 -1
  82. package/components/hooks/useMySearch.js +2 -2
  83. package/components/hooks/useMySitemap.js +4 -1
  84. package/components/hooks/useMyTheme.js +9 -2
  85. package/components/hooks/useParamState.d.ts +6 -0
  86. package/components/hooks/useParamState.js +48 -0
  87. package/components/hooks/useParamState.test.d.ts +1 -0
  88. package/components/hooks/useParamState.test.js +166 -0
  89. package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
  90. package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
  91. package/components/hooks/useRelatedRecords.d.ts +13 -0
  92. package/components/hooks/useRelatedRecords.js +32 -0
  93. package/components/routes/action/edit/ActionEditor.js +2 -2
  94. package/components/routes/action/view/ActionSearch.js +1 -1
  95. package/components/routes/advanced/QueryBuilder.js +1 -1
  96. package/components/routes/advanced/QueryEditor.js +3 -3
  97. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  98. package/components/routes/analytics/AnalyticDetails.js +2 -2
  99. package/components/routes/analytics/AnalyticSearch.js +1 -1
  100. package/components/routes/cases/CaseViewer.d.ts +2 -0
  101. package/components/routes/cases/CaseViewer.js +22 -0
  102. package/components/routes/cases/Cases.d.ts +2 -0
  103. package/components/routes/cases/Cases.js +101 -0
  104. package/components/routes/cases/constants.d.ts +5 -0
  105. package/components/routes/cases/constants.js +5 -0
  106. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  107. package/components/routes/cases/detail/AlertPanel.js +33 -0
  108. package/components/routes/cases/detail/CaseAssets.d.ts +12 -0
  109. package/components/routes/cases/detail/CaseAssets.js +101 -0
  110. package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
  111. package/components/routes/cases/detail/CaseAssets.test.js +163 -0
  112. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  113. package/components/routes/cases/detail/CaseDashboard.js +51 -0
  114. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  115. package/components/routes/cases/detail/CaseDetails.js +61 -0
  116. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  117. package/components/routes/cases/detail/CaseOverview.js +43 -0
  118. package/components/routes/cases/detail/CaseSidebar.d.ts +6 -0
  119. package/components/routes/cases/detail/CaseSidebar.js +61 -0
  120. package/components/routes/cases/detail/CaseTask.d.ts +11 -0
  121. package/components/routes/cases/detail/CaseTask.js +57 -0
  122. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  123. package/components/routes/cases/detail/ItemPage.js +99 -0
  124. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  125. package/components/routes/cases/detail/RelatedCasePanel.js +31 -0
  126. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  127. package/components/routes/cases/detail/TaskPanel.js +52 -0
  128. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +12 -0
  129. package/components/routes/cases/detail/aggregates/CaseAggregate.js +19 -0
  130. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  131. package/components/routes/cases/detail/aggregates/SourceAggregate.js +27 -0
  132. package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
  133. package/components/routes/cases/detail/assets/Asset.js +12 -0
  134. package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
  135. package/components/routes/cases/detail/assets/Asset.test.js +72 -0
  136. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +13 -0
  137. package/components/routes/cases/detail/sidebar/CaseFolder.js +131 -0
  138. package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
  139. package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
  140. package/components/routes/cases/detail/sidebar/utils.js +25 -0
  141. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  142. package/components/routes/cases/hooks/useCase.js +38 -0
  143. package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
  144. package/components/routes/cases/modals/ResolveModal.js +59 -0
  145. package/components/routes/dossiers/DossierEditor.js +2 -2
  146. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  147. package/components/routes/help/ApiDocumentation.js +1 -1
  148. package/components/routes/help/HitBannerDocumentation.js +1 -0
  149. package/components/routes/help/HitDocumentation.js +1 -3
  150. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  151. package/components/routes/hits/search/InformationPane.js +47 -60
  152. package/components/routes/hits/search/LayoutSettings.js +3 -3
  153. package/components/routes/hits/search/QuerySettings.js +2 -1
  154. package/components/routes/hits/search/QuerySettings.test.js +14 -9
  155. package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
  156. package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
  157. package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
  158. package/components/routes/hits/search/SearchPane.js +26 -49
  159. package/components/routes/hits/search/ViewLink.js +3 -3
  160. package/components/routes/hits/search/ViewLink.test.js +8 -8
  161. package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
  162. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
  163. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  164. package/components/routes/hits/search/grid/HitGrid.js +20 -18
  165. package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
  166. package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
  167. package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
  168. package/components/routes/hits/search/shared/IndexPicker.js +20 -0
  169. package/components/routes/hits/view/HitViewer.js +12 -13
  170. package/components/routes/home/ViewCard.js +4 -4
  171. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  172. package/components/routes/observables/ObservableViewer.js +27 -0
  173. package/components/routes/overviews/OverviewViewer.js +2 -2
  174. package/components/routes/views/ViewComposer.js +4 -4
  175. package/locales/en/translation.json +65 -3
  176. package/locales/fr/translation.json +63 -3
  177. package/models/WithMetadata.d.ts +2 -1
  178. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  179. package/models/entities/generated/Case.d.ts +28 -0
  180. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  181. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  182. package/models/entities/generated/EmailParent.d.ts +19 -0
  183. package/models/entities/generated/Enrichments.d.ts +7 -0
  184. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  185. package/models/entities/generated/Hit.d.ts +1 -0
  186. package/models/entities/generated/Howler.d.ts +0 -4
  187. package/models/entities/generated/HttpResponse.d.ts +11 -0
  188. package/models/entities/generated/Item.d.ts +9 -0
  189. package/models/entities/generated/Observable.d.ts +85 -0
  190. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  191. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  192. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  193. package/models/entities/generated/ObservableFile.d.ts +36 -0
  194. package/models/entities/generated/ObservableHowler.d.ts +43 -0
  195. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  196. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  197. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  198. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  199. package/models/entities/generated/ObservableSource.d.ts +23 -0
  200. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  201. package/models/entities/generated/ObservableTls.d.ts +12 -0
  202. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  203. package/models/entities/generated/Rule.d.ts +2 -10
  204. package/models/entities/generated/Task.d.ts +10 -0
  205. package/models/entities/generated/Threat.d.ts +2 -2
  206. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  207. package/package.json +18 -1
  208. package/plugins/clue/components/ClueTypography.js +2 -2
  209. package/plugins/clue/utils.d.ts +2 -1
  210. package/tests/utils.d.ts +2 -0
  211. package/tests/utils.js +8 -0
  212. package/utils/constants.d.ts +3 -3
  213. package/utils/hitFunctions.d.ts +2 -1
  214. package/utils/hitFunctions.js +4 -4
  215. package/utils/typeUtils.d.ts +7 -0
  216. package/utils/typeUtils.js +27 -0
  217. package/components/app/providers/HitProvider.d.ts +0 -22
  218. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  219. package/components/elements/display/icons/BundleButton.js +0 -32
  220. package/components/elements/hit/HitRelated.d.ts +0 -6
  221. package/components/elements/hit/HitRelated.js +0 -7
  222. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  223. package/components/routes/help/BundleDocumentation.js +0 -12
  224. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  225. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  226. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  227. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  228. package/components/routes/hits/search/BundleScroller.d.ts +0 -2
  229. package/components/routes/hits/search/BundleScroller.js +0 -6
  230. package/components/routes/hits/search/HitContextMenu.js +0 -227
  231. /package/components/app/providers/{HitSearchProvider.test.d.ts → RecordSearchProvider.test.d.ts} +0 -0
  232. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → elements/ContextMenu.test.d.ts} +0 -0
  233. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
  234. /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
  235. /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Topic } from '@mui/icons-material';
3
+ import { Typography } from '@mui/material';
4
+ import api from '@cccsaurora/howler-ui/api';
5
+ import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
6
+ import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
7
+ import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
8
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
9
+ import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
10
+ import { useCallback, useContext, useEffect, useState } from 'react';
11
+ import { useTranslation } from 'react-i18next';
12
+ import { useNavigate, useSearchParams } from 'react-router-dom';
13
+ import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
14
+ import CaseCard from '../../elements/case/CaseCard';
15
+ const CasesBase = () => {
16
+ const { t } = useTranslation();
17
+ const navigate = useNavigate();
18
+ const { dispatchApi } = useMyApi();
19
+ const [searchParams, setSearchParams] = useSearchParams();
20
+ const { load } = useContext(TuiListMethodContext);
21
+ const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
22
+ const [phrase, setPhrase] = useState('');
23
+ const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
24
+ const [response, setResponse] = useState(null);
25
+ const [hasError, setHasError] = useState(false);
26
+ const [loading, setLoading] = useState(false);
27
+ const onSearch = useCallback(async () => {
28
+ try {
29
+ setLoading(true);
30
+ setHasError(false);
31
+ if (phrase) {
32
+ searchParams.set('phrase', phrase);
33
+ }
34
+ else {
35
+ searchParams.delete('phrase');
36
+ }
37
+ setSearchParams(searchParams, { replace: true });
38
+ // Check for the actual search query
39
+ const query = phrase ? `*:*${phrase}*` : '*:*';
40
+ // Ensure the overview should be visible and/or matches the type we are filtering for
41
+ setResponse(await dispatchApi(api.search.case.post({
42
+ query,
43
+ rows: pageCount,
44
+ offset
45
+ })));
46
+ }
47
+ catch (e) {
48
+ setHasError(true);
49
+ }
50
+ finally {
51
+ setLoading(false);
52
+ }
53
+ }, [phrase, setSearchParams, searchParams, dispatchApi, pageCount, offset]);
54
+ // Load the items into list when response changes.
55
+ // This hook should only trigger when the 'response' changes.
56
+ useEffect(() => {
57
+ if (response) {
58
+ load(response.items.map((item) => ({
59
+ id: item.case_id,
60
+ item,
61
+ selected: false,
62
+ cursor: false
63
+ })));
64
+ }
65
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
+ }, [response, load]);
67
+ const onPageChange = useCallback((_offset) => {
68
+ if (_offset !== offset) {
69
+ searchParams.set('offset', _offset.toString());
70
+ setSearchParams(searchParams, { replace: true });
71
+ setOffset(_offset);
72
+ }
73
+ }, [offset, searchParams, setSearchParams]);
74
+ useEffect(() => {
75
+ onSearch();
76
+ if (!searchParams.has('offset')) {
77
+ searchParams.set('offset', '0');
78
+ setSearchParams(searchParams, { replace: true });
79
+ }
80
+ // eslint-disable-next-line react-hooks/exhaustive-deps
81
+ }, []);
82
+ useEffect(() => {
83
+ if (response?.total <= offset) {
84
+ setOffset(0);
85
+ searchParams.set('offset', '0');
86
+ setSearchParams(searchParams, { replace: true });
87
+ }
88
+ }, [offset, response?.total, searchParams, setSearchParams]);
89
+ useEffect(() => {
90
+ if (!loading) {
91
+ onSearch();
92
+ }
93
+ // eslint-disable-next-line react-hooks/exhaustive-deps
94
+ }, [offset]);
95
+ const renderer = useCallback((item, className) => _jsx(CaseCard, { case: item, className: className }), []);
96
+ return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: loading, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.cases.search.prompt') }), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/cases/${item.id}`), onCreate: () => navigate('/cases/create'), createPrompt: "route.cases.create", searchPrompt: "route.cases.manager.search", createIcon: _jsx(Topic, { sx: { mr: 1 } }) }));
97
+ };
98
+ const Cases = () => {
99
+ return (_jsx(TuiListProvider, { children: _jsx(CasesBase, {}) }));
100
+ };
101
+ export default Cases;
@@ -0,0 +1,5 @@
1
+ export declare const ESCALATION_COLOR_MAP: {
2
+ normal: string;
3
+ focus: string;
4
+ crisis: string;
5
+ };
@@ -0,0 +1,5 @@
1
+ export const ESCALATION_COLOR_MAP = {
2
+ normal: 'default',
3
+ focus: 'warning',
4
+ crisis: 'error'
5
+ };
@@ -0,0 +1,6 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import { type FC } from 'react';
3
+ declare const AlertPanel: FC<{
4
+ case: Case;
5
+ }>;
6
+ export default AlertPanel;
@@ -0,0 +1,33 @@
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 HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
4
+ import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
5
+ import { chunk, uniq } from 'lodash-es';
6
+ import { useMemo, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Link } from 'react-router-dom';
9
+ const AlertPanel = ({ case: _case }) => {
10
+ const theme = useTheme();
11
+ const { t } = useTranslation();
12
+ const [alertPage, setAlertPage] = useState(1);
13
+ const alertPages = useMemo(() => chunk(uniq((_case?.items ?? []).filter(item => item.type === 'hit')), 5), [_case?.items]);
14
+ if (!_case) {
15
+ return _jsx(Skeleton, { height: 240 });
16
+ }
17
+ return (_jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.alerts') }), _jsx(Pagination, { count: alertPages.length, page: alertPage, onChange: (_, page) => setAlertPage(page) })] }), _jsx(Divider, {}), alertPages?.length > 0 &&
18
+ alertPages[alertPage - 1].map(item => (_jsxs(Box, { position: "relative", children: [_jsx(HitCard, { layout: HitLayout.DENSE, id: item.id }), _jsx(Box, { component: Link, to: item.path, sx: {
19
+ position: 'absolute',
20
+ top: 0,
21
+ left: 0,
22
+ width: '100%',
23
+ height: '100%',
24
+ cursor: 'pointer',
25
+ zIndex: 100,
26
+ borderRadius: '4px',
27
+ '&:hover': {
28
+ background: theme.palette.divider,
29
+ border: `thin solid ${theme.palette.primary.light}`
30
+ }
31
+ } })] }, item.id)))] }));
32
+ };
33
+ export default AlertPanel;
@@ -0,0 +1,12 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
3
+ import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
4
+ import { type FC } from 'react';
5
+ import { type AssetEntry } from './assets/Asset';
6
+ /** Deduplicate and merge seenIn lists into a map keyed by `type:value` */
7
+ export declare const buildAssetEntries: (records: Partial<Hit | Observable>[]) => AssetEntry[];
8
+ declare const CaseAssets: FC<{
9
+ case?: Case;
10
+ caseId?: string;
11
+ }>;
12
+ export default CaseAssets;
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Chip, Grid, Skeleton, Stack, Typography } from '@mui/material';
3
+ import api from '@cccsaurora/howler-ui/api';
4
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
5
+ import { useEffect, useMemo, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { useOutletContext } from 'react-router-dom';
8
+ import useCase from '../hooks/useCase';
9
+ import Asset, {} from './assets/Asset';
10
+ /** All Related fields that carry asset values */
11
+ const ASSET_FIELDS = ['hash', 'hosts', 'ip', 'user', 'ids', 'id', 'uri', 'signature'];
12
+ /** Extract (type, value, seenInId) triples from a record's related field */
13
+ const extractAssets = (related, recordId) => {
14
+ if (!related) {
15
+ return [];
16
+ }
17
+ const results = [];
18
+ for (const field of ASSET_FIELDS) {
19
+ const raw = related[field];
20
+ if (!raw) {
21
+ continue;
22
+ }
23
+ const values = Array.isArray(raw) ? raw : [raw];
24
+ for (const value of values) {
25
+ if (value) {
26
+ results.push({ type: field, value: String(value), id: recordId });
27
+ }
28
+ }
29
+ }
30
+ return results;
31
+ };
32
+ /** Deduplicate and merge seenIn lists into a map keyed by `type:value` */
33
+ export const buildAssetEntries = (records) => {
34
+ const map = new Map();
35
+ for (const record of records) {
36
+ const related = record.related ?? record.related;
37
+ const recordId = record.howler?.id ?? record.howler?.id;
38
+ if (!recordId) {
39
+ continue;
40
+ }
41
+ for (const { type, value, id } of extractAssets(related, recordId)) {
42
+ const key = `${type}:${value}`;
43
+ if (!map.has(key)) {
44
+ map.set(key, { type, value, seenIn: [] });
45
+ }
46
+ const entry = map.get(key);
47
+ if (!entry.seenIn.includes(id)) {
48
+ entry.seenIn.push(id);
49
+ }
50
+ }
51
+ }
52
+ return Array.from(map.values());
53
+ };
54
+ const RELATED_FIELDS = ASSET_FIELDS.map(f => `related.${f}`).join(',');
55
+ const CaseAssets = ({ case: providedCase, caseId }) => {
56
+ const { t } = useTranslation();
57
+ const { dispatchApi } = useMyApi();
58
+ const routeCase = useOutletContext();
59
+ const { case: _case } = useCase({ case: providedCase ?? routeCase, caseId });
60
+ const [records, setRecords] = useState(null);
61
+ const [activeFilters, setActiveFilters] = useState(new Set());
62
+ const ids = useMemo(() => (_case?.items ?? []).filter(item => ['hit', 'observable'].includes(item.type)).map(item => item.id), [_case?.items]);
63
+ useEffect(() => {
64
+ if (ids.length < 1) {
65
+ setRecords([]);
66
+ return;
67
+ }
68
+ dispatchApi(api.v2.search.post(['hit', 'observable'], {
69
+ query: `howler.id:(${ids.join(' OR ')})`,
70
+ fl: `howler.id,${RELATED_FIELDS}`
71
+ })).then(response => setRecords(response.items));
72
+ }, [dispatchApi, ids]);
73
+ const allAssets = useMemo(() => (records ? buildAssetEntries(records) : []), [records]);
74
+ const assetTypes = useMemo(() => (allAssets ? [...new Set(allAssets.map(a => a.type))].sort() : []), [allAssets]);
75
+ const filteredAssets = useMemo(() => {
76
+ if (allAssets.length < 1) {
77
+ return [];
78
+ }
79
+ if (activeFilters.size === 0) {
80
+ return allAssets;
81
+ }
82
+ return allAssets.filter(a => activeFilters.has(a.type));
83
+ }, [allAssets, activeFilters]);
84
+ const toggleFilter = (type) => {
85
+ setActiveFilters(prev => {
86
+ const next = new Set(prev);
87
+ if (next.has(type)) {
88
+ next.delete(type);
89
+ }
90
+ else {
91
+ next.add(type);
92
+ }
93
+ return next;
94
+ });
95
+ };
96
+ if (!_case) {
97
+ return null;
98
+ }
99
+ return (_jsxs(Grid, { container: true, spacing: 2, px: 2, children: [_jsx(Grid, { item: true, xs: 12, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, flexWrap: "wrap", children: [_jsx(Typography, { variant: "subtitle2", color: "text.secondary", children: t('page.cases.assets.filter_by_type') }), records === null ? (_jsx(Skeleton, { width: 240, height: 32 })) : (assetTypes.map(type => (_jsx(Chip, { label: t(`page.cases.assets.type.${type}`), size: "small", onClick: () => toggleFilter(type), color: activeFilters.has(type) ? 'primary' : 'default', variant: activeFilters.has(type) ? 'filled' : 'outlined' }, type))))] }) }), filteredAssets === null ? (Array.from({ length: 6 }, (_, i) => (_jsx(Grid, { item: true, xs: 12, sm: 6, md: 4, xl: 3, children: _jsx(Skeleton, { height: 100 }) }, `skeleton-${i}`)))) : filteredAssets.length === 0 ? (_jsx(Grid, { item: true, xs: 12, children: _jsx(Typography, { color: "text.secondary", children: t('page.cases.assets.empty') }) })) : (filteredAssets.map(asset => (_jsx(Grid, { item: true, xs: 12, md: 6, xl: 4, children: _jsx(Asset, { asset: asset, case: _case }) }, `${asset.type}:${asset.value}`))))] }));
100
+ };
101
+ export default CaseAssets;
@@ -0,0 +1,163 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { act, render, screen } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { createElement } from 'react';
6
+ import { MemoryRouter } from 'react-router-dom';
7
+ import { describe, expect, it, vi } from 'vitest';
8
+ import { buildAssetEntries } from './CaseAssets';
9
+ // ---------------------------------------------------------------------------
10
+ // Pure logic tests — no React needed
11
+ // ---------------------------------------------------------------------------
12
+ const makeHit = (id, related) => ({
13
+ howler: { id },
14
+ related
15
+ });
16
+ const makeObservable = (id, related) => ({
17
+ howler: { id },
18
+ related
19
+ });
20
+ describe('buildAssetEntries', () => {
21
+ it('returns an empty array for records with no related field', () => {
22
+ expect(buildAssetEntries([makeHit('h1', undefined)])).toEqual([]);
23
+ });
24
+ it('extracts a single IP from a hit', () => {
25
+ const result = buildAssetEntries([makeHit('h1', { ip: ['1.2.3.4'] })]);
26
+ expect(result).toHaveLength(1);
27
+ expect(result[0]).toEqual({ type: 'ip', value: '1.2.3.4', seenIn: ['h1'] });
28
+ });
29
+ it('extracts multiple fields from a single record', () => {
30
+ const result = buildAssetEntries([makeHit('h1', { ip: ['1.2.3.4'], user: ['alice'] })]);
31
+ const types = result.map(a => a.type).sort();
32
+ expect(types).toEqual(['ip', 'user']);
33
+ });
34
+ it('deduplicates the same asset value across multiple records', () => {
35
+ const result = buildAssetEntries([makeHit('h1', { ip: ['1.2.3.4'] }), makeObservable('obs1', { ip: ['1.2.3.4'] })]);
36
+ expect(result).toHaveLength(1);
37
+ expect(result[0].seenIn).toEqual(['h1', 'obs1']);
38
+ });
39
+ it('keeps distinct asset values as separate entries', () => {
40
+ const result = buildAssetEntries([makeHit('h1', { ip: ['1.2.3.4'] }), makeHit('h2', { ip: ['5.6.7.8'] })]);
41
+ expect(result).toHaveLength(2);
42
+ });
43
+ it('does not duplicate seenIn ids when the same record appears twice for the same asset', () => {
44
+ const result = buildAssetEntries([makeHit('h1', { ip: ['1.2.3.4'] }), makeHit('h1', { ip: ['1.2.3.4'] })]);
45
+ expect(result[0].seenIn).toEqual(['h1']);
46
+ });
47
+ it('skips records with no howler.id', () => {
48
+ const noId = { related: { ip: ['1.2.3.4'] } };
49
+ expect(buildAssetEntries([noId])).toEqual([]);
50
+ });
51
+ it('handles the scalar `id` field on Related', () => {
52
+ const result = buildAssetEntries([makeHit('h1', { id: 'some-id' })]);
53
+ expect(result).toHaveLength(1);
54
+ expect(result[0]).toEqual({ type: 'id', value: 'some-id', seenIn: ['h1'] });
55
+ });
56
+ it('handles array fields like hash, hosts, user, ids, uri, signature', () => {
57
+ const related = {
58
+ hash: ['abc123'],
59
+ hosts: ['host.example.com'],
60
+ user: ['bob'],
61
+ ids: ['guid-1'],
62
+ uri: ['https://example.com'],
63
+ signature: ['rule-X']
64
+ };
65
+ const result = buildAssetEntries([makeHit('h1', related)]);
66
+ const types = result.map(a => a.type).sort();
67
+ expect(types).toEqual(['hash', 'hosts', 'ids', 'signature', 'uri', 'user']);
68
+ });
69
+ });
70
+ // ---------------------------------------------------------------------------
71
+ // Component rendering tests
72
+ // ---------------------------------------------------------------------------
73
+ const mockDispatchApi = vi.fn();
74
+ vi.mock('components/hooks/useMyApi', () => ({
75
+ default: () => ({ dispatchApi: mockDispatchApi })
76
+ }));
77
+ vi.mock('../hooks/useCase', () => ({
78
+ default: ({ case: c }) => ({ case: c, updateCase: vi.fn(), loading: false, missing: false })
79
+ }));
80
+ const mockCase = {
81
+ case_id: 'case-001',
82
+ items: [
83
+ { id: 'hit-1', type: 'hit', path: 'hit-1' },
84
+ { id: 'obs-1', type: 'observable', path: 'obs-1' }
85
+ ]
86
+ };
87
+ const Wrapper = ({ children }) => createElement(MemoryRouter, { initialEntries: ['/cases/case-001/assets'] }, children);
88
+ // lazy import the component after mocks are set up
89
+ const CaseAssets = (await import('./CaseAssets')).default;
90
+ describe('CaseAssets component', () => {
91
+ beforeEach(() => {
92
+ mockDispatchApi.mockClear();
93
+ });
94
+ it('renders skeletons while records are loading', () => {
95
+ mockDispatchApi.mockReturnValue(new Promise(() => { })); // never resolves
96
+ render(_jsx(CaseAssets, { case: mockCase }), { wrapper: Wrapper });
97
+ // 6 skeleton cards
98
+ const skeletons = document.querySelectorAll('.MuiSkeleton-root');
99
+ expect(skeletons.length).toBeGreaterThan(0);
100
+ });
101
+ it('shows "No assets found" when records have no related data', async () => {
102
+ mockDispatchApi.mockResolvedValue({
103
+ items: [{ howler: { id: 'hit-1' } }, { howler: { id: 'obs-1' } }]
104
+ });
105
+ render(_jsx(CaseAssets, { case: mockCase }), { wrapper: Wrapper });
106
+ await screen.findByText('page.cases.assets.empty');
107
+ });
108
+ it('renders asset cards for extracted assets', async () => {
109
+ mockDispatchApi.mockResolvedValue({
110
+ items: [{ howler: { id: 'hit-1' }, related: { ip: ['1.2.3.4'], user: ['alice'] } }]
111
+ });
112
+ render(_jsx(CaseAssets, { case: mockCase }), { wrapper: Wrapper });
113
+ await screen.findByText('1.2.3.4');
114
+ expect(screen.getByText('alice')).toBeTruthy();
115
+ });
116
+ it('renders a filter chip for each asset type present', async () => {
117
+ mockDispatchApi.mockResolvedValue({
118
+ items: [{ howler: { id: 'hit-1' }, related: { ip: ['1.2.3.4'], user: ['alice'] } }]
119
+ });
120
+ render(_jsx(CaseAssets, { case: mockCase }), { wrapper: Wrapper });
121
+ await screen.findAllByText('page.cases.assets.type.ip');
122
+ expect(screen.getAllByText('page.cases.assets.type.ip')).toHaveLength(2);
123
+ expect(screen.getAllByText('page.cases.assets.type.user')).toHaveLength(2);
124
+ });
125
+ it('filters assets when a type chip is clicked', async () => {
126
+ mockDispatchApi.mockResolvedValue({
127
+ items: [{ howler: { id: 'hit-1' }, related: { ip: ['1.2.3.4'], user: ['alice'] } }]
128
+ });
129
+ render(_jsx(CaseAssets, { case: mockCase }), { wrapper: Wrapper });
130
+ // Wait for both assets to appear
131
+ await screen.findByText('1.2.3.4');
132
+ expect(screen.getByText('alice')).toBeTruthy();
133
+ // Click the 'ip' filter chip
134
+ await act(async () => {
135
+ await userEvent.click(screen.getByRole('button', { name: 'page.cases.assets.type.ip' }));
136
+ });
137
+ // Alice (user) should be filtered out
138
+ expect(screen.queryByText('alice')).toBeNull();
139
+ expect(screen.getByText('1.2.3.4')).toBeTruthy();
140
+ });
141
+ it('restores all assets when an active filter chip is clicked again', async () => {
142
+ mockDispatchApi.mockResolvedValue({
143
+ items: [{ howler: { id: 'hit-1' }, related: { ip: ['1.2.3.4'], user: ['alice'] } }]
144
+ });
145
+ render(_jsx(CaseAssets, { case: mockCase }), { wrapper: Wrapper });
146
+ await screen.findByText('1.2.3.4');
147
+ const ipChip = screen.getByRole('button', { name: 'page.cases.assets.type.ip' });
148
+ await act(async () => {
149
+ await userEvent.click(ipChip);
150
+ });
151
+ await act(async () => {
152
+ await userEvent.click(ipChip);
153
+ });
154
+ expect(screen.getByText('1.2.3.4')).toBeTruthy();
155
+ expect(screen.getByText('alice')).toBeTruthy();
156
+ });
157
+ it('renders nothing when the case has no hit/observable items', async () => {
158
+ const emptyCase = { case_id: 'case-002', items: [] };
159
+ render(_jsx(CaseAssets, { case: emptyCase }), { wrapper: Wrapper });
160
+ await screen.findByText('page.cases.assets.empty');
161
+ expect(mockDispatchApi).not.toHaveBeenCalled();
162
+ });
163
+ });
@@ -0,0 +1,7 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import { type FC } from 'react';
3
+ declare const CaseDashboard: FC<{
4
+ case?: Case;
5
+ caseId?: string;
6
+ }>;
7
+ export default CaseDashboard;
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Grid, useTheme } from '@mui/material';
3
+ import api from '@cccsaurora/howler-ui/api';
4
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
5
+ import dayjs from 'dayjs';
6
+ import { get } from 'lodash-es';
7
+ import { useEffect, useMemo, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { useOutletContext } from 'react-router-dom';
10
+ import useCase from '../hooks/useCase';
11
+ import CaseAggregate from './aggregates/CaseAggregate';
12
+ import AlertPanel from './AlertPanel';
13
+ import CaseOverview from './CaseOverview';
14
+ import RelatedCasePanel from './RelatedCasePanel';
15
+ import TaskPanel from './TaskPanel';
16
+ const AGGREGATE_FIELDS = [
17
+ ['howler.outline.threat', 'material-symbols:warning-rounded', 'warning.main', 'page.cases.dashboard.threat'],
18
+ ['howler.outline.target', 'material-symbols:group', 'primary.main', 'page.cases.dashboard.target'],
19
+ ['howler.outline.indicators', 'fluent:number-symbol-24-filled', null, 'page.cases.dashboard.indicators']
20
+ ];
21
+ const getDuration = (case_) => {
22
+ if (case_?.start) {
23
+ return dayjs
24
+ .duration(dayjs(case_?.end ?? new Date()).diff(dayjs(case_.start), 'minute'), 'minute')
25
+ .format('HH[h] mm[m]');
26
+ }
27
+ return '--';
28
+ };
29
+ const CaseDashboard = ({ case: providedCase, caseId }) => {
30
+ const { t } = useTranslation();
31
+ const { dispatchApi } = useMyApi();
32
+ const theme = useTheme();
33
+ const routeCase = useOutletContext();
34
+ const { case: _case, updateCase } = useCase({ case: providedCase ?? routeCase, caseId });
35
+ const [records, setRecords] = useState(null);
36
+ const ids = useMemo(() => (_case?.items ?? []).filter(item => ['hit', 'observable'].includes(item.type)).map(item => item.id), [_case?.items]);
37
+ useEffect(() => {
38
+ if (ids?.length < 1) {
39
+ return;
40
+ }
41
+ dispatchApi(api.v2.search.post(['hit', 'observable'], {
42
+ query: `howler.id:(${ids?.join(' OR ') || '*'})`,
43
+ fl: AGGREGATE_FIELDS.map(([field]) => field).join(',')
44
+ })).then(response => setRecords(response.items));
45
+ }, [dispatchApi, ids]);
46
+ if (!_case) {
47
+ return null;
48
+ }
49
+ return (_jsxs(Grid, { container: true, spacing: 5, width: "100%", px: 2, children: [_jsx(Grid, { item: true, xs: 12, children: _jsx(CaseOverview, { case: _case, updateCase: updateCase }) }), AGGREGATE_FIELDS.map(([field, icon, iconColor, subtitle]) => (_jsx(Grid, { item: true, xs: 12, md: 6, xl: 3, children: _jsx(CaseAggregate, { icon: icon, iconColor: iconColor && get(theme.palette, iconColor), field: field, records: records, subtitle: t(subtitle) }) }, field))), _jsx(Grid, { item: true, xs: 12, md: 6, xl: 3, children: _jsx(CaseAggregate, { icon: "mingcute:heartbeat-line", iconColor: theme.palette.error.light, title: getDuration(_case), subtitle: t('page.cases.dashboard.duration') }) }), _jsx(Grid, { item: true, xs: 12, children: _jsx(TaskPanel, { case: _case, updateCase: updateCase }) }), _jsx(Grid, { item: true, xs: 12, children: _jsx(AlertPanel, { case: _case }) }), _jsx(Grid, { item: true, xs: 12, children: _jsx(RelatedCasePanel, { case: _case }) })] }));
50
+ };
51
+ export default CaseDashboard;
@@ -0,0 +1,6 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import { type FC } from 'react';
3
+ declare const CaseDetails: FC<{
4
+ case: Case;
5
+ }>;
6
+ export default CaseDetails;
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Check, FormatListBulleted, HourglassBottom, Pause, People, WarningRounded } from '@mui/icons-material';
3
+ import { Autocomplete, Card, Chip, Divider, LinearProgress, Skeleton, Stack, Table, TableBody, TableCell, TableRow, TextField, Typography } from '@mui/material';
4
+ import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
5
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
6
+ import UserList from '@cccsaurora/howler-ui/components/elements/UserList';
7
+ import dayjs from 'dayjs';
8
+ import { useContext, useState } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+ import useCase from '../hooks/useCase';
11
+ import ResolveModal from '../modals/ResolveModal';
12
+ import SourceAggregate from './aggregates/SourceAggregate';
13
+ const CaseDetails = ({ case: providedCase }) => {
14
+ const { t } = useTranslation();
15
+ const { case: _case, updateCase } = useCase({ case: providedCase });
16
+ const { showModal } = useContext(ModalContext);
17
+ const { config } = useContext(ApiConfigContext);
18
+ const [loading, setLoading] = useState(false);
19
+ const wrappedUpdate = async (subset) => {
20
+ try {
21
+ setLoading(true);
22
+ await updateCase(subset);
23
+ }
24
+ finally {
25
+ setLoading(false);
26
+ }
27
+ };
28
+ const handleStatus = (status) => {
29
+ if (status === 'resolved') {
30
+ const onConfirm = () => wrappedUpdate({ status });
31
+ showModal(_jsx(ResolveModal, { case: _case, onConfirm: onConfirm }), { maxHeight: '80vh' });
32
+ }
33
+ else {
34
+ wrappedUpdate({ status });
35
+ }
36
+ };
37
+ if (!_case) {
38
+ return (_jsx(Card, { sx: {
39
+ borderRadius: 0,
40
+ width: '300px',
41
+ maxHeight: 'calc(100vh - 64px)',
42
+ display: 'flex',
43
+ flexDirection: 'column',
44
+ p: 1
45
+ }, children: _jsx(Skeleton, { variant: "rounded", height: 50 }) }));
46
+ }
47
+ return (_jsxs(Card, { elevation: 1, sx: {
48
+ borderRadius: 0,
49
+ width: '300px',
50
+ maxHeight: 'calc(100vh - 64px)',
51
+ display: 'flex',
52
+ flexDirection: 'column',
53
+ p: 1,
54
+ position: 'relative'
55
+ }, children: [_jsx(LinearProgress, { sx: { opacity: +loading, position: 'absolute', top: 0, left: 0, right: 0 } }), _jsxs(Stack, { spacing: 2, children: [_jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [{
56
+ 'in-progress': _jsx(HourglassBottom, { color: "warning" }),
57
+ 'on-hold': _jsx(Pause, { color: "disabled" }),
58
+ resolved: _jsx(Check, { color: "success" })
59
+ }[_case.status] ?? _jsx(WarningRounded, { fontSize: "small" }), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.status') })] }), _jsx(Autocomplete, { size: "small", disabled: loading, value: _case.status, options: config.lookups['howler.status'], renderInput: params => _jsx(TextField, { ...params, size: "small" }), onChange: (_ev, status) => handleStatus(status) })] }), _jsx(Divider, {}), _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(People, {}), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.participants') })] }), _jsx(UserList, { buttonSx: { alignSelf: 'start' }, multiple: true, i18nLabel: "page.cases.detail.assignment", userIds: _case.participants ?? [], onChange: participants => wrappedUpdate({ participants }), disabled: loading })] }), _jsx(Divider, {}), _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(FormatListBulleted, {}), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.properties') })] }), _jsx(Table, { sx: { '& td': { p: 1 } }, children: _jsxs(TableBody, { children: [_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.escalation') }) }), _jsx(TableCell, { children: _jsx(Chip, { size: "small", label: _case.escalation }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.created') }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: dayjs(_case.created).toString() }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.updated') }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: dayjs(_case.updated).toString() }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.sources') }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: _jsx(SourceAggregate, { case: _case }) }) })] })] }) })] })] })] }));
60
+ };
61
+ export default CaseDetails;
@@ -0,0 +1,7 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import { type FC } from 'react';
3
+ declare const CaseOverview: FC<{
4
+ case: Case;
5
+ updateCase: (_case: Partial<Case>) => Promise<void>;
6
+ }>;
7
+ export default CaseOverview;
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Clear, Edit, Save } from '@mui/icons-material';
3
+ import { Box, Card, CardContent, CardHeader, Divider, IconButton, LinearProgress, Skeleton, Stack, useTheme } from '@mui/material';
4
+ import Markdown from '@cccsaurora/howler-ui/components/elements/display/Markdown';
5
+ import MarkdownEditor from '@cccsaurora/howler-ui/components/elements/MarkdownEditor';
6
+ import { useEffect, useState } from 'react';
7
+ const CaseOverview = ({ case: _case, updateCase }) => {
8
+ const theme = useTheme();
9
+ const [editing, setEditing] = useState(false);
10
+ const [loading, setLoading] = useState(false);
11
+ const [overview, setOverview] = useState(_case?.overview);
12
+ useEffect(() => {
13
+ if (!editing && _case?.overview) {
14
+ setOverview(_case.overview);
15
+ }
16
+ }, [_case?.overview, editing]);
17
+ if (!_case) {
18
+ return _jsx(Skeleton, { height: 370 });
19
+ }
20
+ return (_jsxs(Card, { children: [_jsx(CardHeader, { title: _case.title, subheader: _case.summary }), _jsxs(Stack, { children: [_jsx(Divider, {}), _jsx(LinearProgress, { sx: { opacity: +loading } })] }), _jsx(CardContent, { sx: { position: 'relative' }, children: _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Box, { flex: 1, sx: {
21
+ '& > :first-child': {
22
+ marginTop: '0 !important'
23
+ },
24
+ '& > h1,h2,h3,h4,h5': {
25
+ fontSize: theme.typography.h5.fontSize
26
+ }
27
+ }, children: editing ? (_jsx(MarkdownEditor, { height: "40vh", content: overview, setContent: _content => setOverview(_content) })) : (_jsx(Markdown, { md: _case.overview })) }), _jsxs(Stack, { spacing: 1, children: [_jsx(IconButton, { size: "small", disabled: loading, onClick: async () => {
28
+ if (editing) {
29
+ try {
30
+ setLoading(true);
31
+ await updateCase({ overview });
32
+ }
33
+ finally {
34
+ setEditing(false);
35
+ setLoading(false);
36
+ }
37
+ }
38
+ else {
39
+ setEditing(true);
40
+ }
41
+ }, children: editing ? _jsx(Save, { color: loading ? 'disabled' : 'success', fontSize: "small" }) : _jsx(Edit, { fontSize: "small" }) }), editing && (_jsx(IconButton, { size: "small", disabled: loading, onClick: () => setEditing(false), children: _jsx(Clear, { color: loading ? 'disabled' : 'error', fontSize: "small" }) }))] })] }) })] }));
42
+ };
43
+ export default CaseOverview;
@@ -0,0 +1,6 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import { type FC } from 'react';
3
+ declare const CaseSidebar: FC<{
4
+ case: Case;
5
+ }>;
6
+ export default CaseSidebar;