@cccsaurora/howler-ui 3.1.0-dev.1707 → 3.1.0-dev.1712
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/api/index.d.ts +3 -3
- package/api/index.js +7 -7
- package/api/index.test.js +6 -6
- package/api/search/user.d.ts +1 -1
- package/api/v2/case/rules/backfill/count.d.ts +4 -0
- package/api/v2/case/rules/backfill/count.js +8 -0
- package/api/v2/case/rules/backfill/index.d.ts +6 -0
- package/api/v2/case/rules/backfill/index.js +10 -0
- package/api/v2/case/{rules.d.ts → rules/index.d.ts} +2 -0
- package/api/v2/case/{rules.js → rules/index.js} +2 -0
- package/api/v2/index.d.ts +2 -1
- package/api/v2/index.js +2 -1
- package/api/v2/index.test.js +3 -1
- package/api/v2/search/index.d.ts +1 -1
- package/api/v2/search/index.js +2 -2
- package/api/v2/search/index.test.js +2 -2
- package/api/v2/task/index.d.ts +20 -0
- package/api/v2/task/index.js +12 -0
- package/api/v2/task/index.test.d.ts +1 -0
- package/api/v2/task/index.test.js +26 -0
- package/components/app/providers/FieldProvider.js +22 -8
- package/components/app/providers/FieldProvider.test.js +20 -2
- package/components/app/providers/RecordSearchProvider.test.js +22 -17
- package/components/hooks/useMyApi.js +3 -0
- package/components/hooks/useMyUserFunctions.d.ts +5 -5
- package/components/routes/cases/detail/CaseRules.js +67 -4
- package/components/routes/cases/detail/CaseRules.test.js +88 -1
- package/components/routes/hits/search/shared/HitFilter.js +19 -2
- package/components/routes/hits/search/shared/HitFilter.test.js +87 -20
- package/components/routes/home/AddNewCard.js +15 -2
- package/components/routes/home/CasePanel.d.ts +16 -0
- package/components/routes/home/CasePanel.js +105 -0
- package/components/routes/home/CasePanel.test.d.ts +1 -0
- package/components/routes/home/CasePanel.test.js +86 -0
- package/components/routes/home/TasksPanel.d.ts +13 -0
- package/components/routes/home/TasksPanel.js +129 -0
- package/components/routes/home/TasksPanel.test.d.ts +1 -0
- package/components/routes/home/TasksPanel.test.js +173 -0
- package/components/routes/home/ViewCard.d.ts +2 -1
- package/components/routes/home/ViewCard.js +60 -48
- package/components/routes/home/ViewCard.test.js +29 -3
- package/components/routes/home/ViewRefresh.d.ts +14 -9
- package/components/routes/home/ViewRefresh.js +63 -20
- package/components/routes/home/ViewRefresh.test.js +69 -22
- package/components/routes/home/index.js +39 -10
- package/components/routes/home/index.test.js +9 -1
- package/components/routes/views/ViewComposer.test.js +1 -0
- package/locales/en/translation.json +22 -0
- package/locales/fr/translation.json +22 -0
- package/models/entities/HowlerUser.d.ts +1 -1
- package/package.json +7 -1
- package/rest/AxiosClient.d.ts +1 -1
- package/rest/AxiosClient.js +3 -2
- package/rest/AxiosClient.test.js +6 -0
- package/rest/FetchClient.d.ts +1 -1
- package/rest/FetchClient.js +3 -2
- package/rest/FetchClient.test.js +6 -0
- package/rest/index.d.ts +1 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Add, Delete } from '@mui/icons-material';
|
|
3
|
-
import { Button, Card, Chip, IconButton, Stack, Switch, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tooltip, Typography } from '@mui/material';
|
|
2
|
+
import { Add, Delete, History } from '@mui/icons-material';
|
|
3
|
+
import { Button, Card, Chip, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Stack, Switch, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tooltip, Typography } from '@mui/material';
|
|
4
|
+
import { LocalizationProvider } from '@mui/x-date-pickers';
|
|
5
|
+
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
6
|
+
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
|
|
4
7
|
import api from '@cccsaurora/howler-ui/api';
|
|
5
8
|
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
6
9
|
import ConfirmDeleteModal from '@cccsaurora/howler-ui/components/elements/display/modals/ConfirmDeleteModal';
|
|
7
10
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
8
|
-
import
|
|
11
|
+
import dayjs, {} from 'dayjs';
|
|
12
|
+
import { useCallback, useContext, useRef, useState } from 'react';
|
|
9
13
|
import { useTranslation } from 'react-i18next';
|
|
10
14
|
import { useOutletContext } from 'react-router';
|
|
11
15
|
import useCase from '../hooks/useCase';
|
|
@@ -17,6 +21,58 @@ const CaseRules = ({ case: providedCase, caseId }) => {
|
|
|
17
21
|
const routeCase = useOutletContext();
|
|
18
22
|
const { case: _case, update } = useCase({ case: providedCase ?? routeCase, caseId });
|
|
19
23
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
24
|
+
const [backfillRule, setBackfillRule] = useState();
|
|
25
|
+
const [backfillSince, setBackfillSince] = useState(dayjs().subtract(30, 'day'));
|
|
26
|
+
const [backfillCount, setBackfillCount] = useState(null);
|
|
27
|
+
const [backfillLoading, setBackfillLoading] = useState(false);
|
|
28
|
+
const backfillCountRequestId = useRef(0);
|
|
29
|
+
const invalidateBackfillCountRequest = useCallback(() => {
|
|
30
|
+
backfillCountRequestId.current += 1;
|
|
31
|
+
setBackfillLoading(false);
|
|
32
|
+
}, []);
|
|
33
|
+
const handleBackfillDialogClose = useCallback(() => {
|
|
34
|
+
invalidateBackfillCountRequest();
|
|
35
|
+
setBackfillRule(undefined);
|
|
36
|
+
setBackfillCount(null);
|
|
37
|
+
setBackfillSince(dayjs().subtract(30, 'day'));
|
|
38
|
+
}, [invalidateBackfillCountRequest]);
|
|
39
|
+
const handleCountBackfill = useCallback(async () => {
|
|
40
|
+
if (!_case?.case_id || !backfillRule?.rule_id || !backfillSince?.isValid()) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const requestId = ++backfillCountRequestId.current;
|
|
44
|
+
setBackfillLoading(true);
|
|
45
|
+
try {
|
|
46
|
+
const response = await dispatchApi(api.v2.case.rules.backfill.count.post(_case.case_id, backfillRule.rule_id, backfillSince.toISOString()));
|
|
47
|
+
if (response && requestId === backfillCountRequestId.current) {
|
|
48
|
+
setBackfillCount(response.count);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (requestId === backfillCountRequestId.current) {
|
|
53
|
+
setBackfillLoading(false);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}, [_case?.case_id, backfillRule, backfillSince, dispatchApi]);
|
|
57
|
+
const handleBackfillDialogOpen = useCallback((rule) => {
|
|
58
|
+
invalidateBackfillCountRequest();
|
|
59
|
+
setBackfillRule(rule);
|
|
60
|
+
setBackfillCount(null);
|
|
61
|
+
setBackfillSince(dayjs().subtract(30, 'day'));
|
|
62
|
+
}, [invalidateBackfillCountRequest]);
|
|
63
|
+
const handleSubmitBackfill = useCallback(async () => {
|
|
64
|
+
if (!_case?.case_id || !backfillRule?.rule_id || !backfillSince?.isValid() || backfillCount === null) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setBackfillLoading(true);
|
|
68
|
+
try {
|
|
69
|
+
await dispatchApi(api.v2.case.rules.backfill.post(_case.case_id, backfillRule.rule_id, backfillSince.toISOString()));
|
|
70
|
+
handleBackfillDialogClose();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
setBackfillLoading(false);
|
|
74
|
+
}
|
|
75
|
+
}, [_case?.case_id, backfillRule, backfillSince, backfillCount, dispatchApi, handleBackfillDialogClose]);
|
|
20
76
|
const handleCreateRule = useCallback(async (ruleData) => {
|
|
21
77
|
if (!_case?.case_id) {
|
|
22
78
|
return;
|
|
@@ -54,6 +110,13 @@ const CaseRules = ({ case: providedCase, caseId }) => {
|
|
|
54
110
|
fontFamily: 'monospace'
|
|
55
111
|
}, children: rule.query }) }), _jsx(TableCell, { children: _jsx(Stack, { direction: "row", spacing: 0.5, children: (rule.indexes ?? ['hit']).map(idx => (_jsx(Chip, { size: "small", label: idx, variant: "outlined" }, idx))) }) }), _jsx(TableCell, { children: rule.timeframe != null ? (_jsx(Chip, { size: "small", label: rule.expire_after_resolved
|
|
56
112
|
? t('page.cases.rules.timeframe.after_resolved', { days: rule.timeframe })
|
|
57
|
-
: t('page.cases.rules.timeframe.days', { days: rule.timeframe }), color: rule.expire_after_resolved ? 'info' : 'default' })) : (_jsx(Chip, { size: "small", label: t('page.cases.rules.no_expiry'), color: "success" })) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "body2", children: rule.author }) }), _jsx(TableCell, { align: "center", children: _jsx(Switch, { id: `rule-toggle-${rule.rule_id}`, checked: rule.enabled ?? true, onChange: (_e, checked) => handleToggleEnabled(rule.rule_id, checked), size: "small" }) }),
|
|
113
|
+
: t('page.cases.rules.timeframe.days', { days: rule.timeframe }), color: rule.expire_after_resolved ? 'info' : 'default' })) : (_jsx(Chip, { size: "small", label: t('page.cases.rules.no_expiry'), color: "success" })) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "body2", children: rule.author }) }), _jsx(TableCell, { align: "center", children: _jsx(Switch, { id: `rule-toggle-${rule.rule_id}`, checked: rule.enabled ?? true, onChange: (_e, checked) => handleToggleEnabled(rule.rule_id, checked), size: "small" }) }), _jsxs(TableCell, { align: "right", children: [_jsx(Tooltip, { title: t('page.cases.rules.backfill'), children: _jsx(IconButton, { id: `rule-backfill-${rule.rule_id}`, size: "small", disabled: rule.enabled === false, onClick: () => handleBackfillDialogOpen(rule), children: _jsx(History, { fontSize: "small" }) }) }), _jsx(Tooltip, { title: t('delete'), children: _jsx(IconButton, { id: `rule-delete-${rule.rule_id}`, size: "small", onClick: () => handleDeleteRule(rule.rule_id), children: _jsx(Delete, { fontSize: "small" }) }) })] })] }, rule.rule_id))) })] }) })) }), _jsx(CreateRuleDialog, { open: dialogOpen, onClose: () => setDialogOpen(false), onSubmit: handleCreateRule }), _jsxs(Dialog, { open: !!backfillRule, onClose: handleBackfillDialogClose, maxWidth: "sm", fullWidth: true, id: "rule-backfill-dialog", children: [_jsx(DialogTitle, { children: t('page.cases.rules.backfill') }), _jsx(DialogContent, { children: _jsxs(Stack, { spacing: 2, sx: { mt: 1 }, children: [_jsx(Typography, { children: t('page.cases.rules.backfill.description') }), _jsx(LocalizationProvider, { dateAdapter: AdapterDayjs, children: _jsx(DateTimePicker, { label: t('page.cases.rules.backfill.since'), value: backfillSince, onChange: value => {
|
|
114
|
+
if (!value) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
invalidateBackfillCountRequest();
|
|
118
|
+
setBackfillSince(value);
|
|
119
|
+
setBackfillCount(null);
|
|
120
|
+
}, maxDateTime: dayjs(), ampm: false, slotProps: { textField: { id: 'rule-backfill-since', size: 'small', fullWidth: true } } }) }), backfillCount !== null && (_jsx(Typography, { id: "rule-backfill-confirmation", children: t('page.cases.rules.backfill.confirm', { count: backfillCount }) }))] }) }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: handleBackfillDialogClose, children: t('cancel') }), backfillCount === null ? (_jsx(Button, { id: "rule-backfill-count-button", variant: "contained", onClick: handleCountBackfill, disabled: backfillLoading || !backfillSince?.isValid(), children: t('page.cases.rules.backfill.preview') })) : (_jsx(Button, { id: "rule-backfill-submit-button", variant: "contained", onClick: handleSubmitBackfill, disabled: backfillLoading || backfillCount === 0, children: t('page.cases.rules.backfill.submit') }))] })] })] }));
|
|
58
121
|
};
|
|
59
122
|
export default CaseRules;
|
|
@@ -6,6 +6,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
6
6
|
const mockDispatchApi = vi.hoisted(() => vi.fn());
|
|
7
7
|
const mockUpdate = vi.hoisted(() => vi.fn());
|
|
8
8
|
const mockShowModal = vi.hoisted(() => vi.fn());
|
|
9
|
+
const mockTranslate = vi.hoisted(() => vi.fn((key, options) => (options?.count === undefined ? key : `${key}:${options.count}`)));
|
|
9
10
|
const mockCase = vi.hoisted(() => ({
|
|
10
11
|
current: {
|
|
11
12
|
case_id: 'case-001',
|
|
@@ -16,6 +17,9 @@ const mockCase = vi.hoisted(() => ({
|
|
|
16
17
|
vi.mock('components/hooks/useMyApi', () => ({
|
|
17
18
|
default: () => ({ dispatchApi: mockDispatchApi })
|
|
18
19
|
}));
|
|
20
|
+
vi.mock('react-i18next', () => ({
|
|
21
|
+
useTranslation: () => ({ t: mockTranslate })
|
|
22
|
+
}));
|
|
19
23
|
vi.mock('components/app/providers/ModalProvider', async () => {
|
|
20
24
|
const { createContext } = await import('react');
|
|
21
25
|
return {
|
|
@@ -47,6 +51,12 @@ vi.mock('./CreateRuleDialog', () => ({
|
|
|
47
51
|
return open ? (_jsxs("div", { id: "create-rule-dialog", children: [_jsx("button", { id: "rule-submit-button", onClick: () => onSubmit({ query: 'event.kind:alert', destination: 'alerts/incoming' }), children: "submit" }), _jsx("button", { onClick: onClose, children: "cancel" })] })) : null;
|
|
48
52
|
}
|
|
49
53
|
}));
|
|
54
|
+
vi.mock('@mui/x-date-pickers/DateTimePicker', async () => {
|
|
55
|
+
const { default: dayjs } = await import('dayjs');
|
|
56
|
+
return {
|
|
57
|
+
DateTimePicker: ({ onChange }) => (_jsx("button", { id: "rule-backfill-since", onClick: () => onChange(dayjs('2025-06-01')), children: "pick date" }))
|
|
58
|
+
};
|
|
59
|
+
});
|
|
50
60
|
vi.mock('api', () => ({
|
|
51
61
|
default: {
|
|
52
62
|
v2: {
|
|
@@ -54,7 +64,11 @@ vi.mock('api', () => ({
|
|
|
54
64
|
rules: {
|
|
55
65
|
post: vi.fn(),
|
|
56
66
|
del: vi.fn(),
|
|
57
|
-
put: vi.fn()
|
|
67
|
+
put: vi.fn(),
|
|
68
|
+
backfill: {
|
|
69
|
+
post: vi.fn(),
|
|
70
|
+
count: { post: vi.fn() }
|
|
71
|
+
}
|
|
58
72
|
}
|
|
59
73
|
}
|
|
60
74
|
}
|
|
@@ -73,6 +87,13 @@ const makeRule = (overrides) => ({
|
|
|
73
87
|
indexes: ['hit'],
|
|
74
88
|
...overrides
|
|
75
89
|
});
|
|
90
|
+
const createDeferred = () => {
|
|
91
|
+
let resolve;
|
|
92
|
+
const promise = new Promise(resolvePromise => {
|
|
93
|
+
resolve = resolvePromise;
|
|
94
|
+
});
|
|
95
|
+
return { promise, resolve };
|
|
96
|
+
};
|
|
76
97
|
describe('CaseRules', () => {
|
|
77
98
|
beforeEach(() => {
|
|
78
99
|
vi.clearAllMocks();
|
|
@@ -222,6 +243,72 @@ describe('CaseRules', () => {
|
|
|
222
243
|
expect(mockDispatchApi).toHaveBeenCalledWith('put-request');
|
|
223
244
|
});
|
|
224
245
|
});
|
|
246
|
+
it('previews and confirms the number of matching alerts before backfilling', async () => {
|
|
247
|
+
const user = userEvent.setup();
|
|
248
|
+
mockCase.current = createMockCase({
|
|
249
|
+
case_id: 'case-001',
|
|
250
|
+
rules: [makeRule()]
|
|
251
|
+
});
|
|
252
|
+
mockDispatchApi.mockResolvedValueOnce({ count: 23 }).mockResolvedValueOnce({ queued: 23 });
|
|
253
|
+
vi.mocked(api.v2.case.rules.backfill.count.post).mockReturnValue('count-backfill-request');
|
|
254
|
+
vi.mocked(api.v2.case.rules.backfill.post).mockReturnValue('backfill-request');
|
|
255
|
+
render(_jsx(CaseRules, {}));
|
|
256
|
+
await user.click(screen.getByTestId('rule-backfill-rule-001'));
|
|
257
|
+
await user.click(screen.getByTestId('rule-backfill-count-button'));
|
|
258
|
+
await waitFor(() => {
|
|
259
|
+
expect(screen.getByTestId('rule-backfill-confirmation')).toHaveTextContent('page.cases.rules.backfill.confirm:23');
|
|
260
|
+
});
|
|
261
|
+
expect(mockDispatchApi).toHaveBeenCalledWith('count-backfill-request');
|
|
262
|
+
await user.click(screen.getByTestId('rule-backfill-submit-button'));
|
|
263
|
+
await waitFor(() => {
|
|
264
|
+
expect(mockDispatchApi).toHaveBeenCalledWith('backfill-request');
|
|
265
|
+
expect(screen.queryByTestId('rule-backfill-dialog')).not.toBeInTheDocument();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
it('ignores a pending count response after closing and reopening for another rule', async () => {
|
|
269
|
+
const user = userEvent.setup();
|
|
270
|
+
mockCase.current = createMockCase({
|
|
271
|
+
case_id: 'case-001',
|
|
272
|
+
rules: [makeRule(), makeRule({ rule_id: 'rule-002' })]
|
|
273
|
+
});
|
|
274
|
+
const pendingCount = createDeferred();
|
|
275
|
+
mockDispatchApi.mockReturnValueOnce(pendingCount.promise);
|
|
276
|
+
vi.mocked(api.v2.case.rules.backfill.count.post).mockReturnValue('count-backfill-request');
|
|
277
|
+
render(_jsx(CaseRules, {}));
|
|
278
|
+
await user.click(screen.getByTestId('rule-backfill-rule-001'));
|
|
279
|
+
await user.click(screen.getByTestId('rule-backfill-count-button'));
|
|
280
|
+
await user.click(screen.getByText('cancel'));
|
|
281
|
+
await waitFor(() => {
|
|
282
|
+
expect(screen.queryByTestId('rule-backfill-dialog')).not.toBeInTheDocument();
|
|
283
|
+
});
|
|
284
|
+
await user.click(screen.getByTestId('rule-backfill-rule-002'));
|
|
285
|
+
await act(async () => {
|
|
286
|
+
pendingCount.resolve({ count: 23 });
|
|
287
|
+
await pendingCount.promise;
|
|
288
|
+
});
|
|
289
|
+
expect(screen.getByTestId('rule-backfill-count-button')).toBeInTheDocument();
|
|
290
|
+
expect(screen.queryByTestId('rule-backfill-confirmation')).not.toBeInTheDocument();
|
|
291
|
+
});
|
|
292
|
+
it('ignores a pending count response after the preview date changes', async () => {
|
|
293
|
+
const user = userEvent.setup();
|
|
294
|
+
mockCase.current = createMockCase({
|
|
295
|
+
case_id: 'case-001',
|
|
296
|
+
rules: [makeRule()]
|
|
297
|
+
});
|
|
298
|
+
const pendingCount = createDeferred();
|
|
299
|
+
mockDispatchApi.mockReturnValueOnce(pendingCount.promise);
|
|
300
|
+
vi.mocked(api.v2.case.rules.backfill.count.post).mockReturnValue('count-backfill-request');
|
|
301
|
+
render(_jsx(CaseRules, {}));
|
|
302
|
+
await user.click(screen.getByTestId('rule-backfill-rule-001'));
|
|
303
|
+
await user.click(screen.getByTestId('rule-backfill-count-button'));
|
|
304
|
+
await user.click(screen.getByTestId('rule-backfill-since'));
|
|
305
|
+
await act(async () => {
|
|
306
|
+
pendingCount.resolve({ count: 23 });
|
|
307
|
+
await pendingCount.promise;
|
|
308
|
+
});
|
|
309
|
+
expect(screen.getByTestId('rule-backfill-count-button')).toBeEnabled();
|
|
310
|
+
expect(screen.queryByTestId('rule-backfill-confirmation')).not.toBeInTheDocument();
|
|
311
|
+
});
|
|
225
312
|
it('renders multiple rules', () => {
|
|
226
313
|
mockCase.current = createMockCase({
|
|
227
314
|
case_id: 'case-001',
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { AddCircleOutline, FilterList, RemoveCircleOutline } from '@mui/icons-material';
|
|
3
|
-
import { Autocomplete, Checkbox, FormControlLabel, Stack, TextField, Tooltip, Typography } from '@mui/material';
|
|
3
|
+
import { Autocomplete, Checkbox, createFilterOptions, FormControlLabel, Stack, TextField, Tooltip, Typography } from '@mui/material';
|
|
4
4
|
import api from '@cccsaurora/howler-ui/api';
|
|
5
5
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
6
|
+
import { FieldContext } from '@cccsaurora/howler-ui/components/app/providers/FieldProvider';
|
|
6
7
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
7
8
|
import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
|
|
8
9
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
@@ -19,6 +20,13 @@ const ACCEPTED_LOOKUPS = [
|
|
|
19
20
|
'event.provider',
|
|
20
21
|
'organization.name'
|
|
21
22
|
];
|
|
23
|
+
const DEFAULT_FILTER_FIELDS = [
|
|
24
|
+
'howler.assessment',
|
|
25
|
+
'howler.escalation',
|
|
26
|
+
'howler.analytic',
|
|
27
|
+
'howler.detection',
|
|
28
|
+
'event.provider'
|
|
29
|
+
];
|
|
22
30
|
const parseFilter = (value) => {
|
|
23
31
|
const negated = value.startsWith('-');
|
|
24
32
|
const positiveValue = negated ? value.slice(1) : value;
|
|
@@ -72,9 +80,11 @@ const serializeFilter = (category, values, negated) => {
|
|
|
72
80
|
}
|
|
73
81
|
return filter.join('');
|
|
74
82
|
};
|
|
83
|
+
const filterFields = createFilterOptions();
|
|
75
84
|
const HitFilter = ({ size, id, value }) => {
|
|
76
85
|
const { t } = useTranslation();
|
|
77
86
|
const { config } = useContext(ApiConfigContext);
|
|
87
|
+
const { hitFields, getHitFields } = useContext(FieldContext);
|
|
78
88
|
const { dispatchApi } = useMyApi();
|
|
79
89
|
const setSavedFilter = useContextSelector(ParameterContext, ctx => ctx.setFilter);
|
|
80
90
|
const removeSavedFilter = useContextSelector(ParameterContext, ctx => ctx.removeFilter);
|
|
@@ -89,10 +99,15 @@ const HitFilter = ({ size, id, value }) => {
|
|
|
89
99
|
category: null,
|
|
90
100
|
options: []
|
|
91
101
|
});
|
|
102
|
+
const [categorySearchInput, setCategorySearchInput] = useState('');
|
|
92
103
|
const { category, values: filterValues, editable, negated } = parsedFilter;
|
|
93
104
|
const configuredLookup = category ? config.lookups?.[category] : undefined;
|
|
94
105
|
const needsLookups = !!category && editable && !Array.isArray(configuredLookup);
|
|
95
106
|
const loading = needsLookups && customLookups.category !== category;
|
|
107
|
+
const categoryOptions = hitFields.map(field => field.key).filter((field) => !!field);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
void getHitFields();
|
|
110
|
+
}, [getHitFields]);
|
|
96
111
|
useEffect(() => {
|
|
97
112
|
if (value) {
|
|
98
113
|
setSavedFilter(id, value);
|
|
@@ -142,6 +157,8 @@ const HitFilter = ({ size, id, value }) => {
|
|
|
142
157
|
}, [commitFilter, parsedFilter.raw]);
|
|
143
158
|
const lookupOptions = configuredLookup ?? (customLookups.category === category ? customLookups.options : []);
|
|
144
159
|
const chipLabel = `${parsedFilter.category ? `${parsedFilter.category}:` : ''}${parsedFilter.rawClause}`;
|
|
145
|
-
return (_jsx(Stack, { direction: "row", alignItems: "center", spacing: 0.5, children: _jsx(ChipPopper, { icon: _jsxs(Stack, { direction: "row", spacing: 0.25, alignItems: "center", children: [_jsx(FilterList, { fontSize: "small" }), parsedFilter.negated ? (_jsx(Tooltip, { title: t('hit.search.filter.excluded'), children: _jsx(RemoveCircleOutline, { fontSize: "small", color: "error" }) })) : (_jsx(Tooltip, { title: t('hit.search.filter.included'), children: _jsx(AddCircleOutline, { fontSize: "small", color: "success" }) }))] }), label: _jsx(Typography, { variant: "body2", children: chipLabel }), minWidth: "250px", onDelete: () => removeSavedFilter(value), slotProps: { chip: { size: 'small', color: parsedFilter.wildcard ? 'warning' : 'default' } }, children: _jsxs(Stack, { spacing: 1, sx: { minWidth: '225px' }, children: [_jsx(FormControlLabel, { label: t('hit.search.filter.exclude'), control: _jsx(Checkbox, { size: "small", checked: negated, onChange: toggleNegation }) }), _jsx(Autocomplete, { fullWidth: true, disableClearable: true, disabled: !editable, size: size ?? 'small', value: category ?? ACCEPTED_LOOKUPS[0], options:
|
|
160
|
+
return (_jsx(Stack, { direction: "row", alignItems: "center", spacing: 0.5, children: _jsx(ChipPopper, { icon: _jsxs(Stack, { direction: "row", spacing: 0.25, alignItems: "center", children: [_jsx(FilterList, { fontSize: "small" }), parsedFilter.negated ? (_jsx(Tooltip, { title: t('hit.search.filter.excluded'), children: _jsx(RemoveCircleOutline, { fontSize: "small", color: "error" }) })) : (_jsx(Tooltip, { title: t('hit.search.filter.included'), children: _jsx(AddCircleOutline, { fontSize: "small", color: "success" }) }))] }), label: _jsx(Typography, { variant: "body2", children: chipLabel }), minWidth: "250px", onDelete: () => removeSavedFilter(value), slotProps: { chip: { size: 'small', color: parsedFilter.wildcard ? 'warning' : 'default' } }, children: _jsxs(Stack, { spacing: 1, sx: { minWidth: '225px' }, children: [_jsx(FormControlLabel, { label: t('hit.search.filter.exclude'), control: _jsx(Checkbox, { size: "small", checked: negated, onChange: toggleNegation }) }), _jsx(Autocomplete, { fullWidth: true, disableClearable: true, disabled: !editable, size: size ?? 'small', value: category ?? ACCEPTED_LOOKUPS[0], options: categoryOptions, filterOptions: (options, state) => categorySearchInput
|
|
161
|
+
? filterFields(options, { ...state, inputValue: categorySearchInput })
|
|
162
|
+
: options.filter(option => DEFAULT_FILTER_FIELDS.includes(option)), renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.filter.fields') }), onChange: onCategoryChange, onInputChange: (_event, inputValue, reason) => setCategorySearchInput(reason === 'input' ? inputValue : '') }), _jsx(Autocomplete, { fullWidth: true, freeSolo: true, multiple: true, disabled: !category || !editable, loading: loading, size: size ?? 'small', value: filterValues, options: lookupOptions, renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.filter.values') }), getOptionLabel: option => t(option), onChange: onValuesChange })] }) }) }));
|
|
146
163
|
};
|
|
147
164
|
export default memo(HitFilter);
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/// <reference types="vitest" />
|
|
3
3
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import { useState } from 'react';
|
|
4
5
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
6
|
const apiConfigContextToken = vi.hoisted(() => ({ name: 'api-config-context' }));
|
|
7
|
+
const fieldContextToken = vi.hoisted(() => ({ name: 'field-context' }));
|
|
6
8
|
const parameterContextToken = vi.hoisted(() => ({ name: 'parameter-context' }));
|
|
7
9
|
const mockDispatchApi = vi.hoisted(() => vi.fn());
|
|
8
10
|
const mockSetSavedFilter = vi.hoisted(() => vi.fn());
|
|
9
11
|
const mockRemoveSavedFilter = vi.hoisted(() => vi.fn());
|
|
12
|
+
const autocompleteProps = vi.hoisted(() => []);
|
|
10
13
|
const mockAutocompleteSelection = vi.hoisted(() => ({
|
|
11
14
|
value: 'normal'
|
|
12
15
|
}));
|
|
@@ -19,25 +22,32 @@ vi.mock('@mui/material', async (importOriginal) => {
|
|
|
19
22
|
const actual = await importOriginal();
|
|
20
23
|
return {
|
|
21
24
|
...actual,
|
|
22
|
-
Autocomplete: (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
Autocomplete: (props) => {
|
|
26
|
+
autocompleteProps.push(props);
|
|
27
|
+
const { onChange, options, disabled, multiple, value, loading } = props;
|
|
28
|
+
return (_jsx("button", { disabled: disabled, "data-loading": Boolean(loading), "data-options": JSON.stringify(options), onClick: () => onChange(null, mockAutocompleteSelection.value === 'clear'
|
|
29
|
+
? multiple
|
|
30
|
+
? []
|
|
31
|
+
: null
|
|
32
|
+
: multiple
|
|
33
|
+
? mockAutocompleteSelection.value === 'unchanged'
|
|
34
|
+
? value
|
|
35
|
+
: mockAutocompleteSelection.value === 'add'
|
|
36
|
+
? [...(value ?? []), 'added value']
|
|
37
|
+
: mockAutocompleteSelection.value === 'single'
|
|
38
|
+
? (value ?? []).slice(0, 1)
|
|
39
|
+
: (value?.length ?? 0) > 1
|
|
40
|
+
? [value[0], value[0]?.startsWith('C:') ? 'new\\path' : 'changed two']
|
|
41
|
+
: [value?.[0] ?? options[0] ?? 'changed one']
|
|
42
|
+
: value === '*'
|
|
43
|
+
? '*'
|
|
44
|
+
: options.includes('event.provider')
|
|
45
|
+
? 'event.provider'
|
|
46
|
+
: options[0]), children: "change-filter" }));
|
|
47
|
+
},
|
|
48
|
+
Stack: ({ children }) => _jsx("div", { children: children }),
|
|
49
|
+
TextField: ({ label }) => _jsx("div", { children: label }),
|
|
50
|
+
Typography: ({ children }) => _jsx("div", { children: children })
|
|
41
51
|
};
|
|
42
52
|
});
|
|
43
53
|
vi.mock('api', () => ({
|
|
@@ -54,9 +64,21 @@ vi.mock('api', () => ({
|
|
|
54
64
|
vi.mock('components/app/providers/ApiConfigProvider', () => ({
|
|
55
65
|
ApiConfigContext: apiConfigContextToken
|
|
56
66
|
}));
|
|
67
|
+
vi.mock('components/app/providers/FieldProvider', () => ({
|
|
68
|
+
FieldContext: fieldContextToken
|
|
69
|
+
}));
|
|
57
70
|
vi.mock('components/app/providers/ParameterProvider', () => ({
|
|
58
71
|
ParameterContext: parameterContextToken
|
|
59
72
|
}));
|
|
73
|
+
vi.mock('components/elements/display/ChipPopper', () => ({
|
|
74
|
+
default: ({ children, icon, label, onDelete }) => {
|
|
75
|
+
const [open, setOpen] = useState(false);
|
|
76
|
+
return (_jsxs("div", { children: [_jsxs("button", { className: "MuiChip-root", onClick: () => setOpen(current => !current), children: [icon && _jsx("span", { className: "MuiChip-icon", children: icon }), label, onDelete && (_jsx("span", { className: "MuiChip-deleteIcon", onClick: event => {
|
|
77
|
+
event.stopPropagation();
|
|
78
|
+
onDelete(event);
|
|
79
|
+
}, children: "delete-filter" }))] }), open && children] }));
|
|
80
|
+
}
|
|
81
|
+
}));
|
|
60
82
|
vi.mock('components/hooks/useMyApi', () => ({
|
|
61
83
|
default: () => ({ dispatchApi: mockDispatchApi })
|
|
62
84
|
}));
|
|
@@ -73,6 +95,19 @@ vi.mock('react', async (importOriginal) => {
|
|
|
73
95
|
useContext: (context) => {
|
|
74
96
|
if (context === apiConfigContextToken)
|
|
75
97
|
return { config: configValue };
|
|
98
|
+
if (context === fieldContextToken) {
|
|
99
|
+
return {
|
|
100
|
+
hitFields: [
|
|
101
|
+
{ key: 'howler.assessment' },
|
|
102
|
+
{ key: 'howler.escalation' },
|
|
103
|
+
{ key: 'howler.analytic' },
|
|
104
|
+
{ key: 'howler.detection' },
|
|
105
|
+
{ key: 'event.provider' },
|
|
106
|
+
{ key: 'organization.name' }
|
|
107
|
+
],
|
|
108
|
+
getHitFields: vi.fn()
|
|
109
|
+
};
|
|
110
|
+
}
|
|
76
111
|
return actual.useContext(context);
|
|
77
112
|
}
|
|
78
113
|
};
|
|
@@ -84,6 +119,7 @@ describe('HitFilter', () => {
|
|
|
84
119
|
mockDispatchApi.mockReset().mockImplementation(async (_value) => ({ 'event.provider': { azure: 2 } }));
|
|
85
120
|
mockSetSavedFilter.mockReset();
|
|
86
121
|
mockRemoveSavedFilter.mockReset();
|
|
122
|
+
autocompleteProps.length = 0;
|
|
87
123
|
mockAutocompleteSelection.value = 'normal';
|
|
88
124
|
});
|
|
89
125
|
it('initializes from value, fetches custom lookups, updates values, and removes filters', async () => {
|
|
@@ -121,6 +157,37 @@ describe('HitFilter', () => {
|
|
|
121
157
|
render(_jsx(HitFilter, { id: 2, value: 'howler.assessment:*' }));
|
|
122
158
|
await waitFor(() => expect(mockSetSavedFilter).toHaveBeenCalledWith(2, 'howler.assessment:*'));
|
|
123
159
|
});
|
|
160
|
+
it('shows only default fields until the user searches for another indexed field', () => {
|
|
161
|
+
render(_jsx(HitFilter, { id: 3, value: "howler.assessment:*" }));
|
|
162
|
+
fireEvent.click(screen.getByRole('button', { name: /howler.assessment:\*/ }));
|
|
163
|
+
const getFieldAutocomplete = () => [...autocompleteProps].reverse().find(props => props.options.includes('organization.name'));
|
|
164
|
+
const autocompleteState = {
|
|
165
|
+
inputValue: 'howler.assessment',
|
|
166
|
+
getOptionLabel: (option) => option
|
|
167
|
+
};
|
|
168
|
+
let fieldAutocomplete = getFieldAutocomplete();
|
|
169
|
+
expect(fieldAutocomplete.filterOptions(fieldAutocomplete.options, autocompleteState)).toEqual([
|
|
170
|
+
'howler.assessment',
|
|
171
|
+
'howler.escalation',
|
|
172
|
+
'howler.analytic',
|
|
173
|
+
'howler.detection',
|
|
174
|
+
'event.provider'
|
|
175
|
+
]);
|
|
176
|
+
act(() => fieldAutocomplete.onInputChange(null, 'organization', 'input'));
|
|
177
|
+
fieldAutocomplete = getFieldAutocomplete();
|
|
178
|
+
expect(fieldAutocomplete.filterOptions(fieldAutocomplete.options, autocompleteState)).toEqual([
|
|
179
|
+
'organization.name'
|
|
180
|
+
]);
|
|
181
|
+
act(() => fieldAutocomplete.onInputChange(null, 'howler.assessment', 'reset'));
|
|
182
|
+
fieldAutocomplete = getFieldAutocomplete();
|
|
183
|
+
expect(fieldAutocomplete.filterOptions(fieldAutocomplete.options, autocompleteState)).toEqual([
|
|
184
|
+
'howler.assessment',
|
|
185
|
+
'howler.escalation',
|
|
186
|
+
'howler.analytic',
|
|
187
|
+
'howler.detection',
|
|
188
|
+
'event.provider'
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
124
191
|
it('deduplicates decoded literals while preserving quoted OR text', () => {
|
|
125
192
|
const clause = String.raw `event.provider:("a" OR "\a" OR "text OR more" OR "escaped\"quote")`;
|
|
126
193
|
render(_jsx(HitFilter, { id: 15, value: clause }));
|
|
@@ -7,10 +7,13 @@ import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/View
|
|
|
7
7
|
import CustomButton from '@cccsaurora/howler-ui/components/elements/addons/buttons/CustomButton';
|
|
8
8
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
9
|
import { useTranslation } from 'react-i18next';
|
|
10
|
+
import { v4 as uuid } from 'uuid';
|
|
10
11
|
import { useContextSelector } from 'use-context-selector';
|
|
11
12
|
const TYPES = {
|
|
12
13
|
view: ['viewId', 'limit'],
|
|
13
|
-
analytic: ['analyticId', 'type']
|
|
14
|
+
analytic: ['analyticId', 'type'],
|
|
15
|
+
case: [],
|
|
16
|
+
tasks: []
|
|
14
17
|
};
|
|
15
18
|
const VISUALIZATIONS = ['assessment', 'created', 'escalation', 'status', 'detection'];
|
|
16
19
|
const AddNewCard = ({ dashboard, addCard }) => {
|
|
@@ -29,7 +32,11 @@ const AddNewCard = ({ dashboard, addCard }) => {
|
|
|
29
32
|
return;
|
|
30
33
|
}
|
|
31
34
|
addCard({
|
|
32
|
-
entry_id: selectedType === 'view'
|
|
35
|
+
entry_id: selectedType === 'view'
|
|
36
|
+
? config.viewId
|
|
37
|
+
: selectedType === 'analytic'
|
|
38
|
+
? `${config.analyticId}-${config.type}`
|
|
39
|
+
: `${selectedType}-${uuid()}`,
|
|
33
40
|
type: selectedType,
|
|
34
41
|
config: JSON.stringify(config)
|
|
35
42
|
});
|
|
@@ -81,6 +88,12 @@ const AddNewCard = ({ dashboard, addCard }) => {
|
|
|
81
88
|
limit: 3
|
|
82
89
|
});
|
|
83
90
|
}
|
|
91
|
+
else if (selectedType === 'case') {
|
|
92
|
+
_setConfig({ dateRange: 'date.range.all', statusFilter: [], assigneeFilter: [] });
|
|
93
|
+
}
|
|
94
|
+
else if (selectedType === 'tasks') {
|
|
95
|
+
_setConfig({ taskFilter: 'incomplete' });
|
|
96
|
+
}
|
|
84
97
|
else {
|
|
85
98
|
_setConfig({});
|
|
86
99
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type DateRangeOption } from '@cccsaurora/howler-ui/components/routes/cases/search/CaseDateFilter';
|
|
2
|
+
import { type FC } from 'react';
|
|
3
|
+
export interface CaseSettings {
|
|
4
|
+
statusFilter?: string[];
|
|
5
|
+
assigneeFilter?: string[];
|
|
6
|
+
dateRange?: DateRangeOption;
|
|
7
|
+
customStart?: string;
|
|
8
|
+
customEnd?: string;
|
|
9
|
+
}
|
|
10
|
+
interface CasePanelProps extends CaseSettings {
|
|
11
|
+
panelId?: string;
|
|
12
|
+
refreshTick?: symbol;
|
|
13
|
+
onRefreshComplete?: (panelId: string, refreshTick: symbol) => void;
|
|
14
|
+
}
|
|
15
|
+
declare const CasePanel: FC<CasePanelProps>;
|
|
16
|
+
export default CasePanel;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { OpenInNew } from '@mui/icons-material';
|
|
3
|
+
import { Alert, Card, CardContent, CircularProgress, IconButton, Stack, Tooltip, Typography } from '@mui/material';
|
|
4
|
+
import { AppListEmpty } from '@tui/core';
|
|
5
|
+
import api from '@cccsaurora/howler-ui/api';
|
|
6
|
+
import CaseCard from '@cccsaurora/howler-ui/components/elements/case/CaseCard';
|
|
7
|
+
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
8
|
+
import CaseAssigneeFilter from '@cccsaurora/howler-ui/components/routes/cases/search/CaseAssigneeFilter';
|
|
9
|
+
import CaseDateFilter, {} from '@cccsaurora/howler-ui/components/routes/cases/search/CaseDateFilter';
|
|
10
|
+
import CaseStatusFilter from '@cccsaurora/howler-ui/components/routes/cases/search/CaseStatusFilter';
|
|
11
|
+
import dayjs, {} from 'dayjs';
|
|
12
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
13
|
+
import { useTranslation } from 'react-i18next';
|
|
14
|
+
import { Link } from 'react-router';
|
|
15
|
+
import { DATE_RANGE_LUCENE } from '@cccsaurora/howler-ui/utils/constants';
|
|
16
|
+
import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
17
|
+
const CASE_LIMIT = 10;
|
|
18
|
+
const getDate = (value, fallback) => {
|
|
19
|
+
const date = value ? dayjs(value) : fallback;
|
|
20
|
+
return date.isValid() ? date : fallback;
|
|
21
|
+
};
|
|
22
|
+
const CasePanel = ({ statusFilter: initialStatusFilter = [], assigneeFilter: initialAssigneeFilter = [], dateRange: initialDateRange = 'date.range.all', customStart: initialCustomStart, customEnd: initialCustomEnd, panelId = 'case-panel', refreshTick, onRefreshComplete }) => {
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
const { dispatchApi } = useMyApi();
|
|
25
|
+
const [cases, setCases] = useState([]);
|
|
26
|
+
const [loading, setLoading] = useState(false);
|
|
27
|
+
const [error, setError] = useState(false);
|
|
28
|
+
const [statusFilter, setStatusFilter] = useState(initialStatusFilter);
|
|
29
|
+
const [assigneeFilter, setAssigneeFilter] = useState(initialAssigneeFilter);
|
|
30
|
+
const [dateRange, setDateRange] = useState(initialDateRange);
|
|
31
|
+
const [customStart, setCustomStart] = useState(() => getDate(initialCustomStart, dayjs().subtract(2, 'days')));
|
|
32
|
+
const [customEnd, setCustomEnd] = useState(() => getDate(initialCustomEnd, dayjs()));
|
|
33
|
+
const activeRefreshTick = useRef(undefined);
|
|
34
|
+
const filters = useMemo(() => {
|
|
35
|
+
const nextFilters = [];
|
|
36
|
+
if (statusFilter.length > 0) {
|
|
37
|
+
nextFilters.push(`status:(${statusFilter.map(status => `"${status}"`).join(' OR ')})`);
|
|
38
|
+
}
|
|
39
|
+
if (assigneeFilter.length > 0) {
|
|
40
|
+
nextFilters.push(assigneeFilter
|
|
41
|
+
.map(assignee => `(participants:"${sanitizeLuceneQuery(assignee)}" OR tasks.assignment:"${sanitizeLuceneQuery(assignee)}")`)
|
|
42
|
+
.join(' OR '));
|
|
43
|
+
}
|
|
44
|
+
const luceneDate = DATE_RANGE_LUCENE[dateRange];
|
|
45
|
+
if (luceneDate) {
|
|
46
|
+
nextFilters.push(`created:[${luceneDate} TO now]`);
|
|
47
|
+
}
|
|
48
|
+
else if (dateRange === 'date.range.custom') {
|
|
49
|
+
nextFilters.push(`created:[${customStart.toISOString()} TO ${customEnd.toISOString()}]`);
|
|
50
|
+
}
|
|
51
|
+
return nextFilters;
|
|
52
|
+
}, [assigneeFilter, customEnd, customStart, dateRange, statusFilter]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
let refreshComplete = false;
|
|
56
|
+
const completeRefresh = () => {
|
|
57
|
+
if (refreshTick && !refreshComplete) {
|
|
58
|
+
refreshComplete = true;
|
|
59
|
+
if (activeRefreshTick.current === refreshTick) {
|
|
60
|
+
activeRefreshTick.current = undefined;
|
|
61
|
+
}
|
|
62
|
+
onRefreshComplete?.(panelId, refreshTick);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
if (refreshTick) {
|
|
66
|
+
activeRefreshTick.current = refreshTick;
|
|
67
|
+
}
|
|
68
|
+
setLoading(true);
|
|
69
|
+
setError(false);
|
|
70
|
+
void dispatchApi(api.v2.search.post('case', {
|
|
71
|
+
query: 'case_id:*',
|
|
72
|
+
filters,
|
|
73
|
+
rows: CASE_LIMIT,
|
|
74
|
+
sort: 'created desc'
|
|
75
|
+
}, controller.signal), { showError: false })
|
|
76
|
+
.then(response => {
|
|
77
|
+
if (!controller.signal.aborted) {
|
|
78
|
+
setCases(response?.items ?? []);
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
.catch(() => {
|
|
82
|
+
if (!controller.signal.aborted) {
|
|
83
|
+
setError(true);
|
|
84
|
+
setCases([]);
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
.finally(() => {
|
|
88
|
+
if (!controller.signal.aborted) {
|
|
89
|
+
setLoading(false);
|
|
90
|
+
completeRefresh();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return () => {
|
|
94
|
+
controller.abort();
|
|
95
|
+
};
|
|
96
|
+
}, [dispatchApi, filters, onRefreshComplete, panelId, refreshTick]);
|
|
97
|
+
useEffect(() => () => {
|
|
98
|
+
if (activeRefreshTick.current) {
|
|
99
|
+
onRefreshComplete?.(panelId, activeRefreshTick.current);
|
|
100
|
+
activeRefreshTick.current = undefined;
|
|
101
|
+
}
|
|
102
|
+
}, [onRefreshComplete, panelId]);
|
|
103
|
+
return (_jsx(Card, { variant: "outlined", sx: { height: '100%' }, children: _jsx(CardContent, { sx: { height: '100%' }, children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", children: t('route.home.case.title') }), _jsx(Tooltip, { title: t('route.home.case.open'), children: _jsx(IconButton, { component: Link, to: "/cases", children: _jsx(OpenInNew, { fontSize: "small" }) }) })] }), _jsxs(Stack, { direction: "row", spacing: 1, useFlexGap: true, sx: { flexWrap: 'wrap' }, children: [_jsx(CaseStatusFilter, { statusFilter: statusFilter, onChange: setStatusFilter }), _jsx(CaseAssigneeFilter, { assigneeFilter: assigneeFilter, onChange: setAssigneeFilter }), _jsx(CaseDateFilter, { dateRange: dateRange, onChange: setDateRange, customStart: customStart, customEnd: customEnd, onCustomStartChange: setCustomStart, onCustomEndChange: setCustomEnd })] }), error ? (_jsx(Alert, { severity: "error", children: t('route.home.case.error') })) : loading ? (_jsx(Stack, { alignItems: "center", py: 3, children: _jsx(CircularProgress, { size: 28 }) })) : cases.length > 0 ? (cases.map(_case => (_jsx(Link, { to: `/cases/${_case.case_id}`, style: { color: 'inherit', textDecoration: 'none' }, children: _jsx(CaseCard, { case: _case }) }, _case.case_id)))) : (_jsx(AppListEmpty, {}))] }) }) }));
|
|
104
|
+
};
|
|
105
|
+
export default CasePanel;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|