@cccsaurora/howler-ui 2.17.0-dev.502 → 2.17.0-dev.513

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 (131) 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 +6 -0
  14. package/api/v2/search/index.js +18 -0
  15. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  16. package/components/app/App.js +14 -0
  17. package/components/app/providers/FavouritesProvider.js +2 -2
  18. package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
  19. package/components/elements/{hit/HitDetails.d.ts → ObjectDetails.d.ts} +2 -1
  20. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +14 -14
  21. package/components/elements/PluginTypography.d.ts +2 -1
  22. package/components/elements/PluginTypography.js +3 -2
  23. package/components/elements/UserList.d.ts +5 -2
  24. package/components/elements/UserList.js +14 -5
  25. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  26. package/components/elements/display/HowlerCard.js +1 -1
  27. package/components/elements/hit/HitBanner.js +19 -31
  28. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  29. package/components/elements/view/ViewTitle.js +1 -1
  30. package/components/hooks/useHitSelection.js +1 -35
  31. package/components/hooks/useMyPreferences.js +10 -1
  32. package/components/hooks/useMySitemap.js +3 -1
  33. package/components/hooks/useMyTheme.js +9 -2
  34. package/components/routes/action/view/ActionSearch.js +1 -1
  35. package/components/routes/action/view/Integrations.js +1 -9
  36. package/components/routes/advanced/QueryBuilder.js +1 -1
  37. package/components/routes/analytics/AnalyticDetails.js +2 -2
  38. package/components/routes/analytics/AnalyticSearch.js +1 -1
  39. package/components/routes/cases/CaseCard.d.ts +8 -0
  40. package/components/routes/cases/CaseCard.js +34 -0
  41. package/components/routes/cases/CaseViewer.d.ts +2 -0
  42. package/components/routes/cases/CaseViewer.js +24 -0
  43. package/components/routes/cases/Cases.d.ts +2 -0
  44. package/components/routes/cases/Cases.js +101 -0
  45. package/components/routes/cases/constants.d.ts +5 -0
  46. package/components/routes/cases/constants.js +5 -0
  47. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  48. package/components/routes/cases/detail/AlertPanel.js +32 -0
  49. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  50. package/components/routes/cases/detail/CaseDashboard.js +46 -0
  51. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  52. package/components/routes/cases/detail/CaseDetails.js +49 -0
  53. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  54. package/components/routes/cases/detail/CaseOverview.js +43 -0
  55. package/components/routes/cases/detail/CaseSidebar.d.ts +6 -0
  56. package/components/routes/cases/detail/CaseSidebar.js +36 -0
  57. package/components/routes/cases/detail/CaseTask.d.ts +10 -0
  58. package/components/routes/cases/detail/CaseTask.js +46 -0
  59. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  60. package/components/routes/cases/detail/ItemPage.js +93 -0
  61. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  62. package/components/routes/cases/detail/RelatedCasePanel.js +31 -0
  63. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  64. package/components/routes/cases/detail/TaskPanel.js +23 -0
  65. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +12 -0
  66. package/components/routes/cases/detail/aggregates/CaseAggregate.js +19 -0
  67. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  68. package/components/routes/cases/detail/aggregates/SourceAggregate.js +27 -0
  69. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +12 -0
  70. package/components/routes/cases/detail/sidebar/CaseFolder.js +114 -0
  71. package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
  72. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  73. package/components/routes/cases/hooks/useCase.js +38 -0
  74. package/components/routes/help/ApiDocumentation.js +1 -1
  75. package/components/routes/help/HitDocumentation.js +1 -3
  76. package/components/routes/hits/search/HitContextMenu.js +4 -27
  77. package/components/routes/hits/search/HitContextMenu.test.js +0 -140
  78. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  79. package/components/routes/hits/search/InformationPane.js +6 -29
  80. package/components/routes/hits/search/SearchPane.js +3 -5
  81. package/components/routes/hits/search/ViewLink.js +1 -1
  82. package/components/routes/hits/search/grid/EnhancedCell.js +1 -1
  83. package/components/routes/hits/view/HitViewer.js +3 -4
  84. package/components/routes/home/ViewCard.js +1 -1
  85. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  86. package/components/routes/observables/ObservableViewer.js +27 -0
  87. package/components/routes/overviews/OverviewViewer.js +2 -2
  88. package/locales/en/translation.json +422 -397
  89. package/locales/fr/translation.json +429 -406
  90. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  91. package/models/entities/generated/Case.d.ts +28 -0
  92. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  93. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  94. package/models/entities/generated/EmailParent.d.ts +19 -0
  95. package/models/entities/generated/Enrichments.d.ts +7 -0
  96. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  97. package/models/entities/generated/Howler.d.ts +0 -4
  98. package/models/entities/generated/HttpResponse.d.ts +11 -0
  99. package/models/entities/generated/Item.d.ts +9 -0
  100. package/models/entities/generated/Observable.d.ts +84 -0
  101. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  102. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  103. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  104. package/models/entities/generated/ObservableFile.d.ts +36 -0
  105. package/models/entities/generated/ObservableHowler.d.ts +44 -0
  106. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  107. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  108. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  109. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  110. package/models/entities/generated/ObservableSource.d.ts +23 -0
  111. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  112. package/models/entities/generated/ObservableTls.d.ts +12 -0
  113. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  114. package/models/entities/generated/Rule.d.ts +2 -10
  115. package/models/entities/generated/Task.d.ts +10 -0
  116. package/models/entities/generated/Threat.d.ts +2 -2
  117. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  118. package/package.json +125 -114
  119. package/plugins/clue/components/ClueTypography.js +2 -2
  120. package/plugins/clue/utils.d.ts +2 -1
  121. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  122. package/components/elements/display/icons/BundleButton.js +0 -32
  123. package/components/routes/action/view/markdown/integrations.en.md.js +0 -1
  124. package/components/routes/action/view/markdown/integrations.fr.md.js +0 -1
  125. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  126. package/components/routes/help/BundleDocumentation.js +0 -12
  127. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  128. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  129. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  130. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  131. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Api, Article, Book, Code, Dashboard, Description, Edit, ExitToApp, FormatListBulleted, Help, HelpCenter, Key, ManageSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, SupervisorAccount, Terminal, Topic } from '@mui/icons-material';
2
+ import { Api, Article, Book, BookRounded, Code, Dashboard, Description, Edit, ExitToApp, FormatListBulleted, Help, HelpCenter, Key, ManageSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, SupervisorAccount, Terminal, Topic } from '@mui/icons-material';
3
3
  import { AppBrand } from '@cccsaurora/howler-ui/branding/AppBrand';
4
4
  import Classification from '@cccsaurora/howler-ui/components/elements/display/Classification';
5
5
  import DocumentationButton from '@cccsaurora/howler-ui/components/elements/display/DocumentationButton';
@@ -21,6 +21,15 @@ const useMyPreferences = () => {
21
21
  icon: _jsx(Dashboard, {})
22
22
  }
23
23
  },
24
+ {
25
+ type: 'item',
26
+ element: {
27
+ id: 'cases',
28
+ i18nKey: 'route.cases',
29
+ route: '/cases',
30
+ icon: _jsx(BookRounded, {})
31
+ }
32
+ },
24
33
  {
25
34
  type: 'group',
26
35
  element: {
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Article, Book, Code, CreateNewFolder, Dashboard, Description, Edit, EditNote, FormatListBulleted, Help, Info, Key, Person, PersonSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, Terminal, Topic, Work } from '@mui/icons-material';
2
+ import { Article, Book, BookRounded, Code, CreateNewFolder, Dashboard, Description, Edit, EditNote, FormatListBulleted, Help, Info, Key, Person, PersonSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, Terminal, Topic, Work } from '@mui/icons-material';
3
3
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
4
4
  import { useMemo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
@@ -24,6 +24,8 @@ const useMySitemap = () => {
24
24
  return useMemo(() => ({
25
25
  routes: [
26
26
  { path: '/', title: t('route.home'), isRoot: true, icon: _jsx(Dashboard, {}) },
27
+ { path: '/cases', title: t('route.cases'), isRoot: true, icon: _jsx(BookRounded, {}) },
28
+ { path: '/cases/:id', title: t('route.cases.view') },
27
29
  { path: '/admin/users', title: t('route.admin.user.search'), isRoot: true, icon: _jsx(PersonSearch, {}) },
28
30
  {
29
31
  path: '/admin/users/:id',
@@ -1,9 +1,16 @@
1
1
  const DEFAULT_THEME = {
2
+ components: {
3
+ MuiChip: {
4
+ defaultProps: {
5
+ size: 'small'
6
+ }
7
+ }
8
+ },
2
9
  palette: {
3
10
  dark: {
4
11
  background: {
5
- default: '#202020',
6
- paper: '#202020'
12
+ default: '#181818',
13
+ paper: '#181818'
7
14
  },
8
15
  primary: {
9
16
  main: '#7DA1DB'
@@ -109,7 +109,7 @@ const ActionSearch = () => {
109
109
  e.stopPropagation();
110
110
  await deleteAction(item.item.action_id);
111
111
  onSearch();
112
- }, children: _jsx(Delete, {}) })), _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important' }, userId: item.item.owner_id })] }), subheader: item.item.query }), _jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsx(Grid, { container: true, spacing: 1, children: item.item.operations.map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { size: "small", label: t(`operations.${d.operation_id}`) }) }, d.operation_id))) }) })] }, item.item.name));
112
+ }, children: _jsx(Delete, {}) })), _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important' }, userId: item.item.owner_id })] }), subheader: item.item.query }), _jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsx(Grid, { container: true, spacing: 1, children: item.item.operations.map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { label: t(`operations.${d.operation_id}`) }) }, d.operation_id))) }) })] }, item.item.name));
113
113
  }, [deleteAction, navigate, onSearch, t, user.roles, user.username]);
114
114
  return (_jsx(ItemManager, { onSearch: onSearch, onCreate: () => navigate('/action/execute'), onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.actions.search.prompt') }), searchFilters: _jsx(Autocomplete, { multiple: true, size: "small", value: searchModifiers, onChange: (__, values) => setSearchModifiers(values), getOptionLabel: trigger => t(`route.actions.trigger.${trigger}`), options: VALID_ACTION_TRIGGERS, renderInput: params => (_jsx(TextField, { ...params, sx: { maxWidth: '500px' }, label: t('route.actions.trigger') })) }), renderer: renderer, response: response, createPrompt: "route.actions.create", searchPrompt: "route.actions.search", createIcon: _jsx(Terminal, { sx: { mr: 1 } }) }));
115
115
  };
@@ -1,33 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Stack, Tab, Tabs } from '@mui/material';
3
3
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
4
- import Markdown from '@cccsaurora/howler-ui/components/elements/display/Markdown';
5
- import { isEmpty } from 'lodash-es';
6
4
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
7
5
  import { useEffect, useMemo, useState } from 'react';
8
6
  import { useTranslation } from 'react-i18next';
9
7
  import { usePluginStore } from 'react-pluggable';
10
8
  import { useSearchParams } from 'react-router-dom';
11
- import { default as INTEGRATIONS_EN, default as INTEGRATIONS_FR } from './markdown/integrations.en.md';
12
9
  const Integrations = () => {
13
10
  const { t } = useTranslation();
14
- const { i18n } = useTranslation();
15
11
  const [searchParams, setSearchParams] = useSearchParams();
16
12
  const pluginStore = usePluginStore();
17
13
  const pluginIntegrations = useMemo(() => Object.fromEntries(howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.integrations`))), [pluginStore]);
18
14
  const [tab, setTab] = useState(Object.keys(pluginIntegrations)[0] ?? '');
19
- const md = useMemo(() => (i18n.language === 'en' ? INTEGRATIONS_EN : INTEGRATIONS_FR), [i18n.language]);
20
15
  useEffect(() => {
21
16
  searchParams.set('tab', tab);
22
17
  setSearchParams(searchParams);
23
18
  // eslint-disable-next-line react-hooks/exhaustive-deps
24
19
  }, [tab]);
25
20
  const tabData = useMemo(() => {
26
- if (!tab) {
27
- return null;
28
- }
29
21
  return pluginIntegrations[tab]();
30
22
  }, [pluginIntegrations, tab]);
31
- return (_jsx(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: _jsxs(Stack, { spacing: 1, children: [!isEmpty(pluginIntegrations) && (_jsx(Tabs, { value: tab, onChange: (_, _tab) => setTab(_tab), children: Object.keys(pluginIntegrations).map(integration => (_jsx(Tab, { label: t(`route.integrations.${integration}`), value: integration }, integration))) })), tabData ?? _jsx(Markdown, { md: md })] }) }));
23
+ return (_jsx(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: _jsxs(Stack, { spacing: 1, children: [_jsx(Tabs, { value: tab, onChange: (_, _tab) => setTab(_tab), children: Object.keys(pluginIntegrations).map(integration => (_jsx(Tab, { label: t(`route.integrations.${integration}`), value: integration }, integration))) }), tabData] }) }));
32
24
  };
33
25
  export default Integrations;
@@ -233,7 +233,7 @@ const QueryBuilder = () => {
233
233
  height: '100%',
234
234
  '& .MuiFormControl-root': { height: '100%', '& > div': { height: '100%' } }
235
235
  }
236
- ], slotProps: { paper: { sx: { minWidth: '600px' } } } }), _jsx(Card, { variant: "outlined", sx: { flex: 1, maxWidth: '350px', minWidth: '210px' }, children: _jsxs(Stack, { spacing: 0.5, sx: { px: 1, alignItems: 'start' }, children: [_jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: [t('route.advanced.rows'), ": ", STEPS[rows]] }), _jsx(Slider, { size: "small", valueLabelDisplay: "off", value: rows, onChange: (_, value) => setRows(value), min: 0, max: 9, step: 1, marks: true, track: false, sx: { py: 0.5 } })] }) })] }), type === 'lucene' && (_jsx(Autocomplete, { size: "small", getOptionLabel: opt => t(`route.advanced.query.type.${opt}`), options: LUCENE_QUERY_OPTIONS, value: queryType, onChange: (_event, value) => setQueryType(value), renderInput: params => (_jsx(TextField, { ...params, label: t('route.advanced.query.lucene.type'), sx: { minWidth: '230px' } })), renderOption: (props, option) => (_jsx(ListItemText, { ...props, sx: { flexDirection: 'column', alignItems: 'start !important' }, primary: t(`route.advanced.query.type.${option}`), secondary: t(`route.advanced.query.type.${option}.description`) })) })), queryType === 'groupby' && (_jsx(Autocomplete, { size: "small", options: fieldOptions, value: groupByField, onChange: (__, value) => setGroupByField(value), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.pivot.field') }), sx: { minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), allFields && queryType !== 'facet' ? (_jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: allFields, onChange: (__, checked) => setAllFields(checked) }), label: t('route.advanced.fields.all'), sx: { '& > span': { color: 'text.secondary' }, alignSelf: 'start' } })) : (_jsx(Autocomplete, { fullWidth: true, renderTags: values => values.length <= 3 ? (_jsx(Stack, { direction: "row", spacing: 0.5, children: values.map(_value => (_jsx(Chip, { size: "small", label: _value }, _value))) })) : (_jsx(Tooltip, { title: _jsx(Stack, { spacing: 1, children: values.map(_value => (_jsx("span", { children: _value }, _value))) }), children: _jsx(Chip, { size: "small", label: values.length }) })), multiple: true, size: "small", options: fieldOptions, value: fields, onChange: (__, values) => (values.length > 0 ? setFields(values) : setAllFields(true)), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.fields') }), sx: { maxWidth: '500px', width: '20vw', minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), _jsx(FlexOne, {}), type === 'lucene' &&
236
+ ], slotProps: { paper: { sx: { minWidth: '600px' } } } }), _jsx(Card, { variant: "outlined", sx: { flex: 1, maxWidth: '350px', minWidth: '210px' }, children: _jsxs(Stack, { spacing: 0.5, sx: { px: 1, alignItems: 'start' }, children: [_jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: [t('route.advanced.rows'), ": ", STEPS[rows]] }), _jsx(Slider, { size: "small", valueLabelDisplay: "off", value: rows, onChange: (_, value) => setRows(value), min: 0, max: 9, step: 1, marks: true, track: false, sx: { py: 0.5 } })] }) })] }), type === 'lucene' && (_jsx(Autocomplete, { size: "small", getOptionLabel: opt => t(`route.advanced.query.type.${opt}`), options: LUCENE_QUERY_OPTIONS, value: queryType, onChange: (_event, value) => setQueryType(value), renderInput: params => (_jsx(TextField, { ...params, label: t('route.advanced.query.lucene.type'), sx: { minWidth: '230px' } })), renderOption: (props, option) => (_jsx(ListItemText, { ...props, sx: { flexDirection: 'column', alignItems: 'start !important' }, primary: t(`route.advanced.query.type.${option}`), secondary: t(`route.advanced.query.type.${option}.description`) })) })), queryType === 'groupby' && (_jsx(Autocomplete, { size: "small", options: fieldOptions, value: groupByField, onChange: (__, value) => setGroupByField(value), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.pivot.field') }), sx: { minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), allFields && queryType !== 'facet' ? (_jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: allFields, onChange: (__, checked) => setAllFields(checked) }), label: t('route.advanced.fields.all'), sx: { '& > span': { color: 'text.secondary' }, alignSelf: 'start' } })) : (_jsx(Autocomplete, { fullWidth: true, renderTags: values => values.length <= 3 ? (_jsx(Stack, { direction: "row", spacing: 0.5, children: values.map(_value => (_jsx(Chip, { label: _value }, _value))) })) : (_jsx(Tooltip, { title: _jsx(Stack, { spacing: 1, children: values.map(_value => (_jsx("span", { children: _value }, _value))) }), children: _jsx(Chip, { label: values.length }) })), multiple: true, size: "small", options: fieldOptions, value: fields, onChange: (__, values) => (values.length > 0 ? setFields(values) : setAllFields(true)), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.fields') }), sx: { maxWidth: '500px', width: '20vw', minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), _jsx(FlexOne, {}), type === 'lucene' &&
237
237
  (smallButtons ? (_jsx(Tooltip, { title: t('route.advanced.open'), children: _jsx(IconButton, { color: "primary", sx: { alignSelf: 'center' }, component: Link, disabled: !response, to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}`, children: _jsx(OpenInNew, { fontSize: "small" }) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", startIcon: _jsx(OpenInNew, {}), component: Link, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, children: t('route.advanced.open') }))), smallButtons ? (_jsx(Tooltip, { title: response ? t('route.advanced.create.rule') : t('route.advanced.create.rule.disabled'), children: _jsx(IconButton, { size: "small", sx: { alignSelf: 'center' }, color: "info", onClick: onCreateRule, disabled: !response, children: _jsx(SsidChart, {}) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", color: "info", startIcon: _jsx(SsidChart, {}), onClick: onCreateRule, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, tooltip: !response && t('route.advanced.create.rule.disabled'), children: t('route.advanced.create.rule') }))] }), _jsxs(Box, { width: "100%", height: "calc(100vh - 112px)", sx: { position: 'relative', overflow: 'hidden', borderTop: `thin solid ${theme.palette.divider}` }, children: [_jsx(Box, { sx: {
238
238
  position: 'absolute',
239
239
  top: 0,
@@ -51,7 +51,7 @@ const AnalyticDetails = () => {
51
51
  _setFilter(detection);
52
52
  }
53
53
  }, [filter]);
54
- const onOwnerChange = useCallback(async (ownerId) => {
54
+ const onOwnerChange = useCallback(async ([ownerId]) => {
55
55
  const result = await dispatchApi(api.analytic.owner.post(analytic.analytic_id, { username: ownerId }), {
56
56
  throwError: true,
57
57
  showError: true
@@ -108,7 +108,7 @@ const AnalyticDetails = () => {
108
108
  marginTop: '0 !important',
109
109
  marginLeft: `${theme.spacing(-1)} !important`,
110
110
  marginRight: `${theme.spacing(-1)} !important`
111
- }, userId: analytic?.owner, onChange: onOwnerChange, i18nLabel: "route.analytics.set.owner" })) : (_jsx(HowlerAvatar, { userId: analytic?.owner })), _jsx(Stack, { children: users[analytic?.owner] ? (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body1", children: users[analytic?.owner].name }), _jsx(Typography, { component: "a", href: `mailto:${users[analytic?.owner].email}`, variant: "caption", color: "text.secondary", children: users[analytic?.owner].email })] })) : (_jsxs(_Fragment, { children: [_jsx(Skeleton, { variant: "text", width: "70px" }), _jsx(Skeleton, { variant: "text", width: "60px" })] })) })] })] }), filteredContributors.length > 0 && (_jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { variant: "body1", color: "text.secondary", children: t('route.analytics.contributors') }), _jsx(Stack, { direction: "row", alignItems: "center", spacing: 1, children: filteredContributors.map(_user => (_jsx(HowlerAvatar, { userId: _user }, _user))) })] })), analytic?.rule_crontab && (_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Stack, { spacing: 1, justifyContent: "space-between", children: [_jsx(Typography, { variant: "body1", color: "text.secondary", children: t('rule.interval') }), editingInterval ? (_jsxs(FormControl, { sx: { minWidth: '200px' }, children: [_jsx(InputLabel, { children: t('rule.interval') }), _jsx(Select, { size: "small", label: t('rule.interval'), onChange: event => setCrontab(event.target.value), value: crontab, children: RULE_INTERVALS.map(interval => (_jsx(MenuItem, { value: interval.crontab, children: t(interval.key) }, interval.key))) })] })) : (_jsx("code", { style: {
111
+ }, userIds: [analytic?.owner], onChange: onOwnerChange, i18nLabel: "route.analytics.set.owner" })) : (_jsx(HowlerAvatar, { userId: analytic?.owner })), _jsx(Stack, { children: users[analytic?.owner] ? (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body1", children: users[analytic?.owner].name }), _jsx(Typography, { component: "a", href: `mailto:${users[analytic?.owner].email}`, variant: "caption", color: "text.secondary", children: users[analytic?.owner].email })] })) : (_jsxs(_Fragment, { children: [_jsx(Skeleton, { variant: "text", width: "70px" }), _jsx(Skeleton, { variant: "text", width: "60px" })] })) })] })] }), filteredContributors.length > 0 && (_jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { variant: "body1", color: "text.secondary", children: t('route.analytics.contributors') }), _jsx(Stack, { direction: "row", alignItems: "center", spacing: 1, children: filteredContributors.map(_user => (_jsx(HowlerAvatar, { userId: _user }, _user))) })] })), analytic?.rule_crontab && (_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Stack, { spacing: 1, justifyContent: "space-between", children: [_jsx(Typography, { variant: "body1", color: "text.secondary", children: t('rule.interval') }), editingInterval ? (_jsxs(FormControl, { sx: { minWidth: '200px' }, children: [_jsx(InputLabel, { children: t('rule.interval') }), _jsx(Select, { size: "small", label: t('rule.interval'), onChange: event => setCrontab(event.target.value), value: crontab, children: RULE_INTERVALS.map(interval => (_jsx(MenuItem, { value: interval.crontab, children: t(interval.key) }, interval.key))) })] })) : (_jsx("code", { style: {
112
112
  backgroundColor: theme.palette.background.paper,
113
113
  padding: theme.spacing(0.5),
114
114
  alignSelf: 'start',
@@ -128,7 +128,7 @@ const AnalyticSearchBase = () => {
128
128
  padding: theme.spacing(0.5),
129
129
  borderRadius: theme.shape.borderRadius,
130
130
  border: `thin solid ${theme.palette.divider}`
131
- }, children: item.item.rule_type })] })), _jsx(FlexOne, {}), _jsxs(Stack, { direction: "row", spacing: 1, sx: { mt: 1 }, children: [item.item.owner && _jsx(HowlerAvatar, { sx: { width: 24, height: 24 }, userId: item.item.owner }), filteredContributors.length > 0 && _jsx(Divider, { orientation: "vertical", flexItem: true }), _jsx(AvatarGroup, { children: filteredContributors.map(contributor => (_jsx(HowlerAvatar, { sx: { width: 24, height: 24 }, userId: contributor }, contributor))) })] }), _jsx(Tooltip, { title: t('button.pin'), children: _jsx(IconButton, { size: "small", onClick: e => onFavourite(e, item.item), children: appUser.user?.favourite_analytics?.includes(item.item.analytic_id) ? _jsx(Star, {}) : _jsx(StarBorder, {}) }) })] }) }), item.item.detections?.length > 0 && (_jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsxs(Grid, { container: true, spacing: 0.5, sx: { marginTop: `${theme.spacing(-0.5)} !important` }, children: [item.item.detections.slice(0, 5).map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { size: "small", variant: "outlined", label: d }) }, d))), item.item.detections.length > 5 && (_jsx(Grid, { item: true, children: _jsx(Tooltip, { title: _jsx(Stack, { children: item.item.detections.slice(5).map(d => (_jsx("span", { children: d }, d))) }), children: _jsx(Chip, { size: "small", variant: "outlined", label: `+ ${item.item.detections.length - 5}` }) }) }))] }) }))] }, item.item.name));
131
+ }, children: item.item.rule_type })] })), _jsx(FlexOne, {}), _jsxs(Stack, { direction: "row", spacing: 1, sx: { mt: 1 }, children: [item.item.owner && _jsx(HowlerAvatar, { sx: { width: 24, height: 24 }, userId: item.item.owner }), filteredContributors.length > 0 && _jsx(Divider, { orientation: "vertical", flexItem: true }), _jsx(AvatarGroup, { children: filteredContributors.map(contributor => (_jsx(HowlerAvatar, { sx: { width: 24, height: 24 }, userId: contributor }, contributor))) })] }), _jsx(Tooltip, { title: t('button.pin'), children: _jsx(IconButton, { size: "small", onClick: e => onFavourite(e, item.item), children: appUser.user?.favourite_analytics?.includes(item.item.analytic_id) ? _jsx(Star, {}) : _jsx(StarBorder, {}) }) })] }) }), item.item.detections?.length > 0 && (_jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsxs(Grid, { container: true, spacing: 0.5, sx: { marginTop: `${theme.spacing(-0.5)} !important` }, children: [item.item.detections.slice(0, 5).map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { variant: "outlined", label: d }) }, d))), item.item.detections.length > 5 && (_jsx(Grid, { item: true, children: _jsx(Tooltip, { title: _jsx(Stack, { children: item.item.detections.slice(5).map(d => (_jsx("span", { children: d }, d))) }), children: _jsx(Chip, { variant: "outlined", label: `+ ${item.item.detections.length - 5}` }) }) }))] }) }))] }, item.item.name));
132
132
  }, [appUser.user?.favourite_analytics, navigate, onFavourite, t, theme]);
133
133
  return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, searchAdornment: _jsx(InputAdornment, { position: "end", children: _jsx(Tooltip, { title: t(`route.analytics.search.filter.rules.${onlyRules < 0 ? 'hide' : onlyRules > 0 ? 'show' : 'toggle'}`), children: _jsx(IconButton, { onClick: () => setOnlyRules((((onlyRules + 2) % 3) - 1)), children: _jsx(SsidChart, { color: onlyRules < 0 ? 'error' : onlyRules > 0 ? 'info' : 'inherit', sx: { transition: theme.transitions.create(['color']) } }) }) }) }), aboveSearch: _jsx(Typography, { sx: { fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }, variant: "body2", children: t('route.analytics.search.prompt') }), renderer: renderer, response: response, searchPrompt: "route.analytics.manager.search" }));
134
134
  };
@@ -0,0 +1,8 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import { type FC } from 'react';
3
+ declare const CaseCard: FC<{
4
+ case?: Case;
5
+ caseId?: string;
6
+ className?: string;
7
+ }>;
8
+ export default CaseCard;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { CheckCircleOutline, HourglassBottom, RadioButtonUnchecked, UpdateOutlined } from '@mui/icons-material';
3
+ import { Card, Chip, Divider, Grid, Skeleton, Stack, Tooltip, Typography } from '@mui/material';
4
+ import api from '@cccsaurora/howler-ui/api';
5
+ import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
6
+ import PluginChip from '@cccsaurora/howler-ui/components/elements/PluginChip';
7
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
8
+ import dayjs from 'dayjs';
9
+ import { countBy } from 'lodash-es';
10
+ import { useEffect, useState } from 'react';
11
+ import { useTranslation } from 'react-i18next';
12
+ import { twitterShort } from '@cccsaurora/howler-ui/utils/utils';
13
+ const CaseCard = ({ case: providedCase, caseId, className }) => {
14
+ const { t } = useTranslation();
15
+ const { dispatchApi } = useMyApi();
16
+ const [_case, setCase] = useState(providedCase);
17
+ useEffect(() => {
18
+ if (providedCase) {
19
+ setCase(providedCase);
20
+ }
21
+ }, [providedCase]);
22
+ useEffect(() => {
23
+ if (caseId) {
24
+ dispatchApi(api.v2.case.get(caseId), { throwError: false }).then(setCase);
25
+ }
26
+ }, [caseId, dispatchApi]);
27
+ if (!_case) {
28
+ return _jsx(Skeleton, { variant: "rounded", height: 250, sx: { mb: 1 }, className: className });
29
+ }
30
+ return (_jsx(Card, { variant: "outlined", sx: { p: 1, mb: 1 }, className: className, children: _jsx(Stack, { direction: "row", alignItems: "start", spacing: 1, children: _jsxs(Stack, { sx: { flex: 1 }, spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Typography, { variant: "h6", display: "flex", alignItems: "start", flex: 1, children: _case.title }), _case.start && _case.end && (_jsx(Tooltip, { title: dayjs(_case.updated).toString(), children: _jsx(Chip, { icon: _jsx(HourglassBottom, { fontSize: "small" }), size: "small", label: twitterShort(_case.start) + ' - ' + twitterShort(_case.end) }) })), _jsx(Tooltip, { title: dayjs(_case.updated).toString(), children: _jsx(Chip, { icon: _jsx(UpdateOutlined, { fontSize: "small" }), size: "small", label: twitterShort(_case.updated) }) })] }), _jsx(Typography, { variant: "caption", color: "textSecondary", children: _case.summary.trim().split('\n')[0] }), _case.participants?.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Divider, { flexItem: true }), _jsx(Stack, { direction: "row", spacing: 1, children: _case.participants?.map(participant => (_jsx(HowlerAvatar, { sx: { height: '20px', width: '20px' }, userId: participant }, participant))) })] })), _jsx(Divider, { flexItem: true }), _jsxs(Grid, { container: true, spacing: 1, children: [_case.targets?.map(indicator => (_jsx(Grid, { item: true, children: _jsx(PluginChip, { size: "small", color: "primary", context: "casecard", variant: "outlined", value: indicator, label: indicator }) }, indicator))), _case.targets?.length > 0 && (_case.indicators?.length > 0 || _case.threats?.length > 0) && (_jsx(Grid, { item: true, children: _jsx(Divider, { orientation: "vertical" }) })), _case.indicators?.map(indicator => (_jsx(Grid, { item: true, children: _jsx(PluginChip, { variant: "outlined", context: "casecard", value: indicator, label: indicator }) }, indicator))), _case.indicators?.length > 0 && _case.threats?.length > 0 && (_jsx(Grid, { item: true, children: _jsx(Divider, { orientation: "vertical" }) })), _case.threats?.map(indicator => (_jsx(Grid, { item: true, children: _jsx(PluginChip, { size: "small", color: "warning", variant: "outlined", context: "casecard", value: indicator, label: indicator }) }, indicator)))] }), _case.tasks?.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Divider, { flexItem: true }), _jsxs(Stack, { spacing: 0.5, alignItems: "start", children: [_case.tasks.some(task => task.complete) && (_jsx(Chip, { size: "small", color: "success", icon: _jsx(CheckCircleOutline, {}), label: `${countBy(_case.tasks, task => task.complete).true} ${t('complete')}` })), _case.tasks
31
+ .filter(task => !task.complete)
32
+ .map(task => (_jsx(Chip, { icon: _jsx(RadioButtonUnchecked, {}), label: task.summary }, task.id)))] })] }))] }) }) }, _case.case_id));
33
+ };
34
+ export default CaseCard;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("react").NamedExoticComponent<{}>;
2
+ export default _default;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Stack } from '@mui/material';
3
+ import { memo } from 'react';
4
+ import { useParams } from 'react-router-dom';
5
+ import NotFoundPage from '../404';
6
+ import ErrorBoundary from '../ErrorBoundary';
7
+ import CaseDashboard from './detail/CaseDashboard';
8
+ import CaseDetails from './detail/CaseDetails';
9
+ import CaseSidebar from './detail/CaseSidebar';
10
+ import ItemPage from './detail/ItemPage';
11
+ import useCase from './hooks/useCase';
12
+ const CaseViewer = () => {
13
+ const params = useParams();
14
+ const { case: _case, missing } = useCase({ caseId: params.id });
15
+ if (missing) {
16
+ return _jsx(NotFoundPage, {});
17
+ }
18
+ return (_jsxs(Stack, { direction: "row", height: "100%", children: [_jsx(CaseSidebar, { case: _case }), _jsx(Box, { sx: {
19
+ maxHeight: 'calc(100vh - 64px)',
20
+ flex: 1,
21
+ overflow: 'auto'
22
+ }, children: _jsx(ErrorBoundary, { children: !_case || location.pathname.endsWith(_case.case_id) ? (_jsx(CaseDashboard, { case: _case })) : (_jsx(ItemPage, { case: _case })) }) }), _jsx(CaseDetails, { case: _case })] }));
23
+ };
24
+ export default memo(CaseViewer);
@@ -0,0 +1,2 @@
1
+ declare const Cases: () => import("react/jsx-runtime").JSX.Element;
2
+ export default Cases;
@@ -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 './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,32 @@
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[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: {
18
+ position: 'absolute',
19
+ top: 0,
20
+ left: 0,
21
+ width: '100%',
22
+ height: '100%',
23
+ cursor: 'pointer',
24
+ zIndex: 100,
25
+ borderRadius: '4px',
26
+ '&:hover': {
27
+ background: theme.palette.divider,
28
+ border: `thin solid ${theme.palette.primary.light}`
29
+ }
30
+ } })] }, item.id)))] }));
31
+ };
32
+ export default AlertPanel;
@@ -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,46 @@
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 useCase from '../hooks/useCase';
10
+ import CaseAggregate from './aggregates/CaseAggregate';
11
+ import AlertPanel from './AlertPanel';
12
+ import CaseOverview from './CaseOverview';
13
+ import RelatedCasePanel from './RelatedCasePanel';
14
+ import TaskPanel from './TaskPanel';
15
+ const AGGREGATE_FIELDS = [
16
+ ['howler.outline.threat', 'material-symbols:warning-rounded', 'warning.main', 'page.cases.dashboard.threat'],
17
+ ['howler.outline.target', 'material-symbols:group', 'primary.main', 'page.cases.dashboard.target'],
18
+ ['howler.outline.indicators', 'fluent:number-symbol-24-filled', null, 'page.cases.dashboard.indicators']
19
+ ];
20
+ const getDuration = (case_) => {
21
+ if (case_?.start) {
22
+ return dayjs.duration(dayjs(case_?.end ?? new Date()).diff(dayjs(case_.start), 'minute'), 'minute');
23
+ }
24
+ };
25
+ const CaseDashboard = ({ case: providedCase, caseId }) => {
26
+ const { t } = useTranslation();
27
+ const { dispatchApi } = useMyApi();
28
+ const theme = useTheme();
29
+ const { case: _case, updateCase } = useCase({ case: providedCase, caseId });
30
+ const [records, setRecords] = useState(null);
31
+ const ids = useMemo(() => (_case?.items ?? []).filter(item => ['hit', 'observable'].includes(item.type)).map(item => item.id), [_case?.items]);
32
+ useEffect(() => {
33
+ if (ids?.length < 1) {
34
+ return;
35
+ }
36
+ dispatchApi(api.v2.search.post(['hit', 'observable'], {
37
+ query: `howler.id:(${ids?.join(' OR ') || '*'})`,
38
+ fl: AGGREGATE_FIELDS.map(([field]) => field).join(',')
39
+ })).then(response => setRecords(response.items));
40
+ }, [dispatchApi, ids]);
41
+ if (!_case) {
42
+ return null;
43
+ }
44
+ return (_jsxs(Grid, { container: true, spacing: 5, width: "100%", px: 3, 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).format('HH[h] mm[m]'), 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 }) })] }));
45
+ };
46
+ 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,49 @@
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 UserList from '@cccsaurora/howler-ui/components/elements/UserList';
6
+ import dayjs from 'dayjs';
7
+ import { useContext, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import useCase from '../hooks/useCase';
10
+ import SourceAggregate from './aggregates/SourceAggregate';
11
+ const CaseDetails = ({ case: providedCase }) => {
12
+ const { t } = useTranslation();
13
+ const { case: _case, updateCase } = useCase({ case: providedCase });
14
+ const { config } = useContext(ApiConfigContext);
15
+ const [loading, setLoading] = useState(false);
16
+ const wrappedUpdate = async (subset) => {
17
+ try {
18
+ setLoading(true);
19
+ await updateCase(subset);
20
+ }
21
+ finally {
22
+ setLoading(false);
23
+ }
24
+ };
25
+ if (!_case) {
26
+ return (_jsx(Card, { sx: {
27
+ borderRadius: 0,
28
+ width: '300px',
29
+ maxHeight: 'calc(100vh - 64px)',
30
+ display: 'flex',
31
+ flexDirection: 'column',
32
+ p: 1
33
+ }, children: _jsx(Skeleton, { variant: "rounded", height: 50 }) }));
34
+ }
35
+ return (_jsxs(Card, { elevation: 1, sx: {
36
+ borderRadius: 0,
37
+ width: '300px',
38
+ maxHeight: 'calc(100vh - 64px)',
39
+ display: 'flex',
40
+ flexDirection: 'column',
41
+ p: 1,
42
+ position: 'relative'
43
+ }, 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: [{
44
+ 'in-progress': _jsx(HourglassBottom, { color: "warning" }),
45
+ closed: _jsx(Check, { color: "success" }),
46
+ 'on-hold': _jsx(Pause, { color: "disabled" })
47
+ }[_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) => wrappedUpdate({ 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 }) }) })] })] }) })] })] })] }));
48
+ };
49
+ 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;