@cccsaurora/howler-ui 2.17.0-dev.515 → 2.17.0-dev.517

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.
@@ -32,7 +32,12 @@ const HitBanner = ({ hit, layout = HitLayout.NORMAL, showAssigned = true }) => {
32
32
  getMatchingAnalytic(hit).then(analytic => setAnalyticId(analytic?.analytic_id));
33
33
  // eslint-disable-next-line react-hooks/exhaustive-deps
34
34
  }, [hit?.howler.analytic]);
35
- const providerColor = useMemo(() => PROVIDER_COLORS[hit.event?.provider ?? 'unknown'] ?? stringToColor(hit.event.provider), [hit.event?.provider]);
35
+ const providerColor = useMemo(() => {
36
+ if (!hit?.event.provider) {
37
+ return PROVIDER_COLORS.unknown;
38
+ }
39
+ return PROVIDER_COLORS[hit?.event.provider] ?? stringToColor(hit?.event.provider);
40
+ }, [hit?.event.provider]);
36
41
  const mitreId = useMemo(() => {
37
42
  if (hit.threat?.framework?.toLowerCase().startsWith('mitre')) {
38
43
  return;
@@ -104,6 +109,6 @@ const HitBanner = ({ hit, layout = HitLayout.NORMAL, showAssigned = true }) => {
104
109
  width: theme.spacing(3)
105
110
  }
106
111
  }
107
- ], children: [_jsx(HitTimestamp, { hit: hit, layout: layout }), showAssigned && _jsx(Assigned, { hit: hit, layout: layout }), _jsxs(Stack, { direction: "row", spacing: layout !== HitLayout.COMFY ? 0.5 : 1, children: [_jsx(EscalationChip, { hit: hit, layout: layout }), ['in-progress', 'on-hold'].includes(hit.howler.status) && (_jsx(Chip, { sx: { width: 'fit-content', display: 'inline-flex' }, label: hit.howler.status, size: layout !== HitLayout.COMFY ? 'small' : 'medium', color: "primary" })), hit.howler.related && (_jsx(Chip, { size: layout !== HitLayout.COMFY ? 'small' : 'medium', label: t('hit.header.related', { count: hit.howler.related.length }) }))] }), howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.status`, { hit, layout }))] })] }));
112
+ ], children: [_jsx(HitTimestamp, { hit: hit, layout: layout }), showAssigned && _jsx(Assigned, { hit: hit, layout: layout }), _jsxs(Stack, { direction: "row", spacing: layout !== HitLayout.COMFY ? 0.5 : 1, children: [_jsx(EscalationChip, { hit: hit, layout: layout }), ['in-progress', 'on-hold'].includes(hit.howler.status) && (_jsx(Chip, { sx: { width: 'fit-content', display: 'inline-flex' }, label: hit.howler.status, size: layout !== HitLayout.COMFY ? 'small' : 'medium', color: "primary" }))] }), hit.howler.related && (_jsx(Chip, { size: layout !== HitLayout.COMFY ? 'small' : 'medium', label: t('hit.header.related', { count: hit.howler.related.length }) })), howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.status`, { hit, layout }))] })] }));
108
113
  };
109
114
  export default HitBanner;
@@ -1,10 +1,11 @@
1
1
  import type { Task } from '@cccsaurora/howler-ui/models/entities/generated/Task';
2
2
  import { type FC } from 'react';
3
3
  declare const CaseTask: FC<{
4
- task: Task;
4
+ task?: Task;
5
5
  paths: string[];
6
- onDelete: () => void;
7
- onEdit: (task: Partial<Task>) => Promise<void>;
6
+ onDelete?: () => Promise<void>;
7
+ onEdit: (task?: Partial<Task>) => Promise<void>;
8
8
  loading?: boolean;
9
+ newTask?: boolean;
9
10
  }>;
10
11
  export default CaseTask;
@@ -2,45 +2,52 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Check, Close, Delete, Edit } from '@mui/icons-material';
3
3
  import { Autocomplete, Card, Checkbox, Chip, IconButton, LinearProgress, Stack, TextField, Tooltip, Typography } from '@mui/material';
4
4
  import UserList from '@cccsaurora/howler-ui/components/elements/UserList';
5
- import { useState } from 'react';
5
+ import { useEffect, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Link } from 'react-router-dom';
8
- const CaseTask = ({ task, onEdit, onDelete, paths }) => {
8
+ const CaseTask = ({ task, onEdit, onDelete, paths, newTask = false }) => {
9
9
  const { t } = useTranslation();
10
- const [editing, setEditing] = useState(false);
10
+ const [editing, setEditing] = useState(newTask);
11
11
  const [loading, setLoading] = useState(false);
12
- const [summary, setSummary] = useState(task.summary);
13
- const [path, setPath] = useState(task.path);
14
- const dirty = summary !== task.summary || path !== task.path;
15
- const onOwnerChange = async ([assignment]) => {
16
- setLoading(true);
17
- await onEdit({
18
- assignment
19
- });
20
- setLoading(false);
21
- };
12
+ const [summary, setSummary] = useState(task?.summary || '');
13
+ const [path, setPath] = useState(task?.path ?? null);
14
+ const [assignment, setAssignment] = useState(task?.assignment);
15
+ const [complete, setComplete] = useState(task?.complete ?? false);
16
+ const dirty = summary !== task?.summary || path !== task?.path;
22
17
  const onSubmit = async () => {
23
- if (dirty) {
18
+ if (dirty && editing) {
19
+ console.log('confirmed bongo time');
24
20
  setLoading(true);
25
- await onEdit({ summary, path: !path ? null : path });
21
+ await onEdit({ summary, path: !path ? null : path, assignment });
26
22
  setLoading(false);
27
23
  }
28
24
  };
29
- return (_jsxs(Card, { sx: { pl: 0.5, pr: 1, py: 0.5, position: 'relative' }, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Checkbox, { disabled: loading, color: "success", checked: task.complete, size: "small", onChange: async (_ev, complete) => {
30
- try {
25
+ useEffect(() => {
26
+ if (!editing && task?.assignment !== assignment) {
27
+ console.log('confirmed bongo time 2');
28
+ setLoading(true);
29
+ onEdit({ assignment }).finally(() => setLoading(false));
30
+ }
31
+ // eslint-disable-next-line react-hooks/exhaustive-deps
32
+ }, [assignment]);
33
+ useEffect(() => {
34
+ if (!editing && task?.complete !== complete) {
35
+ console.log('confirmed bongo time 3');
36
+ setLoading(true);
37
+ onEdit({ complete }).finally(() => setLoading(false));
38
+ }
39
+ // eslint-disable-next-line react-hooks/exhaustive-deps
40
+ }, [complete]);
41
+ return (_jsxs(Card, { sx: { pl: 0.5, pr: 1, py: 0.5, position: 'relative' }, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Checkbox, { disabled: loading, color: "success", checked: complete, size: "small", onChange: (_ev, _complete) => setComplete(_complete) }), editing ? (_jsx(TextField, { disabled: loading, value: summary, onChange: e => setSummary(e.target.value), size: "small", fullWidth: true, sx: { minWidth: '40%' } })) : (_jsx(Typography, { sx: [task?.complete && { textDecoration: 'line-through' }], children: task?.summary || summary })), !editing && task?.path && _jsx(Chip, { clickable: true, component: Link, to: task.path, label: task.path }), editing && (_jsx(Autocomplete, { disabled: loading, value: path, options: paths, onChange: (_ev, value) => setPath(value), fullWidth: true, renderInput: params => _jsx(TextField, { ...params, size: "small" }) })), _jsx(UserList, { disabled: loading, userIds: [assignment], onChange: ([_assigment]) => setAssignment(_assigment), i18nLabel: "route.cases.task.set.assignment", avatarHeight: 24 }), _jsx("div", { style: { flex: 1 } }), editing && (_jsx(Tooltip, { title: t('route.cases.task.delete'), children: _jsx(IconButton, { size: "small", color: "error", onClick: () => {
31
42
  setLoading(true);
32
- await onEdit({ complete });
33
- }
34
- finally {
35
- setLoading(false);
36
- }
37
- } }), editing ? (_jsx(TextField, { disabled: loading, value: summary, onChange: e => setSummary(e.target.value), size: "small", fullWidth: true, sx: { minWidth: '40%' } })) : (_jsx(Typography, { sx: [task.complete && { textDecoration: 'line-through' }], children: task.summary })), task.path && !editing && _jsx(Chip, { clickable: true, component: Link, to: task.path, label: task.path }), editing && (_jsx(Autocomplete, { disabled: loading, value: path, options: paths, onChange: (_ev, value) => setPath(value), fullWidth: true, renderInput: params => _jsx(TextField, { ...params, size: "small" }) })), task.assignment && (_jsx(UserList, { disabled: loading, userIds: [task.assignment], onChange: onOwnerChange, i18nLabel: "route.cases.task.set.assignment", avatarHeight: 24 })), _jsx("div", { style: { flex: 1 } }), editing && (_jsx(Tooltip, { title: t('route.cases.task.delete'), children: _jsx(IconButton, { size: "small", color: "error", onClick: onDelete, children: _jsx(Delete, { fontSize: "small" }) }) })), _jsx(Tooltip, { title: t(editing ? 'route.cases.task.edit.save' : 'route.cases.task.edit'), children: _jsx(IconButton, { size: "small", color: editing ? 'success' : 'default', onClick: () => {
38
- if (!editing) {
39
- setEditing(true);
40
- return;
41
- }
42
- setEditing(false);
43
- onSubmit();
44
- }, disabled: (!dirty && editing) || loading, children: editing ? _jsx(Check, { fontSize: "small" }) : _jsx(Edit, { fontSize: "small" }) }) }), editing && (_jsx(Tooltip, { title: t('route.cases.task.edit.cancel'), children: _jsx(IconButton, { size: "small", onClick: () => setEditing(false), disabled: loading, children: _jsx(Close, { fontSize: "small" }) }) }))] }), loading && _jsx(LinearProgress, { sx: { left: 0, bottom: 0, right: 0, position: 'absolute' } })] }, task.id));
43
+ onDelete().then(() => setLoading(false));
44
+ }, disabled: loading, children: _jsx(Delete, { fontSize: "small" }) }) })), _jsx(Tooltip, { title: t(editing ? 'route.cases.task.edit.save' : 'route.cases.task.edit'), children: _jsx("span", { children: _jsx(IconButton, { size: "small", color: editing ? 'success' : 'default', onClick: async () => {
45
+ if (!editing) {
46
+ setEditing(true);
47
+ return;
48
+ }
49
+ await onSubmit();
50
+ setEditing(false);
51
+ }, disabled: (!dirty && editing) || loading || !summary, children: editing ? _jsx(Check, { fontSize: "small" }) : _jsx(Edit, { fontSize: "small" }) }) }) }), editing && (_jsx(Tooltip, { title: t('route.cases.task.edit.cancel'), children: _jsx(IconButton, { size: "small", onClick: () => setEditing(false), disabled: loading, children: _jsx(Close, { fontSize: "small" }) }) }))] }), loading && _jsx(LinearProgress, { sx: { left: 0, bottom: 0, right: 0, position: 'absolute' } })] }));
45
52
  };
46
53
  export default CaseTask;
@@ -1,23 +1,52 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Add } from '@mui/icons-material';
2
3
  import { Divider, Skeleton, Stack, Typography } from '@mui/material';
3
- import {} from 'react';
4
+ import { useState } from 'react';
4
5
  import { useTranslation } from 'react-i18next';
5
6
  import CaseTask from './CaseTask';
6
7
  const TaskPanel = ({ case: _case, updateCase }) => {
7
8
  const { t } = useTranslation();
9
+ const [addingTask, setAddingTask] = useState(false);
10
+ const onEdit = (task) => async (newTask) => {
11
+ if (task) {
12
+ await updateCase({
13
+ tasks: _case.tasks.map(_task => {
14
+ if (_task.id !== task.id) {
15
+ return _task;
16
+ }
17
+ return {
18
+ ..._task,
19
+ ...newTask
20
+ };
21
+ })
22
+ });
23
+ }
24
+ else {
25
+ await updateCase({
26
+ tasks: [..._case.tasks, newTask]
27
+ });
28
+ }
29
+ };
8
30
  if (!_case) {
9
31
  return _jsx(Skeleton, { height: 240 });
10
32
  }
11
- return (_jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.tasks') }), _jsx(Divider, {}), _case.tasks.map(task => (_jsx(CaseTask, { task: task, paths: _case.items.map(item => item.path), onEdit: newTask => updateCase({
12
- tasks: _case.tasks.map(_task => {
13
- if (_task.id !== task.id) {
14
- return _task;
15
- }
16
- return {
17
- ..._task,
18
- ...newTask
19
- };
20
- })
21
- }), onDelete: () => updateCase({ tasks: _case.tasks.filter(_task => _task.id !== task.id) }) }, task.id)))] }));
33
+ return (_jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.tasks') }), _jsx(Divider, {}), _case.tasks.map(task => (_jsx(CaseTask, { task: task, paths: _case.items.map(item => item.path), onEdit: onEdit(task), onDelete: () => updateCase({ tasks: _case.tasks.filter(_task => _task.id !== task.id) }) }, task.id))), addingTask && (_jsx(CaseTask, { newTask: true, paths: _case.items.map(item => item.path), onEdit: async (task) => {
34
+ await onEdit()(task);
35
+ setAddingTask(false);
36
+ }, onDelete: async () => setAddingTask(false) })), _jsxs(Stack, { onClick: () => setAddingTask(true), direction: "row", spacing: 2, sx: theme => ({
37
+ borderStyle: 'dashed',
38
+ borderColor: theme.palette.text.secondary,
39
+ borderWidth: '0.15rem',
40
+ borderRadius: '0.15rem',
41
+ opacity: 0.3,
42
+ justifyContent: 'center',
43
+ alignItems: 'center',
44
+ padding: 1,
45
+ transition: theme.transitions.create('opacity'),
46
+ '&:hover': {
47
+ opacity: 1,
48
+ cursor: 'pointer'
49
+ }
50
+ }), children: [_jsx(Add, {}), _jsx(Typography, { children: t('page.cases.dashboard.tasks.add') })] })] }));
22
51
  };
23
52
  export default TaskPanel;
@@ -143,7 +143,7 @@ const InformationPane = ({ onClose, selected: _selected }) => {
143
143
  }[tab]?.();
144
144
  }, [dossiers, filter, hit, loading, tab, users]);
145
145
  const hasError = useMemo(() => !validateRegex(filter), [filter]);
146
- return (_jsxs(VSBox, { top: 10, sx: { height: '100%', flex: 1 }, children: [_jsxs(Stack, { direction: "column", flex: 1, sx: { overflowY: 'auto', flexGrow: 1 }, position: "relative", spacing: 1, ml: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, flexShrink: 0, pr: 2, children: [_jsx(FlexOne, {}), onClose && !location.pathname.startsWith('/bundles') && (_jsx(TuiIconButton, { size: "small", onClick: onClose, tooltip: t('hit.panel.details.exit'), children: _jsx(Clear, {}) })), _jsx(SocketBadge, { size: "small" }), analytic && (_jsx(TuiIconButton, { size: "small", tooltip: t('hit.panel.analytic.open'), disabled: !analytic || loading, route: `/analytics/${analytic.analytic_id}`, children: _jsx(QueryStats, {}) })), !!hit && (_jsx(TuiIconButton, { tooltip: t('hit.panel.open'), href: `/hits/${selected}`, disabled: !hit || loading, size: "small", target: "_blank", children: _jsx(OpenInNew, {}) }))] }), _jsx(Box, { pr: 2, children: loading ? _jsx(Skeleton, { variant: "rounded", height: 152 }) : _jsx(HitBanner, { layout: HitLayout.DENSE, hit: hit }) }), !!hit &&
146
+ return (_jsxs(VSBox, { top: 10, sx: { height: '100%', flex: 1 }, children: [_jsxs(Stack, { direction: "column", flex: 1, sx: { overflowY: 'auto', flexGrow: 1 }, position: "relative", spacing: 1, ml: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, flexShrink: 0, pr: 2, children: [_jsx(FlexOne, {}), onClose && !location.pathname.startsWith('/bundles') && (_jsx(TuiIconButton, { size: "small", onClick: onClose, tooltip: t('hit.panel.details.exit'), children: _jsx(Clear, {}) })), _jsx(SocketBadge, { size: "small" }), analytic && (_jsx(TuiIconButton, { size: "small", tooltip: t('hit.panel.analytic.open'), disabled: !analytic || loading, route: `/analytics/${analytic.analytic_id}`, children: _jsx(QueryStats, {}) })), !!hit && (_jsx(TuiIconButton, { tooltip: t('hit.panel.open'), href: `/hits/${selected}`, disabled: !hit || loading, size: "small", target: "_blank", children: _jsx(OpenInNew, {}) }))] }), _jsx(Box, { pr: 2, children: loading || !hit ? (_jsx(Skeleton, { variant: "rounded", height: 152 })) : (_jsx(HitBanner, { layout: HitLayout.DENSE, hit: hit })) }), !!hit &&
147
147
  (!loading ? (_jsxs(_Fragment, { children: [_jsx(HitOutline, { hit: hit, layout: HitLayout.DENSE }), _jsx(HitLabels, { hit: hit })] })) : (_jsx(Skeleton, { height: 124 }))), (hit?.howler?.links?.length > 0 ||
148
148
  analytic?.notebooks?.length > 0 ||
149
149
  dossiers.filter(_dossier => _dossier.pivots?.length > 0).length > 0) && (_jsxs(Stack, { direction: "row", spacing: 1, pr: 2, children: [analytic?.notebooks?.length > 0 && _jsx(HitNotebooks, { analytic: analytic, hit: hit }), hit?.howler?.links?.length > 0 &&
@@ -114,7 +114,6 @@
114
114
  "help.hit.banner.description": "See the below hit banner example for the hit keys necessary to properly populate it. If you have any additional questions, ask in the HOWLER support channel.",
115
115
  "help.hit.banner.json": "Here is the hit data used to populate this banner:",
116
116
  "help.hit.banner.title": "Populating the Hit Banner",
117
- "help.hit.bundle.title": "Hit Bundles",
118
117
  "help.hit.labels.title": "Hit Labels",
119
118
  "help.hit.links.title": "Hit Links",
120
119
  "help.hit.schema.description.missing": "No description provided.",
@@ -168,6 +167,7 @@
168
167
  "hit.header.escalation": "Escalation Level: ",
169
168
  "hit.header.indicators": "Indicators",
170
169
  "hit.header.rationale": "Rationale",
170
+ "hit.header.related": "{{count}} related records",
171
171
  "hit.header.scrutiny": "Scrutiny: ",
172
172
  "hit.header.status": "Status: ",
173
173
  "hit.header.summary": "Summary",
@@ -618,7 +618,6 @@
618
618
  "route.help.views": "Views",
619
619
  "route.history": "History mode: See all previous queries",
620
620
  "route.hits": "Alerts",
621
- "route.hits.bundle": "View Bundle",
622
621
  "route.hits.view": "View Hit",
623
622
  "route.home": "User Dashboard",
624
623
  "route.home.add": "Add New Panel",
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.515",
104
+ "version": "2.17.0-dev.517",
105
105
  "exports": {
106
106
  "./i18n": "./i18n.js",
107
107
  "./index.css": "./index.css",