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

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 (117) 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/index.d.ts +4 -0
  12. package/api/v2/search/index.js +16 -0
  13. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  14. package/components/app/App.js +14 -0
  15. package/components/app/providers/FavouritesProvider.js +2 -2
  16. package/components/elements/{hit/HitDetails.d.ts → ObjectDetails.d.ts} +2 -1
  17. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +14 -14
  18. package/components/elements/PluginTypography.d.ts +2 -1
  19. package/components/elements/PluginTypography.js +3 -2
  20. package/components/elements/UserList.d.ts +1 -0
  21. package/components/elements/UserList.js +2 -2
  22. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  23. package/components/elements/display/HowlerCard.js +1 -1
  24. package/components/elements/hit/HitBanner.js +19 -31
  25. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  26. package/components/elements/view/ViewTitle.js +1 -1
  27. package/components/hooks/useHitSelection.js +1 -35
  28. package/components/hooks/useMyPreferences.js +10 -1
  29. package/components/hooks/useMySitemap.js +3 -1
  30. package/components/hooks/useMyTheme.js +9 -2
  31. package/components/routes/action/view/ActionSearch.js +1 -1
  32. package/components/routes/action/view/Integrations.js +1 -9
  33. package/components/routes/advanced/QueryBuilder.js +1 -1
  34. package/components/routes/analytics/AnalyticSearch.js +1 -1
  35. package/components/routes/cases/CaseCard.d.ts +8 -0
  36. package/components/routes/cases/CaseCard.js +34 -0
  37. package/components/routes/cases/CaseViewer.d.ts +2 -0
  38. package/components/routes/cases/CaseViewer.js +38 -0
  39. package/components/routes/cases/Cases.d.ts +2 -0
  40. package/components/routes/cases/Cases.js +101 -0
  41. package/components/routes/cases/constants.d.ts +5 -0
  42. package/components/routes/cases/constants.js +5 -0
  43. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  44. package/components/routes/cases/detail/AlertPanel.js +29 -0
  45. package/components/routes/cases/detail/CaseAggregate.d.ts +10 -0
  46. package/components/routes/cases/detail/CaseAggregate.js +30 -0
  47. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  48. package/components/routes/cases/detail/CaseDashboard.js +49 -0
  49. package/components/routes/cases/detail/CaseSidebar.d.ts +6 -0
  50. package/components/routes/cases/detail/CaseSidebar.js +35 -0
  51. package/components/routes/cases/detail/CaseTask.d.ts +9 -0
  52. package/components/routes/cases/detail/CaseTask.js +38 -0
  53. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  54. package/components/routes/cases/detail/ItemPage.js +93 -0
  55. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  56. package/components/routes/cases/detail/RelatedCasePanel.js +28 -0
  57. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  58. package/components/routes/cases/detail/TaskPanel.js +20 -0
  59. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +12 -0
  60. package/components/routes/cases/detail/sidebar/CaseFolder.js +114 -0
  61. package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
  62. package/components/routes/help/ApiDocumentation.js +1 -1
  63. package/components/routes/help/HitDocumentation.js +1 -3
  64. package/components/routes/hits/search/HitContextMenu.js +4 -27
  65. package/components/routes/hits/search/HitContextMenu.test.js +0 -140
  66. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  67. package/components/routes/hits/search/InformationPane.js +6 -29
  68. package/components/routes/hits/search/SearchPane.js +3 -5
  69. package/components/routes/hits/search/ViewLink.js +1 -1
  70. package/components/routes/hits/search/grid/EnhancedCell.js +1 -1
  71. package/components/routes/hits/view/HitViewer.js +3 -4
  72. package/components/routes/home/ViewCard.js +1 -1
  73. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  74. package/components/routes/observables/ObservableViewer.js +27 -0
  75. package/locales/en/translation.json +413 -397
  76. package/locales/fr/translation.json +420 -406
  77. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  78. package/models/entities/generated/Case.d.ts +27 -0
  79. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  80. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  81. package/models/entities/generated/EmailParent.d.ts +19 -0
  82. package/models/entities/generated/Enrichments.d.ts +7 -0
  83. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  84. package/models/entities/generated/Howler.d.ts +0 -4
  85. package/models/entities/generated/HttpResponse.d.ts +11 -0
  86. package/models/entities/generated/Item.d.ts +9 -0
  87. package/models/entities/generated/Observable.d.ts +84 -0
  88. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  89. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  90. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  91. package/models/entities/generated/ObservableFile.d.ts +36 -0
  92. package/models/entities/generated/ObservableHowler.d.ts +44 -0
  93. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  94. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  95. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  96. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  97. package/models/entities/generated/ObservableSource.d.ts +23 -0
  98. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  99. package/models/entities/generated/ObservableTls.d.ts +12 -0
  100. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  101. package/models/entities/generated/Rule.d.ts +2 -10
  102. package/models/entities/generated/Task.d.ts +10 -0
  103. package/models/entities/generated/Threat.d.ts +2 -2
  104. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  105. package/package.json +11 -2
  106. package/plugins/clue/components/ClueTypography.js +2 -2
  107. package/plugins/clue/utils.d.ts +2 -1
  108. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  109. package/components/elements/display/icons/BundleButton.js +0 -32
  110. package/components/routes/action/view/markdown/integrations.en.md.js +0 -1
  111. package/components/routes/action/view/markdown/integrations.fr.md.js +0 -1
  112. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  113. package/components/routes/help/BundleDocumentation.js +0 -12
  114. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  115. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  116. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  117. package/components/routes/hits/search/BundleParentMenu.js +0 -32
@@ -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,
@@ -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,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Skeleton, Stack } from '@mui/material';
3
+ import api from '@cccsaurora/howler-ui/api';
4
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
5
+ import { memo, useEffect, useState } from 'react';
6
+ import { useParams } from 'react-router-dom';
7
+ import NotFoundPage from '../404';
8
+ import CaseDashboard from './detail/CaseDashboard';
9
+ import CaseSidebar from './detail/CaseSidebar';
10
+ import ItemPage from './detail/ItemPage';
11
+ const CaseViewer = () => {
12
+ const params = useParams();
13
+ const { dispatchApi } = useMyApi();
14
+ const [_case, setCase] = useState();
15
+ const [loading, setLoading] = useState(false);
16
+ const [notFound, setNotFound] = useState(false);
17
+ useEffect(() => {
18
+ if (!params.id) {
19
+ return;
20
+ }
21
+ setLoading(true);
22
+ dispatchApi(api.v2.case.get(params.id))
23
+ .then(_dossier => {
24
+ setCase(_dossier);
25
+ })
26
+ .catch(() => setNotFound(true))
27
+ .finally(() => setLoading(false));
28
+ }, [dispatchApi, params.id]);
29
+ if (notFound) {
30
+ return _jsx(NotFoundPage, {});
31
+ }
32
+ return (_jsxs(Stack, { direction: "row", height: "100%", children: [_jsx(CaseSidebar, { case: _case }), _jsxs(Box, { sx: {
33
+ maxHeight: 'calc(100vh - 64px)',
34
+ flex: 1,
35
+ overflow: 'auto'
36
+ }, children: [loading && _jsx(Skeleton, { variant: "rounded", height: 240 }), !_case || location.pathname.endsWith(_case.case_id) ? (_jsx(CaseDashboard, { case: _case })) : (_jsx(ItemPage, { case: _case }))] })] }));
37
+ };
38
+ 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,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Divider, Pagination, 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
+ 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: {
15
+ position: 'absolute',
16
+ top: 0,
17
+ left: 0,
18
+ width: '100%',
19
+ height: '100%',
20
+ cursor: 'pointer',
21
+ zIndex: 100,
22
+ borderRadius: '4px',
23
+ '&:hover': {
24
+ background: theme.palette.divider,
25
+ border: `thin solid ${theme.palette.primary.light}`
26
+ }
27
+ } })] }, item.id)))] }));
28
+ };
29
+ export default AlertPanel;
@@ -0,0 +1,10 @@
1
+ import { type FC } from 'react';
2
+ declare const CaseAggregate: FC<{
3
+ icon?: string;
4
+ iconColor?: string;
5
+ field?: string;
6
+ ids?: string[];
7
+ title?: string;
8
+ subtitle?: string;
9
+ }>;
10
+ export default CaseAggregate;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Icon } from '@iconify/react';
3
+ import { Card, CardContent, Stack, styled, Tooltip, tooltipClasses, Typography, useTheme } from '@mui/material';
4
+ import api from '@cccsaurora/howler-ui/api';
5
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
6
+ import { get, isEmpty, uniq } from 'lodash-es';
7
+ import { useEffect, useState } from 'react';
8
+ const NoMaxWidthTooltip = styled(({ className, ...props }) => (_jsx(Tooltip, { ...props, classes: { popper: className } })))({
9
+ [`& .${tooltipClasses.tooltip}`]: {
10
+ maxWidth: 'none'
11
+ }
12
+ });
13
+ const CaseAggregate = ({ icon, iconColor, field, ids, title, subtitle }) => {
14
+ const { dispatchApi } = useMyApi();
15
+ const theme = useTheme();
16
+ const [values, setValues] = useState([]);
17
+ useEffect(() => {
18
+ if (ids?.length < 1 || !field) {
19
+ return;
20
+ }
21
+ dispatchApi(api.v2.search.post(['hit', 'observable'], {
22
+ query: `howler.id:(${ids?.join(' OR ') || '*'})`,
23
+ fl: field
24
+ })).then(response => {
25
+ setValues(uniq(response.items.map(entry => get(entry, field)).flat()));
26
+ });
27
+ }, [dispatchApi, field, ids]);
28
+ return (_jsx(Card, { sx: { height: '100%' }, children: _jsx(CardContent, { children: _jsxs(Stack, { alignItems: "center", spacing: 1, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [icon && _jsx(Icon, { fontSize: "96px", icon: icon, color: iconColor || theme.palette.grey[700] }), _jsx(NoMaxWidthTooltip, { title: !isEmpty(values) && (_jsx(Stack, { spacing: 0.5, children: values.map(value => (_jsx("span", { children: value }, value))) })), children: _jsxs(Typography, { variant: "h3", children: [values.length, !isEmpty(values) && !!title && ' - ', title] }) })] }), _jsx(Typography, { color: "textSecondary", children: subtitle })] }) }) }));
29
+ };
30
+ export default CaseAggregate;
@@ -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,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Card, CardContent, CardHeader, Divider, Grid, useTheme } from '@mui/material';
3
+ import api from '@cccsaurora/howler-ui/api';
4
+ import Markdown from '@cccsaurora/howler-ui/components/elements/display/Markdown';
5
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
6
+ import dayjs from 'dayjs';
7
+ import { useCallback, useEffect, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import AlertPanel from './AlertPanel';
10
+ import CaseAggregate from './CaseAggregate';
11
+ import RelatedCasePanel from './RelatedCasePanel';
12
+ import TaskPanel from './TaskPanel';
13
+ const getDuration = (case_) => {
14
+ if (case_?.start) {
15
+ return dayjs.duration(dayjs(case_?.end ?? new Date()).diff(dayjs(case_.start), 'minute'), 'minute');
16
+ }
17
+ };
18
+ const CaseDashboard = ({ case: providedCase, caseId }) => {
19
+ const { t } = useTranslation();
20
+ const { dispatchApi } = useMyApi();
21
+ const theme = useTheme();
22
+ const [_case, setCase] = useState(providedCase);
23
+ useEffect(() => {
24
+ if (providedCase) {
25
+ setCase(providedCase);
26
+ }
27
+ }, [providedCase]);
28
+ useEffect(() => {
29
+ if (caseId) {
30
+ dispatchApi(api.v2.case.get(caseId), { throwError: false }).then(setCase);
31
+ }
32
+ }, [caseId, dispatchApi]);
33
+ const updateCase = useCallback(async (_updatedCase) => {
34
+ if (!_case?.case_id) {
35
+ return;
36
+ }
37
+ try {
38
+ setCase(await dispatchApi(api.v2.case.put(_case.case_id, _updatedCase)));
39
+ }
40
+ finally {
41
+ return;
42
+ }
43
+ }, [_case?.case_id, dispatchApi]);
44
+ if (!_case) {
45
+ return null;
46
+ }
47
+ return (_jsxs(Grid, { container: true, spacing: 5, width: "100%", px: 3, children: [_jsx(Grid, { item: true, xs: 12, children: _jsxs(Card, { children: [_jsx(CardHeader, { title: _case.title, subheader: _case.summary }), _jsx(Divider, {}), _jsx(CardContent, { children: _jsx(Markdown, { md: _case.overview }) })] }) }), _jsx(Grid, { item: true, xs: 12, md: 6, xl: 3, children: _jsx(CaseAggregate, { icon: "material-symbols:warning-rounded", iconColor: theme.palette.warning.main, field: "howler.outline.threat", ids: _case.items.filter(item => ['hit', 'observable'].includes(item.type)).map(item => item.id), subtitle: t('page.cases.dashboard.threat') }) }), _jsx(Grid, { item: true, xs: 12, md: 6, xl: 3, children: _jsx(CaseAggregate, { icon: "material-symbols:group", iconColor: theme.palette.primary.main, field: "howler.outline.target", ids: _case.items.filter(item => ['hit', 'observable'].includes(item.type)).map(item => item.id), subtitle: t('page.cases.dashboard.target') }) }), _jsx(Grid, { item: true, xs: 12, md: 6, xl: 3, children: _jsx(CaseAggregate, { icon: "fluent:number-symbol-24-filled", field: "howler.outline.indicators", ids: _case.items.filter(item => ['hit', 'observable'].includes(item.type)).map(item => item.id), subtitle: t('page.cases.dashboard.indicators') }) }), _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]'), ids: _case.items.filter(item => ['hit', 'observable'].includes(item.type)).map(item => item.id), 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 }) })] }));
48
+ };
49
+ 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 CaseSidebar: FC<{
4
+ case: Case;
5
+ }>;
6
+ export default CaseSidebar;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Circle, Dashboard } from '@mui/icons-material';
3
+ import { Box, Card, Chip, Divider, Skeleton, Stack, Typography, useTheme } from '@mui/material';
4
+ import dayjs from 'dayjs';
5
+ import {} from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Link } from 'react-router-dom';
8
+ import { ESCALATION_COLOR_MAP } from '../constants';
9
+ import CaseFolder from './sidebar/CaseFolder';
10
+ const CaseSidebar = ({ case: _case }) => {
11
+ const { t } = useTranslation();
12
+ const theme = useTheme();
13
+ return (_jsxs(Box, { sx: {
14
+ width: '350px',
15
+ maxHeight: 'calc(100vh - 64px)',
16
+ display: 'flex',
17
+ flexDirection: 'column'
18
+ }, children: [_jsxs(Card, { sx: { borderRadius: 0, px: 2, py: 1 }, children: [_case?.title ? _jsx(Typography, { variant: "body1", children: _case.title }) : _jsx(Skeleton, { height: 24 }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", divider: _jsx(Circle, { color: "disabled", sx: { fontSize: '8px' } }), children: [_jsxs(Typography, { variant: "caption", color: "textSecondary", children: [t('started'), ": ", _case?.created ? dayjs(_case.created).toString() : _jsx(Skeleton, { height: 14 })] }), _case?.escalation ? (_jsx(Chip, { color: ESCALATION_COLOR_MAP[_case.escalation], label: t(_case.escalation) })) : (_jsx(Skeleton, { height: 24 }))] })] }), _jsxs(Stack, { direction: "row", alignItems: "center", sx: {
19
+ cursor: 'pointer',
20
+ px: 1,
21
+ py: 1,
22
+ transition: theme.transitions.create('background', { duration: 100 }),
23
+ color: `${theme.palette.text.primary} !important`,
24
+ textDecoration: 'none',
25
+ background: 'transparent',
26
+ borderRight: `thin solid ${theme.palette.divider}`,
27
+ '&:hover': {
28
+ background: theme.palette.grey[800]
29
+ }
30
+ }, component: Link, to: `/cases/${_case?.case_id}`, children: [_jsx(Dashboard, {}), _jsx(Typography, { sx: { userSelect: 'none', pl: 0.5, textWrap: 'nowrap' }, children: t('page.cases.dashboard') })] }), _jsx(Divider, {}), _case && (_jsx(Box, { flex: 1, overflow: "auto", width: "100%", sx: {
31
+ position: 'relative',
32
+ borderRight: `thin solid ${theme.palette.divider}`
33
+ }, children: _jsx(Box, { position: "absolute", sx: { left: 0 }, children: _jsx(CaseFolder, { case: _case }) }) }))] }));
34
+ };
35
+ export default CaseSidebar;
@@ -0,0 +1,9 @@
1
+ import type { Task } from '@cccsaurora/howler-ui/models/entities/generated/Task';
2
+ import { type FC } from 'react';
3
+ declare const CaseTask: FC<{
4
+ task: Task;
5
+ paths: string[];
6
+ onDelete: () => void;
7
+ onEdit: (task: Partial<Task>) => Promise<void>;
8
+ }>;
9
+ export default CaseTask;
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Check, Close, Delete, Edit } from '@mui/icons-material';
3
+ import { Autocomplete, Card, Checkbox, Chip, IconButton, LinearProgress, Stack, TextField, Tooltip, Typography } from '@mui/material';
4
+ import UserList from '@cccsaurora/howler-ui/components/elements/UserList';
5
+ import { useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Link } from 'react-router-dom';
8
+ const CaseTask = ({ task, onEdit, onDelete, paths }) => {
9
+ const { t } = useTranslation();
10
+ const [editing, setEditing] = useState(false);
11
+ const [loading, setLoading] = useState(false);
12
+ const [summary, setSummary] = useState(task.summary);
13
+ const [path, setPath] = useState(task.path);
14
+ const dirty = summary !== task.summary || path !== task.path;
15
+ const onOwnerChange = async (assignment) => {
16
+ setLoading(true);
17
+ await onEdit({
18
+ assignment
19
+ });
20
+ setLoading(false);
21
+ };
22
+ const onSubmit = async () => {
23
+ if (dirty) {
24
+ setLoading(true);
25
+ await onEdit({ summary, path: !path ? null : path });
26
+ setLoading(false);
27
+ }
28
+ };
29
+ return (_jsxs(Card, { sx: { pl: 0.5, pr: 1, py: 0.5, position: 'relative' }, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Checkbox, { color: "success", checked: task.complete, size: "small", onChange: (_ev, complete) => onEdit({ complete }) }), editing ? (_jsx(TextField, { value: summary, onChange: e => setSummary(e.target.value), size: "small", fullWidth: true, sx: { minWidth: '40%' } })) : (_jsx(Typography, { sx: [task.complete && { textDecoration: 'line-through' }], children: task.summary })), task.path && !editing && _jsx(Chip, { clickable: true, component: Link, to: task.path, label: task.path }), editing && (_jsx(Autocomplete, { value: path, options: paths, onChange: (_ev, value) => setPath(value), fullWidth: true, renderInput: params => _jsx(TextField, { ...params, size: "small" }) })), task.assignment && (_jsx(UserList, { userId: task.assignment, onChange: onOwnerChange, i18nLabel: "route.cases.task.set.assignment", avatarHeight: 24 })), _jsx("div", { style: { flex: 1 } }), editing && (_jsx(Tooltip, { title: t('route.cases.task.delete'), children: _jsx(IconButton, { size: "small", color: "error", onClick: onDelete, children: _jsx(Delete, { fontSize: "small" }) }) })), _jsx(Tooltip, { title: t(editing ? 'route.cases.task.edit.save' : 'route.cases.task.edit'), children: _jsx(IconButton, { size: "small", color: editing ? 'success' : 'default', onClick: () => {
30
+ if (!editing) {
31
+ setEditing(true);
32
+ return;
33
+ }
34
+ setEditing(false);
35
+ onSubmit();
36
+ }, disabled: !dirty && editing, children: editing ? _jsx(Check, { fontSize: "small" }) : _jsx(Edit, { fontSize: "small" }) }) }), editing && (_jsx(Tooltip, { title: t('route.cases.task.edit.cancel'), children: _jsx(IconButton, { size: "small", onClick: () => setEditing(false), children: _jsx(Close, { fontSize: "small" }) }) }))] }), loading && _jsx(LinearProgress, { sx: { left: 0, bottom: 0, right: 0, position: 'absolute' } })] }, task.id));
37
+ };
38
+ export default CaseTask;
@@ -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 ItemPage: FC<{
4
+ case: Case;
5
+ }>;
6
+ export default ItemPage;
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import api from '@cccsaurora/howler-ui/api';
3
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
4
+ import NotFoundPage from '@cccsaurora/howler-ui/components/routes/404';
5
+ import InformationPane from '@cccsaurora/howler-ui/components/routes/hits/search/InformationPane';
6
+ import ObservableViewer from '@cccsaurora/howler-ui/components/routes/observables/ObservableViewer';
7
+ import { useEffect, useMemo, useState } from 'react';
8
+ import { useLocation } from 'react-router';
9
+ import CaseDashboard from './CaseDashboard';
10
+ const ItemPage = ({ case: _case }) => {
11
+ const location = useLocation();
12
+ const { dispatchApi } = useMyApi();
13
+ const [item, setItem] = useState(null);
14
+ const [loading, setLoading] = useState(true);
15
+ const subPath = decodeURIComponent(location.pathname).replace(`/cases/${_case.case_id}/`, '');
16
+ const normalizedSubPath = useMemo(() => subPath.replace(/^\/+|\/+$/g, ''), [subPath]);
17
+ useEffect(() => {
18
+ let cancelled = false;
19
+ const resolveItem = async () => {
20
+ setLoading(true);
21
+ if (!normalizedSubPath) {
22
+ if (!cancelled) {
23
+ setItem(null);
24
+ setLoading(false);
25
+ }
26
+ return;
27
+ }
28
+ let currentCase = _case;
29
+ let remainingPath = normalizedSubPath;
30
+ while (currentCase && remainingPath) {
31
+ const currentRemainingPath = remainingPath;
32
+ const matchedNestedCase = currentCase.items
33
+ .filter(_item => _item?.path &&
34
+ _item?.type?.toLowerCase() === 'case' &&
35
+ (currentRemainingPath === _item.path || currentRemainingPath.startsWith(`${_item.path}/`)))
36
+ .sort((a, b) => (b.path?.length || 0) - (a.path?.length || 0))[0];
37
+ if (!matchedNestedCase) {
38
+ break;
39
+ }
40
+ if (currentRemainingPath === matchedNestedCase.path) {
41
+ if (!cancelled) {
42
+ setItem(matchedNestedCase);
43
+ setLoading(false);
44
+ }
45
+ return;
46
+ }
47
+ if (!matchedNestedCase.id) {
48
+ if (!cancelled) {
49
+ setItem(null);
50
+ setLoading(false);
51
+ }
52
+ return;
53
+ }
54
+ const nextCase = await dispatchApi(api.v2.case.get(matchedNestedCase.id), { throwError: false });
55
+ if (!nextCase) {
56
+ if (!cancelled) {
57
+ setItem(null);
58
+ setLoading(false);
59
+ }
60
+ return;
61
+ }
62
+ remainingPath = currentRemainingPath.slice((matchedNestedCase.path?.length || 0) + 1);
63
+ currentCase = nextCase;
64
+ }
65
+ const resolvedItem = currentCase?.items?.find(_item => _item.path === remainingPath);
66
+ if (!cancelled) {
67
+ setItem(resolvedItem || null);
68
+ setLoading(false);
69
+ }
70
+ };
71
+ resolveItem();
72
+ return () => {
73
+ cancelled = true;
74
+ };
75
+ }, [_case, dispatchApi, normalizedSubPath]);
76
+ if (loading) {
77
+ return null;
78
+ }
79
+ if (!item) {
80
+ return _jsx(NotFoundPage, {});
81
+ }
82
+ if (item.type === 'hit') {
83
+ return _jsx(InformationPane, { selected: item.id });
84
+ }
85
+ if (item.type === 'observable') {
86
+ return _jsx(ObservableViewer, { observableId: item.id });
87
+ }
88
+ if (item.type === 'case') {
89
+ return _jsx(CaseDashboard, { caseId: item.id });
90
+ }
91
+ return _jsx("h1", { children: JSON.stringify(item) });
92
+ };
93
+ export default ItemPage;