@cccsaurora/howler-ui 2.17.0-dev.520 → 2.17.0-dev.522
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/ObjectDetails.js +4 -4
- 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/routes/cases/detail/CaseDetails.js +13 -1
- package/components/routes/cases/detail/sidebar/CaseFolder.js +30 -14
- 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
|
@@ -5,7 +5,7 @@ import { ArrowDropDown, InfoOutlined } from '@mui/icons-material';
|
|
|
5
5
|
import { Accordion, AccordionDetails, AccordionSummary, Box, Divider, Grid, Stack, TextField, Tooltip, Typography, useTheme } from '@mui/material';
|
|
6
6
|
import { flatten } from 'flat';
|
|
7
7
|
import Fuse from 'fuse.js';
|
|
8
|
-
import { capitalize, groupBy, isArray, isEmpty, isNull, isObject, isPlainObject, isUndefined, max, sortBy, uniq } from 'lodash-es';
|
|
8
|
+
import { capitalize, groupBy, isArray, isBoolean, isEmpty, isNull, isNumber, isObject, isPlainObject, isUndefined, max, sortBy, uniq } from 'lodash-es';
|
|
9
9
|
import { memo, useEffect, useMemo, useState } from 'react';
|
|
10
10
|
import { useTranslation } from 'react-i18next';
|
|
11
11
|
import Throttler from '@cccsaurora/howler-ui/utils/Throttler';
|
|
@@ -56,7 +56,7 @@ const ObjectRenderer = memo(({ obj: obj, data, parentKey, indent = false }) => {
|
|
|
56
56
|
}, [data]);
|
|
57
57
|
const longestKey = useMemo(() => max(entries.map(([key]) => key.length)), [entries]);
|
|
58
58
|
return (_jsxs(Stack, { direction: "row", overflow: "hidden", maxWidth: "100%", children: [indent && _jsx(Divider, { orientation: "vertical", flexItem: true, sx: { borderColor: 'primary.main', borderWidth: '2px' } }), _jsx(Stack, { flex: 1, ml: 1, maxWidth: "100%", children: entries
|
|
59
|
-
.filter(([__, val]) => !isNull(val) && !isUndefined(val) && !isEmpty(val))
|
|
59
|
+
.filter(([__, val]) => !isNull(val) && !isUndefined(val) && !isEmpty(val) && !isBoolean(val) && !isNumber(val))
|
|
60
60
|
.map(([key, val]) => {
|
|
61
61
|
if (Array.isArray(val)) {
|
|
62
62
|
return _jsx(ListRenderer, { obj: obj, maxKeyLength: longestKey, objKey: key, entries: val }, key);
|
|
@@ -82,7 +82,7 @@ const Collapsible = memo(({ obj, title, data, query }) => {
|
|
|
82
82
|
const throttler = useMemo(() => new Throttler(400), []);
|
|
83
83
|
const [scores, setScores] = useState([]);
|
|
84
84
|
const [results, setResults] = useState({});
|
|
85
|
-
const options = useMemo(() => Object.entries(data).map(([key, value]) => ({ key, value })), [data]);
|
|
85
|
+
const options = useMemo(() => Object.entries(data).map(([key, value]) => ({ key, value: value.toString() })), [data]);
|
|
86
86
|
const keys = useMemo(() => options
|
|
87
87
|
.flatMap(option => (isArray(option.value) ? Object.keys(flatten(option.value)) : []))
|
|
88
88
|
.map(key => key.replace(/\d+/g, 'value'))
|
|
@@ -117,7 +117,7 @@ const ObjectDetails = ({ obj }) => {
|
|
|
117
117
|
const groups = useMemo(() => groupBy(Object.entries(flatten(obj ?? {}, { safe: true })).filter(([key, value]) => !key.startsWith('__') &&
|
|
118
118
|
key.includes('.') &&
|
|
119
119
|
['howler', 'labels'].every(prefix => !key.startsWith(prefix)) &&
|
|
120
|
-
!isEmpty(value)), ([key]) => key.split('.')[0]), [obj]);
|
|
120
|
+
(!isEmpty(value) || isNumber(value) || isBoolean(value))), ([key]) => key.split('.')[0]), [obj]);
|
|
121
121
|
return (_jsxs(Stack, { spacing: 1, children: [_jsx(TextField, { value: query, onChange: event => setQuery(event.target.value), label: t('overview.search') }), Object.entries(groups).map(([section, entries]) => {
|
|
122
122
|
return (_jsx(Collapsible, { obj: obj, query: query, title: section
|
|
123
123
|
.split('_')
|
|
@@ -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);
|
|
@@ -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,
|
|
@@ -44,6 +56,6 @@ const CaseDetails = ({ case: providedCase }) => {
|
|
|
44
56
|
'in-progress': _jsx(HourglassBottom, { color: "warning" }),
|
|
45
57
|
closed: _jsx(Check, { color: "success" }),
|
|
46
58
|
'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) =>
|
|
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;
|
|
@@ -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: [] };
|
|
@@ -61,17 +61,32 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
|
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
}, [tree.leaves, dispatchApi]);
|
|
64
|
+
const getIconColor = (itemType, itemKey, leafId) => {
|
|
65
|
+
if (itemType === 'hit' && leafId) {
|
|
66
|
+
const meta = hitMetadata[leafId];
|
|
67
|
+
if (meta?.escalation && ESCALATION_COLORS[meta.escalation]) {
|
|
68
|
+
return ESCALATION_COLORS[meta.escalation];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (itemType === 'case' && itemKey) {
|
|
72
|
+
const caseData = nestedCases[itemKey];
|
|
73
|
+
if (caseData?.escalation && ESCALATION_COLORS[caseData.escalation]) {
|
|
74
|
+
return ESCALATION_COLORS[caseData.escalation];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return 'default';
|
|
78
|
+
};
|
|
64
79
|
const getItemColor = (itemType, itemKey, leafId) => {
|
|
65
80
|
if (itemType === 'hit' && leafId) {
|
|
66
81
|
const meta = hitMetadata[leafId];
|
|
67
|
-
if (meta?.
|
|
68
|
-
return `${
|
|
82
|
+
if (meta?.escalation && ESCALATION_COLORS[meta.escalation]) {
|
|
83
|
+
return `${ESCALATION_COLORS[meta.escalation]}.light`;
|
|
69
84
|
}
|
|
70
85
|
}
|
|
71
86
|
if (itemType === 'case' && itemKey) {
|
|
72
87
|
const caseData = nestedCases[itemKey];
|
|
73
88
|
if (caseData?.escalation && ESCALATION_COLORS[caseData.escalation]) {
|
|
74
|
-
return `${ESCALATION_COLORS[caseData.escalation]}.
|
|
89
|
+
return `${ESCALATION_COLORS[caseData.escalation]}.light`;
|
|
75
90
|
}
|
|
76
91
|
}
|
|
77
92
|
return 'text.secondary';
|
|
@@ -123,22 +138,23 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
|
|
|
123
138
|
const itemPath = fullRelativePath
|
|
124
139
|
? `/cases/${currentRootCaseId}/${fullRelativePath}`
|
|
125
140
|
: `/cases/${currentRootCaseId}`;
|
|
126
|
-
const getIconForType = (
|
|
127
|
-
|
|
141
|
+
const getIconForType = () => {
|
|
142
|
+
const iconColor = getIconColor(itemType, itemKey, leaf.id);
|
|
143
|
+
switch (itemType) {
|
|
128
144
|
case 'case':
|
|
129
|
-
return _jsx(BookRounded, { fontSize: "small" });
|
|
145
|
+
return _jsx(BookRounded, { fontSize: "small", color: iconColor });
|
|
130
146
|
case 'observable':
|
|
131
|
-
return _jsx(Visibility, { fontSize: "small" });
|
|
147
|
+
return _jsx(Visibility, { fontSize: "small", color: iconColor });
|
|
132
148
|
case 'hit':
|
|
133
|
-
return _jsx(CheckCircle, { fontSize: "small" });
|
|
149
|
+
return _jsx(CheckCircle, { fontSize: "small", color: iconColor });
|
|
134
150
|
case 'table':
|
|
135
|
-
return _jsx(TableChart, { fontSize: "small" });
|
|
151
|
+
return _jsx(TableChart, { fontSize: "small", color: iconColor });
|
|
136
152
|
case 'lead':
|
|
137
|
-
return _jsx(Lightbulb, { fontSize: "small" });
|
|
153
|
+
return _jsx(Lightbulb, { fontSize: "small", color: iconColor });
|
|
138
154
|
case 'reference':
|
|
139
|
-
return _jsx(LinkIcon, { fontSize: "small" });
|
|
155
|
+
return _jsx(LinkIcon, { fontSize: "small", color: iconColor });
|
|
140
156
|
default:
|
|
141
|
-
return _jsx(Article, { fontSize: "small" });
|
|
157
|
+
return _jsx(Article, { fontSize: "small", color: iconColor });
|
|
142
158
|
}
|
|
143
159
|
};
|
|
144
160
|
const leafColor = getItemColor(itemType, itemKey, leaf.id);
|
|
@@ -163,7 +179,7 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
|
|
|
163
179
|
transition: theme.transitions.create('transform', { duration: 100 }),
|
|
164
180
|
transform: isCaseOpen ? 'rotate(90deg)' : 'rotate(0deg)'
|
|
165
181
|
}
|
|
166
|
-
] }), getIconForType(
|
|
182
|
+
] }), 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
183
|
})] }))] }));
|
|
168
184
|
};
|
|
169
185
|
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.522",
|
|
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;
|