@cccsaurora/howler-ui 2.17.0-dev.521 → 2.17.0-dev.523
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.
- package/components/elements/display/Modal.js +1 -0
- package/components/elements/hit/HitBanner.js +3 -17
- package/components/elements/hit/elements/AnalyticLink.d.ts +8 -0
- package/components/elements/hit/elements/AnalyticLink.js +22 -0
- package/components/hooks/useHitActions.d.ts +1 -1
- package/components/hooks/useHitActions.js +2 -2
- package/components/hooks/useMySitemap.js +2 -1
- package/components/routes/cases/detail/CaseDetails.js +15 -3
- package/components/routes/cases/detail/Untitled-1.md.js +1 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.js +39 -29
- package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
- package/components/routes/cases/modals/ResolveModal.js +56 -0
- package/locales/en/translation.json +2 -0
- package/package.json +2 -1
- package/utils/constants.d.ts +3 -3
|
@@ -15,6 +15,7 @@ const Modal = () => {
|
|
|
15
15
|
left: '50%',
|
|
16
16
|
maxWidth: options.maxWidth || '1200px',
|
|
17
17
|
maxHeight: options.maxHeight || '400px',
|
|
18
|
+
height: '100%',
|
|
18
19
|
transform: 'translate(-50%, -50%)',
|
|
19
20
|
backgroundColor: 'background.paper',
|
|
20
21
|
borderRadius: theme.shape.borderRadius,
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Chip, Divider, Grid, Stack, Tooltip, Typography, avatarClasses, iconButtonClasses, useTheme } from '@mui/material';
|
|
3
|
-
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
4
3
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
5
4
|
import { uniq } from 'lodash-es';
|
|
6
5
|
import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
|
|
7
|
-
import { useCallback, useContext,
|
|
6
|
+
import { useCallback, useContext, useMemo } from 'react';
|
|
8
7
|
import { Trans, useTranslation } from 'react-i18next';
|
|
9
8
|
import { usePluginStore } from 'react-pluggable';
|
|
10
|
-
import { Link } from 'react-router-dom';
|
|
11
9
|
import { ESCALATION_COLORS, PROVIDER_COLORS } from '@cccsaurora/howler-ui/utils/constants';
|
|
12
10
|
import { stringToColor } from '@cccsaurora/howler-ui/utils/utils';
|
|
13
11
|
import PluginTypography from '../PluginTypography';
|
|
12
|
+
import AnalyticLink from './elements/AnalyticLink';
|
|
14
13
|
import Assigned from './elements/Assigned';
|
|
15
14
|
import EscalationChip from './elements/EscalationChip';
|
|
16
15
|
import HitTimestamp from './elements/HitTimestamp';
|
|
@@ -21,17 +20,8 @@ const HitBanner = ({ hit, layout = HitLayout.NORMAL, showAssigned = true }) => {
|
|
|
21
20
|
const { config } = useContext(ApiConfigContext);
|
|
22
21
|
const theme = useTheme();
|
|
23
22
|
const pluginStore = usePluginStore();
|
|
24
|
-
const { getMatchingAnalytic } = useMatchers();
|
|
25
|
-
const [analyticId, setAnalyticId] = useState();
|
|
26
23
|
const compressed = useMemo(() => layout === HitLayout.DENSE, [layout]);
|
|
27
24
|
const textVariant = useMemo(() => (layout === HitLayout.COMFY ? 'body1' : 'caption'), [layout]);
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if (!hit?.howler.analytic) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
getMatchingAnalytic(hit).then(analytic => setAnalyticId(analytic?.analytic_id));
|
|
33
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
|
-
}, [hit?.howler.analytic]);
|
|
35
25
|
const providerColor = useMemo(() => {
|
|
36
26
|
if (!hit?.event.provider) {
|
|
37
27
|
return PROVIDER_COLORS.unknown;
|
|
@@ -91,11 +81,7 @@ const HitBanner = ({ hit, layout = HitLayout.NORMAL, showAssigned = true }) => {
|
|
|
91
81
|
}, spacing: layout !== HitLayout.COMFY ? 1 : 2, divider: _jsx(Divider, { orientation: "horizontal", sx: [
|
|
92
82
|
layout !== HitLayout.COMFY && { marginTop: '4px !important' },
|
|
93
83
|
{ mr: `${theme.spacing(-1)} !important` }
|
|
94
|
-
] }), children: [_jsxs(Typography, { variant:
|
|
95
|
-
e.stopPropagation();
|
|
96
|
-
}, onClick: e => {
|
|
97
|
-
e.stopPropagation();
|
|
98
|
-
}, children: hit.howler.analytic })) : (hit.howler.analytic), hit.howler.detection && ': ', hit.howler.detection] }), hit.howler?.rationale && (_jsxs(Typography, { flex: 1, variant: textVariant, color: ESCALATION_COLORS[hit.howler.escalation] + '.main', sx: { fontWeight: 'bold' }, children: [t('hit.header.rationale'), ": ", hit.howler.rationale] })), hit.howler?.outline && (_jsxs(_Fragment, { children: [_jsxs(Grid, { container: true, spacing: layout !== HitLayout.COMFY ? 1 : 2, sx: { ml: `${theme.spacing(-1)} !important` }, children: [hit.howler.outline.threat && (_jsx(Grid, { item: true, children: _jsx(Wrapper, { i18nKey: "hit.header.threat", value: hit.howler.outline.threat, field: "howler.outline.threat" }) })), hit.howler.outline.target && (_jsx(Grid, { item: true, children: _jsx(Wrapper, { i18nKey: "hit.header.target", value: hit.howler.outline.target, field: "howler.outline.target" }) }))] }), hit.howler.outline.indicators?.length > 0 && (_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Typography, { component: "span", variant: textVariant, children: [t('hit.header.indicators'), ":"] }), _jsx(Grid, { container: true, spacing: 0.5, sx: { mt: `${theme.spacing(-0.5)} !important`, ml: `${theme.spacing(0.25)} !important` }, children: uniq(hit.howler.outline.indicators).map((_indicator, index) => {
|
|
84
|
+
] }), children: [_jsx(AnalyticLink, { hit: hit }), hit.howler?.rationale && (_jsxs(Typography, { flex: 1, variant: textVariant, color: ESCALATION_COLORS[hit.howler.escalation] + '.main', sx: { fontWeight: 'bold' }, children: [t('hit.header.rationale'), ": ", hit.howler.rationale] })), hit.howler?.outline && (_jsxs(_Fragment, { children: [_jsxs(Grid, { container: true, spacing: layout !== HitLayout.COMFY ? 1 : 2, sx: { ml: `${theme.spacing(-1)} !important` }, children: [hit.howler.outline.threat && (_jsx(Grid, { item: true, children: _jsx(Wrapper, { i18nKey: "hit.header.threat", value: hit.howler.outline.threat, field: "howler.outline.threat" }) })), hit.howler.outline.target && (_jsx(Grid, { item: true, children: _jsx(Wrapper, { i18nKey: "hit.header.target", value: hit.howler.outline.target, field: "howler.outline.target" }) }))] }), hit.howler.outline.indicators?.length > 0 && (_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Typography, { component: "span", variant: textVariant, children: [t('hit.header.indicators'), ":"] }), _jsx(Grid, { container: true, spacing: 0.5, sx: { mt: `${theme.spacing(-0.5)} !important`, ml: `${theme.spacing(0.25)} !important` }, children: uniq(hit.howler.outline.indicators).map((_indicator, index) => {
|
|
99
85
|
return (_jsx(Grid, { item: true, children: _jsxs(Stack, { direction: "row", children: [_jsx(PluginTypography, { context: "indicators", variant: textVariant, value: _indicator, children: _indicator }), index < hit.howler.outline.indicators.length - 1 && (_jsx(Typography, { variant: textVariant, children: ',' }))] }) }, _indicator));
|
|
100
86
|
}) })] })), hit.howler.outline.summary && (_jsx(Wrapper, { i18nKey: "hit.header.summary", value: hit.howler.outline.summary, paragraph: true, textOverflow: "wrap", sx: [compressed && { marginTop: `0 !important` }], field: "howler.outline.summary" }))] }))] }), _jsxs(Stack, { direction: "column", spacing: layout !== HitLayout.COMFY ? 0.5 : 1, alignSelf: "stretch", sx: [
|
|
101
87
|
{ minWidth: 0, alignItems: { sm: 'end', md: 'start' }, flex: 1, pl: 1 },
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Typography } from '@mui/material';
|
|
3
|
+
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
import { Link } from 'react-router-dom';
|
|
6
|
+
const AnalyticLink = ({ hit, compressed, alignSelf = 'start' }) => {
|
|
7
|
+
const { getMatchingAnalytic } = useMatchers();
|
|
8
|
+
const [analyticId, setAnalyticId] = useState();
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!hit?.howler.analytic) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
getMatchingAnalytic(hit).then(analytic => setAnalyticId(analytic?.analytic_id));
|
|
14
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
15
|
+
}, [hit?.howler.analytic]);
|
|
16
|
+
return (_jsxs(Typography, { variant: compressed ? 'body1' : 'h6', fontWeight: compressed && 'bold', sx: { alignSelf, '& a': { color: 'text.primary' } }, children: [analyticId ? (_jsx(Link, { to: `/analytics/${analyticId}`, onAuxClick: e => {
|
|
17
|
+
e.stopPropagation();
|
|
18
|
+
}, onClick: e => {
|
|
19
|
+
e.stopPropagation();
|
|
20
|
+
}, children: hit.howler.analytic })) : (hit.howler.analytic), hit.howler.detection && ': ', hit.howler.detection] }));
|
|
21
|
+
};
|
|
22
|
+
export default AnalyticLink;
|
|
@@ -7,7 +7,7 @@ declare const useHitActions: (_hits: Hit | Hit[]) => {
|
|
|
7
7
|
canAssess: boolean;
|
|
8
8
|
loading: boolean;
|
|
9
9
|
manage: (transition: string) => Promise<void>;
|
|
10
|
-
assess: (assessment: string, skipRationale?: boolean) => Promise<void>;
|
|
10
|
+
assess: (assessment: string, skipRationale?: boolean, providedRationale?: any) => Promise<void>;
|
|
11
11
|
vote: (v: string) => Promise<void>;
|
|
12
12
|
selectedVote: string;
|
|
13
13
|
};
|
|
@@ -90,9 +90,9 @@ const useHitActions = (_hits) => {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}, [dispatchApi, hits, selectedVote, updateHit, user.email]);
|
|
93
|
-
const assess = useCallback(async (assessment, skipRationale = false) => {
|
|
93
|
+
const assess = useCallback(async (assessment, skipRationale = false, providedRationale = null) => {
|
|
94
94
|
const rationale = skipRationale
|
|
95
|
-
? t('rationale.default', { assessment })
|
|
95
|
+
? (providedRationale ?? t('rationale.default', { assessment }))
|
|
96
96
|
: await new Promise(res => {
|
|
97
97
|
showModal(_jsx(RationaleModal, { hits: hits, onSubmit: _rationale => {
|
|
98
98
|
res(_rationale);
|
|
@@ -25,7 +25,8 @@ const useMySitemap = () => {
|
|
|
25
25
|
routes: [
|
|
26
26
|
{ path: '/', title: t('route.home'), isRoot: true, icon: _jsx(Dashboard, {}) },
|
|
27
27
|
{ path: '/cases', title: t('route.cases'), isRoot: true, icon: _jsx(BookRounded, {}) },
|
|
28
|
-
{ path: '/cases/:id', title: t('route.cases.view') },
|
|
28
|
+
{ path: '/cases/:id', title: t('route.cases.view'), breadcrumbs: ['/cases'] },
|
|
29
|
+
{ path: '/cases/:id/*', title: t('route.cases.view'), breadcrumbs: ['/cases'] },
|
|
29
30
|
{ path: '/admin/users', title: t('route.admin.user.search'), isRoot: true, icon: _jsx(PersonSearch, {}) },
|
|
30
31
|
{
|
|
31
32
|
path: '/admin/users/:id',
|
|
@@ -2,15 +2,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Check, FormatListBulleted, HourglassBottom, Pause, People, WarningRounded } from '@mui/icons-material';
|
|
3
3
|
import { Autocomplete, Card, Chip, Divider, LinearProgress, Skeleton, Stack, Table, TableBody, TableCell, TableRow, TextField, Typography } from '@mui/material';
|
|
4
4
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
5
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
5
6
|
import UserList from '@cccsaurora/howler-ui/components/elements/UserList';
|
|
6
7
|
import dayjs from 'dayjs';
|
|
7
8
|
import { useContext, useState } from 'react';
|
|
8
9
|
import { useTranslation } from 'react-i18next';
|
|
9
10
|
import useCase from '../hooks/useCase';
|
|
11
|
+
import ResolveModal from '../modals/ResolveModal';
|
|
10
12
|
import SourceAggregate from './aggregates/SourceAggregate';
|
|
11
13
|
const CaseDetails = ({ case: providedCase }) => {
|
|
12
14
|
const { t } = useTranslation();
|
|
13
15
|
const { case: _case, updateCase } = useCase({ case: providedCase });
|
|
16
|
+
const { showModal } = useContext(ModalContext);
|
|
14
17
|
const { config } = useContext(ApiConfigContext);
|
|
15
18
|
const [loading, setLoading] = useState(false);
|
|
16
19
|
const wrappedUpdate = async (subset) => {
|
|
@@ -22,6 +25,15 @@ const CaseDetails = ({ case: providedCase }) => {
|
|
|
22
25
|
setLoading(false);
|
|
23
26
|
}
|
|
24
27
|
};
|
|
28
|
+
const handleStatus = (status) => {
|
|
29
|
+
if (status === 'resolved') {
|
|
30
|
+
const onConfirm = () => wrappedUpdate({ status });
|
|
31
|
+
showModal(_jsx(ResolveModal, { case: _case, onConfirm: onConfirm }), { maxHeight: '80vh' });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
wrappedUpdate({ status });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
25
37
|
if (!_case) {
|
|
26
38
|
return (_jsx(Card, { sx: {
|
|
27
39
|
borderRadius: 0,
|
|
@@ -42,8 +54,8 @@ const CaseDetails = ({ case: providedCase }) => {
|
|
|
42
54
|
position: 'relative'
|
|
43
55
|
}, children: [_jsx(LinearProgress, { sx: { opacity: +loading, position: 'absolute', top: 0, left: 0, right: 0 } }), _jsxs(Stack, { spacing: 2, children: [_jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [{
|
|
44
56
|
'in-progress': _jsx(HourglassBottom, { color: "warning" }),
|
|
45
|
-
|
|
46
|
-
|
|
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) =>
|
|
57
|
+
'on-hold': _jsx(Pause, { color: "disabled" }),
|
|
58
|
+
resolved: _jsx(Check, { color: "success" })
|
|
59
|
+
}[_case.status] ?? _jsx(WarningRounded, { fontSize: "small" }), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.status') })] }), _jsx(Autocomplete, { size: "small", disabled: loading, value: _case.status, options: config.lookups['howler.status'], renderInput: params => _jsx(TextField, { ...params, size: "small" }), onChange: (_ev, status) => handleStatus(status) })] }), _jsx(Divider, {}), _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(People, {}), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.participants') })] }), _jsx(UserList, { buttonSx: { alignSelf: 'start' }, multiple: true, i18nLabel: "page.cases.detail.assignment", userIds: _case.participants ?? [], onChange: participants => wrappedUpdate({ participants }), disabled: loading })] }), _jsx(Divider, {}), _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(FormatListBulleted, {}), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.properties') })] }), _jsx(Table, { sx: { '& td': { p: 1 } }, children: _jsxs(TableBody, { children: [_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.escalation') }) }), _jsx(TableCell, { children: _jsx(Chip, { size: "small", label: _case.escalation }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.created') }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: dayjs(_case.created).toString() }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.updated') }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: dayjs(_case.updated).toString() }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.sources') }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: _jsx(SourceAggregate, { case: _case }) }) })] })] }) })] })] })] }));
|
|
48
60
|
};
|
|
49
61
|
export default CaseDetails;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default "<h3>Actions</h3>\n\n<ul class=\"actions_list\">\n\n<li>\n\n</li>\n\n<li>\n\n</li>\n\n</ul>\n\n<h3>Data</h3>\n\n<table class=\"table_overview\">\n\n<tbody>\n\n<tr>\n\n<td style=\"padding:8px;font-weight:bold;\">Tangent</td>\n\n<td style=\"padding:8px;\">Triangle</td>\n\n</tr>\n\n<tr>\n\n<td style=\"padding:8px;font-weight:bold;\">Start date</td>\n\n<td style=\"padding:8px;\">1</td>\n\n</tr>\n\n<tr>\n\n<td style=\"padding:8px;font-weight:bold;\">End date</td>\n\n<td style=\"padding:8px;\">1</td>\n\n</tr>\n\n<tr>\n\n<td style=\"padding:8px;font-weight:bold;\">Country</td>\n\n<td style=\"padding:8px;\"><span style=\"text-transform: uppercase;\">Canada</span> / <span style=\"text-transform: capitalize;\">Canada / <span style=\"text-transform: capitalize;\">Canada</span></td>\n\n</tr>\n\n</tbody>\n\n</table>\n\n<h3>Visualization</h3>\n\n```mermaid\n\ngraph LR\n\nA[Password Spraying IP] -- Login Failure Account A --> D{ Entra ID }\n\nA[Password Spraying IP] -- Login Failure Account B --> D{ Entra ID }\n\nA[Password Spraying IP] -- Login Failure Account C --> D{ Entra ID }\n\nA[Password Spraying IP] -- Login Failure Account D --> D{ Entra ID }\n\nA[Password Spraying IP] -- Login Failure Account E --> D{ Entra ID }\n\nA[Password Spraying IP] -- Login Failure Account E --> D{ Entra ID }\n\nA[Password Spraying IP] == Login Attempt Account E ==> D{ Entra ID }\n\nD{ Entra ID } == \u00a0Entra ID Returns Code {{error.code}} ==> A\n\nclassDef orange fill:#f96,stroke:#333,stroke-width:2px\n\nclassDef blue fill:#32bedd,stroke:#333,stroke-width:2px\n\nclass D blue\n\nclass A orange\n\n```\n\n<style>\n\n/* Actions */\n\n.actions_list li {\n\ndisplay:inline!important;\n\nmargin-right: 45px!important;\n\nmargin-bottom: 15px!important;\n\n}\n\n.actions_list {\n\nlist-style: none!important;\n\nmargin: 0px!important;\n\npadding: 0px!important;\n\n}\n\n.actions_list img {\n\nheight:20px!important;\n\nmargin-bottom:-7px!important;\n\npadding-right:5px!important;\n\n}\n\n/* Tables */\n\n.MuiPaper-root.MuiPaper-elevation.MuiPaper-rounded.MuiPaper-elevation1.MuiTableContainer-root {\n\nbox-shadow:unset !important;\n\nwidth: fit-content !important;\n\n}\n\n/* General */\n\nh3 {\n\nborder-bottom: 2px solid #2d7dc9;\n\n/*border-bottom: 5px solid rgba(255, 255, 255, 0.12);*/\n\n/*color: #393939;*/\n\n}\n\n/* Visualization */\n\n.mermaid {\n\nbackground-color:#fff;\n\npadding:15px;\n\ntext-align: center;\n\n}\n\n/* Boites */\n\n.actor.actor-top {\n\nstroke: #393939 !important;\n\nfill: lightgrey !important;\n\n}\n\n/* T\u00eate du bonhomme */\n\n.actor-man circle {\n\nstroke: #393939 !important;\n\nfill: #28A745 !important;\n\n}\n\n/* Corps du bonhomme */\n\n.actor-man line {\n\nstroke: #393939 !important;\n\nfill: #393939 !important;\n\n}\n\n/* Lignes verticales */\n\n.actor-line {\n\nstroke: #393939 !important;\n\nfill: #393939 !important;\n\n}\n\n/* Description des fl\u00e8ches horizontales */\n\n.messageText {\n\nfill: #393939 !important;\n\nstroke: none;\n\n}\n\n/* T\u00eates de fl\u00e8ches */\n\nmarker#arrowhead path {\n\nfill: #DC3545 !important;\n\n}\n\n/* Texte en-dessous du bonhomme */\n\ntext.actor > tspan {\n\nfill: #393939 !important;\n\n}\n\n/* Fl\u00e8ches horizontales */\n\n.messageLine0, .messageLine1 {\n\nstroke-width: 1.5;\n\nstroke-dasharray: none;\n\nstroke: #393939 !important;\n\n}\n\n/* Texte dans boites */\n\n.actor-box {\n\nfont-weight: bold !important;\n\ncolor: #393939 !important;\n\n}\n\n/* Cacher le bonhomme du bas */\n\n.actor-bottom {\n\ndisplay:None;\n\n}\n\n</style>\n"
|
|
@@ -6,7 +6,7 @@ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
|
6
6
|
import { get, last, omit, set } from 'lodash-es';
|
|
7
7
|
import { useEffect, useMemo, useState } from 'react';
|
|
8
8
|
import { Link, useLocation } from 'react-router-dom';
|
|
9
|
-
import { ESCALATION_COLORS
|
|
9
|
+
import { ESCALATION_COLORS } from '@cccsaurora/howler-ui/utils/constants';
|
|
10
10
|
const buildTree = (items = []) => {
|
|
11
11
|
// Root tree node stores direct children in `leaves` and nested folders as object keys.
|
|
12
12
|
const tree = { leaves: [] };
|
|
@@ -44,34 +44,43 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
|
|
|
44
44
|
const currentRootCaseId = rootCaseId || _case?.case_id;
|
|
45
45
|
// Metadata for hit-type items
|
|
46
46
|
useEffect(() => {
|
|
47
|
-
tree.leaves
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
status: hit.howler?.status,
|
|
57
|
-
escalation: hit.howler?.escalation,
|
|
58
|
-
assessment: hit.howler?.assessment
|
|
59
|
-
}
|
|
60
|
-
}));
|
|
61
|
-
});
|
|
47
|
+
const ids = tree.leaves?.filter(leaf => leaf.type?.toLowerCase() === 'hit').map(leaf => leaf.id);
|
|
48
|
+
if (!ids || ids.length < 1) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
dispatchApi(api.search.hit.post({ query: `howler.id:(${ids.join(' OR ')})` }), { throwError: false }).then(result => {
|
|
52
|
+
if (result?.items?.length < 1) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
setHitMetadata(Object.fromEntries(result.items.map(hit => [hit.howler.id, hit.howler])));
|
|
62
56
|
});
|
|
63
57
|
}, [tree.leaves, dispatchApi]);
|
|
58
|
+
const getIconColor = (itemType, itemKey, leafId) => {
|
|
59
|
+
if (itemType === 'hit' && leafId) {
|
|
60
|
+
const meta = hitMetadata[leafId];
|
|
61
|
+
if (meta?.escalation && ESCALATION_COLORS[meta.escalation]) {
|
|
62
|
+
return ESCALATION_COLORS[meta.escalation];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (itemType === 'case' && itemKey) {
|
|
66
|
+
const caseData = nestedCases[itemKey];
|
|
67
|
+
if (caseData?.escalation && ESCALATION_COLORS[caseData.escalation]) {
|
|
68
|
+
return ESCALATION_COLORS[caseData.escalation];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return 'default';
|
|
72
|
+
};
|
|
64
73
|
const getItemColor = (itemType, itemKey, leafId) => {
|
|
65
74
|
if (itemType === 'hit' && leafId) {
|
|
66
75
|
const meta = hitMetadata[leafId];
|
|
67
|
-
if (meta?.
|
|
68
|
-
return `${
|
|
76
|
+
if (meta?.escalation && ESCALATION_COLORS[meta.escalation]) {
|
|
77
|
+
return `${ESCALATION_COLORS[meta.escalation]}.light`;
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
if (itemType === 'case' && itemKey) {
|
|
72
81
|
const caseData = nestedCases[itemKey];
|
|
73
82
|
if (caseData?.escalation && ESCALATION_COLORS[caseData.escalation]) {
|
|
74
|
-
return `${ESCALATION_COLORS[caseData.escalation]}.
|
|
83
|
+
return `${ESCALATION_COLORS[caseData.escalation]}.light`;
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
86
|
return 'text.secondary';
|
|
@@ -123,22 +132,23 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
|
|
|
123
132
|
const itemPath = fullRelativePath
|
|
124
133
|
? `/cases/${currentRootCaseId}/${fullRelativePath}`
|
|
125
134
|
: `/cases/${currentRootCaseId}`;
|
|
126
|
-
const getIconForType = (
|
|
127
|
-
|
|
135
|
+
const getIconForType = () => {
|
|
136
|
+
const iconColor = getIconColor(itemType, itemKey, leaf.id);
|
|
137
|
+
switch (itemType) {
|
|
128
138
|
case 'case':
|
|
129
|
-
return _jsx(BookRounded, { fontSize: "small" });
|
|
139
|
+
return _jsx(BookRounded, { fontSize: "small", color: iconColor });
|
|
130
140
|
case 'observable':
|
|
131
|
-
return _jsx(Visibility, { fontSize: "small" });
|
|
141
|
+
return _jsx(Visibility, { fontSize: "small", color: iconColor });
|
|
132
142
|
case 'hit':
|
|
133
|
-
return _jsx(CheckCircle, { fontSize: "small" });
|
|
143
|
+
return _jsx(CheckCircle, { fontSize: "small", color: iconColor });
|
|
134
144
|
case 'table':
|
|
135
|
-
return _jsx(TableChart, { fontSize: "small" });
|
|
145
|
+
return _jsx(TableChart, { fontSize: "small", color: iconColor });
|
|
136
146
|
case 'lead':
|
|
137
|
-
return _jsx(Lightbulb, { fontSize: "small" });
|
|
147
|
+
return _jsx(Lightbulb, { fontSize: "small", color: iconColor });
|
|
138
148
|
case 'reference':
|
|
139
|
-
return _jsx(LinkIcon, { fontSize: "small" });
|
|
149
|
+
return _jsx(LinkIcon, { fontSize: "small", color: iconColor });
|
|
140
150
|
default:
|
|
141
|
-
return _jsx(Article, { fontSize: "small" });
|
|
151
|
+
return _jsx(Article, { fontSize: "small", color: iconColor });
|
|
142
152
|
}
|
|
143
153
|
};
|
|
144
154
|
const leafColor = getItemColor(itemType, itemKey, leaf.id);
|
|
@@ -163,7 +173,7 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
|
|
|
163
173
|
transition: theme.transitions.create('transform', { duration: 100 }),
|
|
164
174
|
transform: isCaseOpen ? 'rotate(90deg)' : 'rotate(0deg)'
|
|
165
175
|
}
|
|
166
|
-
] }), getIconForType(
|
|
176
|
+
] }), getIconForType(), _jsx(Typography, { variant: "caption", color: leafColor, sx: { userSelect: 'none', pl: 0.5, textWrap: 'nowrap' }, children: last(leaf.path?.split('/') || []) })] }), isCase && isCaseOpen && isCaseLoading && (_jsx(Stack, { pl: step * 1.5 + 4, py: 0.25, children: _jsx(Skeleton, { width: 140, height: 16 }) })), isCase && isCaseOpen && nestedCase && (_jsx(CaseFolder, { case: nestedCase, step: step + 1, rootCaseId: currentRootCaseId, pathPrefix: fullRelativePath }))] }, `${_case?.case_id}-${leaf.id}-${leaf.path}`));
|
|
167
177
|
})] }))] }));
|
|
168
178
|
};
|
|
169
179
|
export default CaseFolder;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { OpenInNew } from '@mui/icons-material';
|
|
3
|
+
import { Autocomplete, Box, Button, Card, Chip, Divider, IconButton, LinearProgress, Skeleton, Stack, TextField, Typography } from '@mui/material';
|
|
4
|
+
import api from '@cccsaurora/howler-ui/api';
|
|
5
|
+
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
6
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
7
|
+
import AnalyticLink from '@cccsaurora/howler-ui/components/elements/hit/elements/AnalyticLink';
|
|
8
|
+
import EscalationChip from '@cccsaurora/howler-ui/components/elements/hit/elements/EscalationChip';
|
|
9
|
+
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
10
|
+
import useHitActions from '@cccsaurora/howler-ui/components/hooks/useHitActions';
|
|
11
|
+
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
12
|
+
import { uniq } from 'lodash-es';
|
|
13
|
+
import { useContext, useEffect, useMemo, useState } from 'react';
|
|
14
|
+
import { useTranslation } from 'react-i18next';
|
|
15
|
+
import { Link } from 'react-router-dom';
|
|
16
|
+
import useCase from '../hooks/useCase';
|
|
17
|
+
const ResolveModal = ({ case: _case, onConfirm }) => {
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
const { dispatchApi } = useMyApi();
|
|
20
|
+
const { close } = useContext(ModalContext);
|
|
21
|
+
const { config } = useContext(ApiConfigContext);
|
|
22
|
+
const { updateCase } = useCase({ case: _case });
|
|
23
|
+
const [loading, setLoading] = useState(true);
|
|
24
|
+
const [rationale, setRationale] = useState('');
|
|
25
|
+
const [assessment, setAssessment] = useState(null);
|
|
26
|
+
const [hits, setHits] = useState([]);
|
|
27
|
+
const hitIds = useMemo(() => uniq((_case?.items ?? []).filter(item => item.type === 'hit').map(item => item.id)), [_case?.items]);
|
|
28
|
+
const { assess } = useHitActions(hits);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
(async () => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await dispatchApi(api.search.hit.post({ query: `howler.id:(${hitIds.join(' OR ')}) AND -howler.status:resolved` }));
|
|
33
|
+
setHits(result.items);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
}, [dispatchApi, hitIds]);
|
|
40
|
+
const handleConfirm = async () => {
|
|
41
|
+
setLoading(true);
|
|
42
|
+
try {
|
|
43
|
+
await assess(assessment, true, rationale);
|
|
44
|
+
await updateCase({ status: 'resolved' });
|
|
45
|
+
onConfirm();
|
|
46
|
+
close();
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return (_jsxs(Stack, { spacing: 2, p: 2, alignItems: "start", sx: { minWidth: 'min(1000px, 60vw)', maxHeight: '100%', height: '100%' }, children: [_jsx(Typography, { variant: "h4", children: t('modal.cases.resolve') }), _jsx(Typography, { children: t('modal.cases.resolve.description') }), _jsxs(Stack, { spacing: 1, overflow: "auto", width: "100%", flex: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Box, { flex: 1, children: _jsx(TextField, { size: "small", fullWidth: true, placeholder: t('modal.rationale.label'), value: rationale, onChange: ev => setRationale(ev.target.value) }) }), _jsx(Box, { flex: 1, children: _jsx(Autocomplete, { size: "small", value: assessment, onChange: (_ev, _assessment) => setAssessment(_assessment), options: config.lookups['howler.assessment'], disablePortal: true, renderInput: params => (_jsx(TextField, { ...params, placeholder: t('hit.details.actions.assessment'), fullWidth: true })) }) })] }), _jsxs(Stack, { position: "relative", children: [_jsx(Divider, {}), _jsx(LinearProgress, { sx: { opacity: +loading } })] }), loading
|
|
53
|
+
? hitIds.map(id => _jsx(Skeleton, { variant: "rounded", height: "40px", width: "100%" }, id))
|
|
54
|
+
: hits.map(hit => (_jsx(Card, { sx: { p: 1, flexShrink: 0 }, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, width: "100%", children: [_jsx(AnalyticLink, { hit: hit, compressed: true, alignSelf: "center" }), _jsx(EscalationChip, { hit: hit, layout: HitLayout.DENSE }), _jsx(Chip, { sx: { width: 'fit-content', display: 'inline-flex' }, label: hit.howler.status, size: "small", color: "primary" }), _jsx("div", { style: { flex: 1 } }), _jsx(IconButton, { size: "small", component: Link, to: `/hits/${hit.howler.id}`, children: _jsx(OpenInNew, { fontSize: "small" }) })] }) }, hit.howler.id)))] }), _jsxs(Stack, { direction: "row", spacing: 1, alignSelf: "end", children: [_jsx(Button, { variant: "outlined", color: "error", onClick: close, children: t('cancel') }), _jsx(Button, { variant: "outlined", color: "success", disabled: loading || !assessment || !rationale, onClick: handleConfirm, children: t('confirm') })] })] }));
|
|
55
|
+
};
|
|
56
|
+
export default ResolveModal;
|
|
@@ -292,6 +292,8 @@
|
|
|
292
292
|
"modal.action.empty": "Action Name cannot be empty.",
|
|
293
293
|
"modal.action.label": "Action Name",
|
|
294
294
|
"modal.action.title": "Save Action",
|
|
295
|
+
"modal.cases.resolve": "Resolve Case",
|
|
296
|
+
"modal.cases.resolve.description": "When resolving a case, you must either assess all open alerts, or add an assessment to the alerts.",
|
|
295
297
|
"modal.confirm.delete.description": "Are you sure you want to delete this item?",
|
|
296
298
|
"modal.confirm.delete.title": "Confirm Deletion",
|
|
297
299
|
"modal.rationale.description": "Provide a rationale that succinctly explains to other analysts why you are confident in this assessment.",
|
package/package.json
CHANGED
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"internal-slot": "1.0.7"
|
|
102
102
|
},
|
|
103
103
|
"type": "module",
|
|
104
|
-
"version": "2.17.0-dev.
|
|
104
|
+
"version": "2.17.0-dev.523",
|
|
105
105
|
"exports": {
|
|
106
106
|
"./i18n": "./i18n.js",
|
|
107
107
|
"./index.css": "./index.css",
|
|
@@ -188,6 +188,7 @@
|
|
|
188
188
|
"./components/routes/help/markdown/fr/*.md": "./components/routes/help/markdown/fr/*.md.js",
|
|
189
189
|
"./components/routes/help/markdown/en/*.md": "./components/routes/help/markdown/en/*.md.js",
|
|
190
190
|
"./components/routes/admin/users/*": "./components/routes/admin/users/*.js",
|
|
191
|
+
"./components/routes/cases/modals/*": "./components/routes/cases/modals/*.js",
|
|
191
192
|
"./components/routes/cases/hooks/*": "./components/routes/cases/hooks/*.js",
|
|
192
193
|
"./components/routes/cases/detail/*": "./components/routes/cases/detail/*.js",
|
|
193
194
|
"./components/routes/cases/detail/sidebar/*": "./components/routes/cases/detail/sidebar/*.js",
|
package/utils/constants.d.ts
CHANGED
|
@@ -5,9 +5,9 @@ export declare const VERSION: any;
|
|
|
5
5
|
export declare const MY_LOCAL_STORAGE_PREFIX = "howler.ui";
|
|
6
6
|
export declare const MY_SESSION_STORAGE_PREFIX = "howler.ui.cache";
|
|
7
7
|
export declare const ESCALATION_COLORS: {
|
|
8
|
-
alert:
|
|
9
|
-
evidence:
|
|
10
|
-
hit:
|
|
8
|
+
alert: "warning";
|
|
9
|
+
evidence: "error";
|
|
10
|
+
hit: "primary";
|
|
11
11
|
};
|
|
12
12
|
export declare const STATUS_COLORS: {
|
|
13
13
|
open: string;
|