@cccsaurora/howler-ui 3.1.0-dev.1707 → 3.1.0-dev.1708
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/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/components/routes/cases/detail/CaseRules.js +67 -4
- package/components/routes/cases/detail/CaseRules.test.js +88 -1
- package/locales/en/translation.json +6 -0
- package/locales/fr/translation.json +6 -0
- package/package.json +5 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { hpost, joinAllUri } from '@cccsaurora/howler-ui/api';
|
|
2
|
+
import { uri as caseUri } from '@cccsaurora/howler-ui/api/v2/case';
|
|
3
|
+
export const uri = (caseId, ruleId) => {
|
|
4
|
+
return joinAllUri(caseUri(caseId), 'rules', ruleId, 'backfill', 'count');
|
|
5
|
+
};
|
|
6
|
+
export const post = (caseId, ruleId, since) => {
|
|
7
|
+
return hpost(uri(caseId, ruleId), { since });
|
|
8
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { hpost, joinAllUri } from '@cccsaurora/howler-ui/api';
|
|
2
|
+
import { uri as caseUri } from '@cccsaurora/howler-ui/api/v2/case';
|
|
3
|
+
import * as count from './count';
|
|
4
|
+
export const uri = (caseId, ruleId) => {
|
|
5
|
+
return joinAllUri(caseUri(caseId), 'rules', ruleId, 'backfill');
|
|
6
|
+
};
|
|
7
|
+
export const post = (caseId, ruleId, since) => {
|
|
8
|
+
return hpost(uri(caseId, ruleId), { since });
|
|
9
|
+
};
|
|
10
|
+
export { count };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import * as backfill from '@cccsaurora/howler-ui/api/v2/case/rules/backfill';
|
|
1
2
|
import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
|
|
2
3
|
import type { Rule } from '@cccsaurora/howler-ui/models/entities/generated/Rule';
|
|
3
4
|
export declare const uri: (caseId: string, ruleId?: string) => string;
|
|
4
5
|
export declare const post: (caseId: string, ruleData: Partial<Rule>) => Promise<Case | undefined>;
|
|
5
6
|
export declare const del: (caseId: string, ruleId: string) => Promise<Case | undefined>;
|
|
6
7
|
export declare const put: (caseId: string, ruleId: string, data: Partial<Rule>) => Promise<Case | undefined>;
|
|
8
|
+
export { backfill };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// eslint-disable-next-line import/no-cycle
|
|
2
2
|
import { hdelete, hpost, hput, joinAllUri } from '@cccsaurora/howler-ui/api';
|
|
3
3
|
import { uri as parentUri } from '@cccsaurora/howler-ui/api/v2/case';
|
|
4
|
+
import * as backfill from '@cccsaurora/howler-ui/api/v2/case/rules/backfill';
|
|
4
5
|
export const uri = (caseId, ruleId) => {
|
|
5
6
|
if (ruleId) {
|
|
6
7
|
return joinAllUri(parentUri(caseId), 'rules', ruleId);
|
|
@@ -16,3 +17,4 @@ export const del = (caseId, ruleId) => {
|
|
|
16
17
|
export const put = (caseId, ruleId, data) => {
|
|
17
18
|
return hput(uri(caseId, ruleId), data);
|
|
18
19
|
};
|
|
20
|
+
export { backfill };
|
|
@@ -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',
|
|
@@ -480,6 +480,12 @@
|
|
|
480
480
|
"page.cases.observables.type.user": "User",
|
|
481
481
|
"page.cases.rules": "Rules",
|
|
482
482
|
"page.cases.rules.author": "Author",
|
|
483
|
+
"page.cases.rules.backfill": "Backfill rule",
|
|
484
|
+
"page.cases.rules.backfill.confirm": "This will submit {{count}} matching alert(s) to correlation. Continue?",
|
|
485
|
+
"page.cases.rules.backfill.description": "Find existing alerts matching this rule and submit them to correlation.",
|
|
486
|
+
"page.cases.rules.backfill.preview": "Preview matches",
|
|
487
|
+
"page.cases.rules.backfill.since": "Alerts since",
|
|
488
|
+
"page.cases.rules.backfill.submit": "Submit to correlation",
|
|
483
489
|
"page.cases.rules.create": "Create Rule",
|
|
484
490
|
"page.cases.rules.delete.confirm": "Delete this rule?",
|
|
485
491
|
"page.cases.rules.destination": "Alert Destination Path",
|
|
@@ -480,6 +480,12 @@
|
|
|
480
480
|
"page.cases.observables.type.user": "Utilisateur",
|
|
481
481
|
"page.cases.rules": "Règles",
|
|
482
482
|
"page.cases.rules.author": "Auteur",
|
|
483
|
+
"page.cases.rules.backfill": "Rattraper les alertes",
|
|
484
|
+
"page.cases.rules.backfill.confirm": "{{count}} alerte(s) correspondante(s) seront soumises à la corrélation. Continuer ?",
|
|
485
|
+
"page.cases.rules.backfill.description": "Rechercher les alertes existantes correspondant à cette règle et les soumettre à la corrélation.",
|
|
486
|
+
"page.cases.rules.backfill.preview": "Aperçu des correspondances",
|
|
487
|
+
"page.cases.rules.backfill.since": "Alertes depuis",
|
|
488
|
+
"page.cases.rules.backfill.submit": "Soumettre à la corrélation",
|
|
483
489
|
"page.cases.rules.create": "Créer une règle",
|
|
484
490
|
"page.cases.rules.delete.confirm": "Supprimer cette règle ?",
|
|
485
491
|
"page.cases.rules.destination": "Chemin de destination des alertes",
|
package/package.json
CHANGED
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"url": "https://github.com/CybercentreCanada/howler"
|
|
98
98
|
},
|
|
99
99
|
"type": "module",
|
|
100
|
-
"version": "3.1.0-dev.
|
|
100
|
+
"version": "3.1.0-dev.1708",
|
|
101
101
|
"exports": {
|
|
102
102
|
"./i18n": "./i18n.js",
|
|
103
103
|
"./index.css": "./index.css",
|
|
@@ -243,6 +243,10 @@
|
|
|
243
243
|
"./api/v2/case": "./api/v2/case/index.js",
|
|
244
244
|
"./api/v2/search/*": "./api/v2/search/*.js",
|
|
245
245
|
"./api/v2/search": "./api/v2/search/index.js",
|
|
246
|
+
"./api/v2/case/rules/*": "./api/v2/case/rules/*.js",
|
|
247
|
+
"./api/v2/case/rules": "./api/v2/case/rules/index.js",
|
|
248
|
+
"./api/v2/case/rules/backfill/*": "./api/v2/case/rules/backfill/*.js",
|
|
249
|
+
"./api/v2/case/rules/backfill": "./api/v2/case/rules/backfill/index.js",
|
|
246
250
|
"./api/search/histogram/*": "./api/search/histogram/*.js",
|
|
247
251
|
"./api/search/histogram": "./api/search/histogram/index.js",
|
|
248
252
|
"./api/search/count/*": "./api/search/count/*.js",
|