@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.
@@ -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, useEffect, useMemo, useState } from 'react';
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: compressed ? 'body1' : 'h6', fontWeight: compressed && 'bold', sx: { alignSelf: 'start', '& a': { color: 'text.primary' } }, children: [analyticId ? (_jsx(Link, { to: `/analytics/${analyticId}`, onAuxClick: e => {
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,8 @@
1
+ import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
+ import { type FC } from 'react';
3
+ declare const AnalyticLink: FC<{
4
+ hit: Hit;
5
+ compressed?: boolean;
6
+ alignSelf?: string;
7
+ }>;
8
+ export default AnalyticLink;
@@ -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) => wrappedUpdate({ status }) })] }), _jsx(Divider, {}), _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(People, {}), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.participants') })] }), _jsx(UserList, { buttonSx: { alignSelf: 'start' }, multiple: true, i18nLabel: "page.cases.detail.assignment", userIds: _case.participants ?? [], onChange: participants => wrappedUpdate({ participants }), disabled: loading })] }), _jsx(Divider, {}), _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(FormatListBulleted, {}), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.properties') })] }), _jsx(Table, { sx: { '& td': { p: 1 } }, children: _jsxs(TableBody, { children: [_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.escalation') }) }), _jsx(TableCell, { children: _jsx(Chip, { size: "small", label: _case.escalation }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.created') }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: dayjs(_case.created).toString() }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.updated') }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: dayjs(_case.updated).toString() }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.sources') }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: _jsx(SourceAggregate, { case: _case }) }) })] })] }) })] })] })] }));
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, STATUS_COLORS } from '@cccsaurora/howler-ui/utils/constants';
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?.status && STATUS_COLORS[meta.status]) {
68
- return `${STATUS_COLORS[meta.status]}.main`;
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]}.main`;
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 = (type) => {
127
- switch (type) {
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(itemType), _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}`));
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,7 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import { type FC } from 'react';
3
+ declare const ResolveModal: FC<{
4
+ case: Case;
5
+ onConfirm: () => void;
6
+ }>;
7
+ export default ResolveModal;
@@ -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.520",
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",
@@ -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: string;
9
- evidence: string;
10
- hit: string;
8
+ alert: "warning";
9
+ evidence: "error";
10
+ hit: "primary";
11
11
  };
12
12
  export declare const STATUS_COLORS: {
13
13
  open: string;