@cccsaurora/howler-ui 2.19.0-cases.1122 → 2.19.0-cases.1129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/elements/display/QueryResultText.d.ts +1 -0
- package/components/elements/display/QueryResultText.js +2 -2
- package/components/routes/cases/detail/CaseRules.js +5 -7
- package/components/routes/cases/detail/CaseRules.test.js +18 -1
- package/components/routes/cases/detail/CreateRuleDialog.js +37 -55
- package/components/routes/cases/detail/CreateRuleDialog.test.js +47 -22
- package/locales/en/translation.json +18 -13
- package/locales/fr/translation.json +18 -13
- package/models/entities/generated/Rule.d.ts +3 -1
- package/package.json +1 -1
|
@@ -2,13 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Typography } from '@mui/material';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { Link } from 'react-router-dom';
|
|
5
|
-
const QueryResultText = ({ count, query }) => {
|
|
5
|
+
const QueryResultText = ({ count, query, mb = 0.5 }) => {
|
|
6
6
|
const { t } = useTranslation();
|
|
7
7
|
return (_jsxs(Typography, { sx: theme => ({
|
|
8
8
|
color: theme.palette.text.secondary,
|
|
9
9
|
fontSize: '0.9em',
|
|
10
10
|
fontStyle: 'italic',
|
|
11
|
-
mb
|
|
11
|
+
mb,
|
|
12
12
|
'& a': { color: theme.palette.text.secondary }
|
|
13
13
|
}), variant: "body2", children: [t('search.total', { count }), " ", _jsx(Link, { to: `/hits?query=${encodeURIComponent(query)}`, children: t('search.open') })] }));
|
|
14
14
|
};
|
|
@@ -5,7 +5,6 @@ import api from '@cccsaurora/howler-ui/api';
|
|
|
5
5
|
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
6
6
|
import ConfirmDeleteModal from '@cccsaurora/howler-ui/components/elements/display/modals/ConfirmDeleteModal';
|
|
7
7
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
8
|
-
import dayjs from 'dayjs';
|
|
9
8
|
import { useCallback, useContext, useState } from 'react';
|
|
10
9
|
import { useTranslation } from 'react-i18next';
|
|
11
10
|
import { useOutletContext } from 'react-router-dom';
|
|
@@ -47,11 +46,10 @@ const CaseRules = ({ case: providedCase, caseId }) => {
|
|
|
47
46
|
return null;
|
|
48
47
|
}
|
|
49
48
|
const rules = _case.rules ?? [];
|
|
50
|
-
return (_jsxs(Stack, { spacing: 2, sx: { p: 2, height: '100%', overflow: 'auto' }, children: [_jsx(Button, { id: "create-rule-button", variant: "outlined", startIcon: _jsx(Add, {}), onClick: () => setDialogOpen(true), sx: { alignSelf: 'end' }, children: t('page.cases.rules.create') }), _jsx(Card, { children: rules.length === 0 ? (_jsx(Typography, { m: 2, color: "textSecondary", textAlign: "center", children: t('page.cases.rules.empty') })) : (_jsx(TableContainer, { children: _jsxs(Table, { id: "rules-table", size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('page.cases.rules.destination') }), _jsx(TableCell, { children: t('page.cases.rules.query') }), _jsx(TableCell, { children: t('hit.search.index') }), _jsx(TableCell, { children: t('page.cases.rules.timeframe') }), _jsx(TableCell, { children: t('page.cases.rules.author') }), _jsx(TableCell, { align: "center", children: t('enabled') }), _jsx(TableCell, { align: "right" })] }) }), _jsx(TableBody, { children: rules.map(rule => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}) })] }) })) }), _jsx(CreateRuleDialog, { open: dialogOpen, onClose: () => setDialogOpen(false), onSubmit: handleCreateRule })] }));
|
|
49
|
+
return (_jsxs(Stack, { spacing: 2, sx: { p: 2, height: '100%', overflow: 'auto' }, children: [_jsx(Button, { id: "create-rule-button", variant: "outlined", startIcon: _jsx(Add, {}), onClick: () => setDialogOpen(true), sx: { alignSelf: 'end' }, children: t('page.cases.rules.create') }), _jsx(Card, { children: rules.length === 0 ? (_jsx(Typography, { m: 2, color: "textSecondary", textAlign: "center", children: t('page.cases.rules.empty') })) : (_jsx(TableContainer, { children: _jsxs(Table, { id: "rules-table", size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('page.cases.rules.destination') }), _jsx(TableCell, { children: t('page.cases.rules.query') }), _jsx(TableCell, { children: t('hit.search.index') }), _jsx(TableCell, { children: t('page.cases.rules.timeframe') }), _jsx(TableCell, { children: t('page.cases.rules.author') }), _jsx(TableCell, { align: "center", children: t('enabled') }), _jsx(TableCell, { align: "right" })] }) }), _jsx(TableBody, { children: rules.map(rule => (_jsxs(TableRow, { id: `rule-row-${rule.rule_id}`, children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "body2", sx: { fontFamily: 'monospace' }, children: rule.destination }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "body2", sx: {
|
|
50
|
+
fontFamily: 'monospace'
|
|
51
|
+
}, 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
|
|
52
|
+
? t('page.cases.rules.timeframe.after_resolved', { days: rule.timeframe })
|
|
53
|
+
: 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" }) }), _jsx(TableCell, { align: "right", children: _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 })] }));
|
|
56
54
|
};
|
|
57
55
|
export default CaseRules;
|
|
@@ -68,7 +68,8 @@ const makeRule = (overrides) => ({
|
|
|
68
68
|
destination: 'alerts/{{howler.analytic}}',
|
|
69
69
|
author: 'analyst1',
|
|
70
70
|
enabled: true,
|
|
71
|
-
timeframe:
|
|
71
|
+
timeframe: 14,
|
|
72
|
+
expire_after_resolved: false,
|
|
72
73
|
indexes: ['hit'],
|
|
73
74
|
...overrides
|
|
74
75
|
});
|
|
@@ -104,6 +105,22 @@ describe('CaseRules', () => {
|
|
|
104
105
|
render(_jsx(CaseRules, {}));
|
|
105
106
|
expect(screen.getByText('page.cases.rules.no_expiry')).toBeInTheDocument();
|
|
106
107
|
});
|
|
108
|
+
it('shows after-resolved chip when expire_after_resolved is true', () => {
|
|
109
|
+
mockCase.current = createMockCase({
|
|
110
|
+
case_id: 'case-001',
|
|
111
|
+
rules: [makeRule({ timeframe: 7, expire_after_resolved: true })]
|
|
112
|
+
});
|
|
113
|
+
render(_jsx(CaseRules, {}));
|
|
114
|
+
expect(screen.getByText('page.cases.rules.timeframe.after_resolved')).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
it('shows days chip when expire_after_resolved is false', () => {
|
|
117
|
+
mockCase.current = createMockCase({
|
|
118
|
+
case_id: 'case-001',
|
|
119
|
+
rules: [makeRule({ timeframe: 14, expire_after_resolved: false })]
|
|
120
|
+
});
|
|
121
|
+
render(_jsx(CaseRules, {}));
|
|
122
|
+
expect(screen.getByText('page.cases.rules.timeframe.days')).toBeInTheDocument();
|
|
123
|
+
});
|
|
107
124
|
it('renders enabled toggle for each rule', () => {
|
|
108
125
|
mockCase.current = createMockCase({
|
|
109
126
|
case_id: 'case-001',
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMonaco } from '@monaco-editor/react';
|
|
3
|
-
import { Search } from '@mui/icons-material';
|
|
4
|
-
import { Box, Button, Card,
|
|
3
|
+
import { CheckCircleOutline, FilterList, RadioButtonUnchecked, Search } from '@mui/icons-material';
|
|
4
|
+
import { Autocomplete, Box, Button, Card, Chip, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Stack, TextField, Tooltip, Typography } from '@mui/material';
|
|
5
5
|
import api from '@cccsaurora/howler-ui/api';
|
|
6
|
+
import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
|
|
6
7
|
import QueryResultText from '@cccsaurora/howler-ui/components/elements/display/QueryResultText';
|
|
7
8
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
8
9
|
import QueryEditor from '@cccsaurora/howler-ui/components/routes/advanced/QueryEditor';
|
|
9
|
-
import dayjs, {} from 'dayjs';
|
|
10
10
|
import { useCallback, useEffect, useState } from 'react';
|
|
11
11
|
import { useTranslation } from 'react-i18next';
|
|
12
|
-
import { LocalizationProvider } from '@mui/x-date-pickers';
|
|
13
|
-
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
14
|
-
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
|
|
15
12
|
const DEFAULT_TIMEFRAME_DAYS = 14;
|
|
16
13
|
const PROMPT_CONTEXT = 'isRecordQuery && !suggestWidgetVisible && !renameInputVisible && !inSnippetMode && !quickFixWidgetVisible';
|
|
17
14
|
const OPTIONS = {
|
|
@@ -26,6 +23,15 @@ const OPTIONS = {
|
|
|
26
23
|
renderLineHighlight: 'none',
|
|
27
24
|
overviewRulerLanes: 0
|
|
28
25
|
};
|
|
26
|
+
const INDEX_OPTIONS = ['hit', 'event'];
|
|
27
|
+
const Subtitle = ({ i18nKey }) => {
|
|
28
|
+
const { t } = useTranslation();
|
|
29
|
+
return (_jsx(Typography, { sx: theme => ({
|
|
30
|
+
color: theme.palette.text.secondary,
|
|
31
|
+
fontSize: '0.9em',
|
|
32
|
+
fontStyle: 'italic'
|
|
33
|
+
}), variant: "body2", children: t(i18nKey) }));
|
|
34
|
+
};
|
|
29
35
|
const CreateRuleDialog = ({ open, onClose, onSubmit }) => {
|
|
30
36
|
const { t } = useTranslation();
|
|
31
37
|
const { dispatchApi } = useMyApi();
|
|
@@ -33,8 +39,9 @@ const CreateRuleDialog = ({ open, onClose, onSubmit }) => {
|
|
|
33
39
|
const [query, setQuery] = useState('');
|
|
34
40
|
const [destination, setDestination] = useState('');
|
|
35
41
|
const [indexes, setIndexes] = useState(['hit']);
|
|
36
|
-
const [
|
|
37
|
-
const [
|
|
42
|
+
const [timeframeDays, setTimeframeDays] = useState(DEFAULT_TIMEFRAME_DAYS);
|
|
43
|
+
const [hasExpiry, setHasExpiry] = useState(true);
|
|
44
|
+
const [expireAfterResolved, setExpireAfterResolved] = useState(false);
|
|
38
45
|
const [loading, setLoading] = useState(false);
|
|
39
46
|
const [searching, setSearching] = useState(false);
|
|
40
47
|
const [response, setResponse] = useState(null);
|
|
@@ -42,8 +49,9 @@ const CreateRuleDialog = ({ open, onClose, onSubmit }) => {
|
|
|
42
49
|
setQuery('');
|
|
43
50
|
setDestination('');
|
|
44
51
|
setIndexes(['hit']);
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
setTimeframeDays(DEFAULT_TIMEFRAME_DAYS);
|
|
53
|
+
setHasExpiry(true);
|
|
54
|
+
setExpireAfterResolved(false);
|
|
47
55
|
setResponse(null);
|
|
48
56
|
}, []);
|
|
49
57
|
const handleSearch = useCallback(async () => {
|
|
@@ -73,7 +81,8 @@ const CreateRuleDialog = ({ open, onClose, onSubmit }) => {
|
|
|
73
81
|
await onSubmit({
|
|
74
82
|
query: query.trim(),
|
|
75
83
|
destination: destination.trim(),
|
|
76
|
-
timeframe:
|
|
84
|
+
timeframe: hasExpiry ? timeframeDays : null,
|
|
85
|
+
expire_after_resolved: hasExpiry ? expireAfterResolved : false,
|
|
77
86
|
indexes
|
|
78
87
|
});
|
|
79
88
|
onClose();
|
|
@@ -81,7 +90,7 @@ const CreateRuleDialog = ({ open, onClose, onSubmit }) => {
|
|
|
81
90
|
finally {
|
|
82
91
|
setLoading(false);
|
|
83
92
|
}
|
|
84
|
-
}, [query, destination,
|
|
93
|
+
}, [query, destination, hasExpiry, timeframeDays, expireAfterResolved, indexes, onSubmit, onClose]);
|
|
85
94
|
const onMount = useCallback((ed) => {
|
|
86
95
|
ed.createContextKey('isRecordQuery', true);
|
|
87
96
|
}, []);
|
|
@@ -116,48 +125,21 @@ const CreateRuleDialog = ({ open, onClose, onSubmit }) => {
|
|
|
116
125
|
}, [monaco, handleSearch]);
|
|
117
126
|
return (_jsxs(Dialog, { open: open, onClose: onClose, maxWidth: "md", fullWidth: true, id: "create-rule-dialog", TransitionProps: { onEnter: handleOpen }, PaperProps: {
|
|
118
127
|
elevation: 0
|
|
119
|
-
}, children: [_jsx(DialogTitle, { children: t('page.cases.rules.create') }), _jsx(DialogContent, { children: _jsxs(Stack, { spacing:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
fontStyle: 'italic',
|
|
136
|
-
mt: 0.5
|
|
137
|
-
}), variant: "body2", children: t('hit.search.prompt') }))] }), _jsx(TextField, { id: "rule-destination-input", label: t('page.cases.rules.destination'), value: destination, onChange: e => setDestination(e.target.value), fullWidth: true, placeholder: "alerts/{{howler.analytic}}", helperText: t('page.cases.rules.destination.help'), size: "small" }), _jsxs(Box, { children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: t('page.cases.rules.indexes') }), _jsxs(FormGroup, { row: true, children: [_jsx(FormControlLabel, { control: _jsx(Checkbox, { id: "rule-index-hit", checked: indexes.includes('hit'), onChange: (_e, checked) => {
|
|
138
|
-
setIndexes(prev => {
|
|
139
|
-
if (checked) {
|
|
140
|
-
return prev.includes('hit') ? prev : [...prev, 'hit'];
|
|
141
|
-
}
|
|
142
|
-
return prev.filter(i => i !== 'hit');
|
|
143
|
-
});
|
|
144
|
-
} }), label: t('hit.search.index.hit') }), _jsx(FormControlLabel, { control: _jsx(Checkbox, { id: "rule-index-event", checked: indexes.includes('event'), onChange: (_e, checked) => {
|
|
145
|
-
setIndexes(prev => {
|
|
146
|
-
if (checked) {
|
|
147
|
-
return prev.includes('event') ? prev : [...prev, 'event'];
|
|
148
|
-
}
|
|
149
|
-
return prev.filter(i => i !== 'event');
|
|
150
|
-
});
|
|
151
|
-
} }), label: t('hit.search.index.event') })] })] }), _jsxs(Stack, { direction: "row", spacing: 2, alignItems: "center", children: [_jsx(LocalizationProvider, { dateAdapter: AdapterDayjs, children: _jsx(DateTimePicker, { slotProps: {
|
|
152
|
-
textField: {
|
|
153
|
-
id: 'rule-timeframe-input',
|
|
154
|
-
fullWidth: true,
|
|
155
|
-
size: 'small'
|
|
156
|
-
}
|
|
157
|
-
}, label: t('page.cases.rules.timeframe'), value: timeframe, onChange: nv => {
|
|
158
|
-
if (nv) {
|
|
159
|
-
setTimeframe(nv);
|
|
160
|
-
}
|
|
161
|
-
}, disabled: noExpiry, ampm: false, disablePast: true, sx: { flex: 1 } }) }), _jsx(FormControlLabel, { control: _jsx(Checkbox, { id: "rule-no-expiry-checkbox", checked: noExpiry, onChange: (_e, checked) => setNoExpiry(checked) }), label: t('page.cases.rules.no_expiry') })] })] }) }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: onClose, children: t('cancel') }), _jsx(Button, { id: "rule-submit-button", variant: "contained", onClick: handleSubmit, disabled: !response || loading || !query.trim() || !destination.trim() || indexes.length === 0, children: t('page.cases.rules.create') })] })] }));
|
|
128
|
+
}, children: [_jsx(DialogTitle, { children: t('page.cases.rules.create') }), _jsx(DialogContent, { children: _jsxs(Stack, { spacing: 3, sx: { mt: 1 }, children: [_jsxs(Box, { children: [_jsxs(Stack, { direction: "row", justifyContent: "space-between", children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: t('page.cases.rules.query') }), _jsx(ChipPopper, { icon: _jsx(FilterList, { fontSize: "small" }), label: indexes.map(opt => t(`hit.search.index.${opt}`)).join(', '), minWidth: "225px", slotProps: { chip: { size: 'small' } }, children: _jsx(Autocomplete, { size: "small", multiple: true, options: INDEX_OPTIONS, value: indexes, onChange: (_ev, values) => values.length > 0 && setIndexes(values), getOptionLabel: opt => t(`hit.search.index.${opt}`), renderInput: params => _jsx(TextField, { ...params }) }) })] }), _jsx(Stack, { direction: "row", alignItems: "center", spacing: 1, children: _jsx(Box, { sx: { border: 1, borderColor: 'divider', borderRadius: 1, flex: 1 }, children: _jsxs(Card, { variant: "outlined", sx: theme => ({
|
|
129
|
+
width: '100%',
|
|
130
|
+
height: theme.spacing(5),
|
|
131
|
+
py: 1,
|
|
132
|
+
pl: 1,
|
|
133
|
+
pr: 0.5,
|
|
134
|
+
position: 'relative',
|
|
135
|
+
overflow: 'visible',
|
|
136
|
+
display: 'flex',
|
|
137
|
+
alignItems: 'center',
|
|
138
|
+
justifyContent: 'space-between',
|
|
139
|
+
'& .monaco-editor': {
|
|
140
|
+
position: 'absolute !important'
|
|
141
|
+
},
|
|
142
|
+
transition: theme.transitions.create('border-color')
|
|
143
|
+
}), onKeyDown: e => e.stopPropagation(), children: [_jsx(QueryEditor, { query: query, setQuery: handleQueryChange, language: "lucene", id: "rule-query-editor", height: "20px", editorOptions: OPTIONS, onMount: onMount }), _jsx(Tooltip, { title: t('route.search'), children: _jsx("span", { children: _jsx(IconButton, { size: "small", id: "rule-search-button", onClick: handleSearch, disabled: searching || !query.trim(), children: _jsx(Search, {}) }) }) })] }) }) }), response ? (_jsx(QueryResultText, { count: response.total, query: query, mb: 0 })) : (_jsx(Subtitle, { i18nKey: "hit.search.prompt" }))] }), _jsxs(Box, { children: [_jsx(TextField, { id: "rule-destination-input", label: t('page.cases.rules.destination'), value: destination, onChange: e => setDestination(e.target.value), fullWidth: true, placeholder: "alerts/{{howler.analytic}}", size: "small" }), _jsx(Subtitle, { i18nKey: "page.cases.rules.destination.help" })] }), _jsxs(Stack, { children: [_jsx(TextField, { id: "rule-timeframe-input", type: "number", label: t('page.cases.rules.timeframe'), value: timeframeDays, onChange: e => setTimeframeDays(Math.max(1, parseInt(e.target.value, 10) || 1)), size: "small", disabled: !hasExpiry, sx: { flex: 1 }, InputProps: { inputProps: { min: 1 } } }), _jsx(Subtitle, { i18nKey: "page.cases.rules.timeframe.help" })] }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", children: [_jsx(Chip, { id: "rule-no-expiry-checkbox", label: t('page.cases.rules.no_expiry'), icon: !hasExpiry ? _jsx(CheckCircleOutline, {}) : _jsx(RadioButtonUnchecked, {}), variant: "filled", color: !hasExpiry ? 'primary' : 'default', clickable: true, disabled: expireAfterResolved, onClick: () => setHasExpiry(prev => !prev) }), _jsx(Chip, { id: "rule-expire-after-resolved", label: t('page.cases.rules.expire_after_resolved'), icon: expireAfterResolved ? _jsx(CheckCircleOutline, {}) : _jsx(RadioButtonUnchecked, {}), variant: "filled", color: expireAfterResolved ? 'primary' : 'default', clickable: hasExpiry, onClick: () => setExpireAfterResolved(prev => !prev), disabled: !hasExpiry })] })] }) }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: onClose, children: t('cancel') }), _jsx(Button, { id: "rule-submit-button", variant: "contained", onClick: handleSubmit, disabled: !response || loading || !query.trim() || !destination.trim() || indexes.length === 0, children: t('page.cases.rules.create') })] })] }));
|
|
162
144
|
};
|
|
163
145
|
export default CreateRuleDialog;
|
|
@@ -35,8 +35,8 @@ vi.mock('@mui/x-date-pickers/AdapterDayjs', () => ({
|
|
|
35
35
|
AdapterDayjs: class {
|
|
36
36
|
}
|
|
37
37
|
}));
|
|
38
|
-
vi.mock('
|
|
39
|
-
|
|
38
|
+
vi.mock('components/elements/display/ChipPopper', () => ({
|
|
39
|
+
default: ({ children }) => _jsx(_Fragment, { children: children })
|
|
40
40
|
}));
|
|
41
41
|
import api from '@cccsaurora/howler-ui/api';
|
|
42
42
|
import CreateRuleDialog from './CreateRuleDialog';
|
|
@@ -60,8 +60,8 @@ describe('CreateRuleDialog', () => {
|
|
|
60
60
|
expect(screen.getByTestId('rule-destination-input')).toBeInTheDocument();
|
|
61
61
|
expect(screen.getByTestId('rule-timeframe-input')).toBeInTheDocument();
|
|
62
62
|
expect(screen.getByTestId('rule-no-expiry-checkbox')).toBeInTheDocument();
|
|
63
|
-
expect(screen.getByTestId('rule-
|
|
64
|
-
expect(screen.
|
|
63
|
+
expect(screen.getByTestId('rule-expire-after-resolved')).toBeInTheDocument();
|
|
64
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
65
65
|
});
|
|
66
66
|
it('disables submit when query is empty', () => {
|
|
67
67
|
render(_jsx(CreateRuleDialog, { ...defaultProps }));
|
|
@@ -149,7 +149,7 @@ describe('CreateRuleDialog', () => {
|
|
|
149
149
|
expect(screen.getByText('hit.search.prompt')).toBeInTheDocument();
|
|
150
150
|
expect(screen.queryByTestId('query-result-text')).not.toBeInTheDocument();
|
|
151
151
|
});
|
|
152
|
-
it('includes timeframe as
|
|
152
|
+
it('includes timeframe as number when expiry is enabled', async () => {
|
|
153
153
|
const user = userEvent.setup();
|
|
154
154
|
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
|
155
155
|
mockDispatchApi.mockResolvedValueOnce({ items: [], total: 1, offset: 0, rows: 0 });
|
|
@@ -169,7 +169,7 @@ describe('CreateRuleDialog', () => {
|
|
|
169
169
|
});
|
|
170
170
|
await waitFor(() => {
|
|
171
171
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
172
|
-
timeframe:
|
|
172
|
+
timeframe: 14
|
|
173
173
|
}));
|
|
174
174
|
});
|
|
175
175
|
});
|
|
@@ -198,25 +198,18 @@ describe('CreateRuleDialog', () => {
|
|
|
198
198
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
199
199
|
query: 'event.kind:alert',
|
|
200
200
|
destination: 'alerts/incoming',
|
|
201
|
-
timeframe:
|
|
201
|
+
timeframe: null
|
|
202
202
|
}));
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
|
-
it('
|
|
206
|
-
render(_jsx(CreateRuleDialog, { ...defaultProps }));
|
|
207
|
-
const hitCheckbox = screen.getByTestId('rule-index-hit');
|
|
208
|
-
const eventCheckbox = screen.getByTestId('rule-index-event');
|
|
209
|
-
expect(hitCheckbox).toBeChecked();
|
|
210
|
-
expect(eventCheckbox).not.toBeChecked();
|
|
211
|
-
});
|
|
212
|
-
it('includes both indexes when both are selected', async () => {
|
|
205
|
+
it('includes expire_after_resolved when checkbox is checked', async () => {
|
|
213
206
|
const user = userEvent.setup();
|
|
214
207
|
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
|
215
208
|
mockDispatchApi.mockResolvedValueOnce({ items: [], total: 1, offset: 0, rows: 0 });
|
|
216
209
|
api.search.hit.post.mockReturnValue('search-request');
|
|
217
210
|
render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
|
|
218
211
|
await act(async () => {
|
|
219
|
-
await user.click(screen.getByTestId('rule-
|
|
212
|
+
await user.click(screen.getByTestId('rule-expire-after-resolved'));
|
|
220
213
|
});
|
|
221
214
|
await act(async () => {
|
|
222
215
|
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
@@ -232,15 +225,31 @@ describe('CreateRuleDialog', () => {
|
|
|
232
225
|
});
|
|
233
226
|
await waitFor(() => {
|
|
234
227
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
235
|
-
|
|
228
|
+
timeframe: 14,
|
|
229
|
+
expire_after_resolved: true
|
|
236
230
|
}));
|
|
237
231
|
});
|
|
238
232
|
});
|
|
239
|
-
it('
|
|
233
|
+
it('defaults to hit index checked', () => {
|
|
234
|
+
render(_jsx(CreateRuleDialog, { ...defaultProps }));
|
|
235
|
+
// With 'hit' selected by default, its chip appears in the Autocomplete input area
|
|
236
|
+
expect(screen.getAllByText('hit.search.index.hit').length).toBeGreaterThan(0);
|
|
237
|
+
// 'event' is not selected, so its chip should not appear
|
|
238
|
+
expect(screen.queryByText('hit.search.index.event')).not.toBeInTheDocument();
|
|
239
|
+
});
|
|
240
|
+
it('includes both indexes when both are selected', async () => {
|
|
240
241
|
const user = userEvent.setup();
|
|
242
|
+
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
|
241
243
|
mockDispatchApi.mockResolvedValueOnce({ items: [], total: 1, offset: 0, rows: 0 });
|
|
242
244
|
api.search.hit.post.mockReturnValue('search-request');
|
|
243
|
-
render(_jsx(CreateRuleDialog, { ...defaultProps }));
|
|
245
|
+
render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
|
|
246
|
+
// Open the index Autocomplete and select 'event'
|
|
247
|
+
await act(async () => {
|
|
248
|
+
await user.click(screen.getByRole('combobox'));
|
|
249
|
+
});
|
|
250
|
+
await act(async () => {
|
|
251
|
+
await user.click(screen.getByRole('option', { name: 'hit.search.index.event' }));
|
|
252
|
+
});
|
|
244
253
|
await act(async () => {
|
|
245
254
|
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
246
255
|
});
|
|
@@ -250,10 +259,26 @@ describe('CreateRuleDialog', () => {
|
|
|
250
259
|
await act(async () => {
|
|
251
260
|
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
252
261
|
});
|
|
253
|
-
// Uncheck the default hit index
|
|
254
262
|
await act(async () => {
|
|
255
|
-
await user.click(screen.getByTestId('rule-
|
|
263
|
+
await user.click(screen.getByTestId('rule-submit-button'));
|
|
256
264
|
});
|
|
257
|
-
|
|
265
|
+
await waitFor(() => {
|
|
266
|
+
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
267
|
+
indexes: expect.arrayContaining(['hit', 'event'])
|
|
268
|
+
}));
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
it('prevents deselecting the last index', async () => {
|
|
272
|
+
const user = userEvent.setup();
|
|
273
|
+
render(_jsx(CreateRuleDialog, { ...defaultProps }));
|
|
274
|
+
// 'hit' is selected by default; open the dropdown and try to deselect it
|
|
275
|
+
await act(async () => {
|
|
276
|
+
await user.click(screen.getByRole('combobox'));
|
|
277
|
+
});
|
|
278
|
+
await act(async () => {
|
|
279
|
+
await user.click(screen.getByRole('option', { name: 'hit.search.index.hit' }));
|
|
280
|
+
});
|
|
281
|
+
// 'hit' should still be selected because deselecting the last index is prevented
|
|
282
|
+
expect(screen.getAllByText('hit.search.index.hit').length).toBeGreaterThan(0);
|
|
258
283
|
});
|
|
259
284
|
});
|
|
@@ -391,18 +391,6 @@
|
|
|
391
391
|
"page.403.title": "403: Access Forbidden",
|
|
392
392
|
"page.404.description": "The page you are looking for cannot be found...",
|
|
393
393
|
"page.404.title": "404: Not found",
|
|
394
|
-
"page.cases.observables": "Observables",
|
|
395
|
-
"page.cases.observables.empty": "No observables found.",
|
|
396
|
-
"page.cases.observables.filter_by_type": "Filter by type:",
|
|
397
|
-
"page.cases.observables.seen_in": "Seen in",
|
|
398
|
-
"page.cases.observables.type.hash": "Hash",
|
|
399
|
-
"page.cases.observables.type.hosts": "Host",
|
|
400
|
-
"page.cases.observables.type.id": "ID",
|
|
401
|
-
"page.cases.observables.type.ids": "ID",
|
|
402
|
-
"page.cases.observables.type.ip": "IP",
|
|
403
|
-
"page.cases.observables.type.signature": "Signature",
|
|
404
|
-
"page.cases.observables.type.uri": "URI",
|
|
405
|
-
"page.cases.observables.type.user": "User",
|
|
406
394
|
"page.cases.created": "Created",
|
|
407
395
|
"page.cases.dashboard": "Dashboard",
|
|
408
396
|
"page.cases.dashboard.alerts": "Alerts",
|
|
@@ -418,13 +406,25 @@
|
|
|
418
406
|
"page.cases.dashboard.tasks.hide_child": "Hide child case tasks",
|
|
419
407
|
"page.cases.dashboard.tasks.show_child": "Show child case tasks",
|
|
420
408
|
"page.cases.dashboard.threat": "Threats",
|
|
409
|
+
"page.cases.detail.escalation": "Escalation",
|
|
421
410
|
"page.cases.detail.participants": "Participants",
|
|
422
411
|
"page.cases.detail.properties": "Properties",
|
|
423
412
|
"page.cases.detail.status": "Status",
|
|
424
|
-
"page.cases.detail.escalation": "Escalation",
|
|
425
413
|
"page.cases.detail.viewers": "Active Viewers",
|
|
426
414
|
"page.cases.escalation": "Escalation",
|
|
427
415
|
"page.cases.folder.drop.root": "Place here to move to root",
|
|
416
|
+
"page.cases.observables": "Observables",
|
|
417
|
+
"page.cases.observables.empty": "No observables found.",
|
|
418
|
+
"page.cases.observables.filter_by_type": "Filter by type:",
|
|
419
|
+
"page.cases.observables.seen_in": "Seen in",
|
|
420
|
+
"page.cases.observables.type.hash": "Hash",
|
|
421
|
+
"page.cases.observables.type.hosts": "Host",
|
|
422
|
+
"page.cases.observables.type.id": "ID",
|
|
423
|
+
"page.cases.observables.type.ids": "ID",
|
|
424
|
+
"page.cases.observables.type.ip": "IP",
|
|
425
|
+
"page.cases.observables.type.signature": "Signature",
|
|
426
|
+
"page.cases.observables.type.uri": "URI",
|
|
427
|
+
"page.cases.observables.type.user": "User",
|
|
428
428
|
"page.cases.rules": "Rules",
|
|
429
429
|
"page.cases.rules.author": "Author",
|
|
430
430
|
"page.cases.rules.create": "Create Rule",
|
|
@@ -432,9 +432,14 @@
|
|
|
432
432
|
"page.cases.rules.destination": "Alert Destination Path",
|
|
433
433
|
"page.cases.rules.destination.help": "Handlebars template for the case item path (e.g. alerts/{{howler.analytic}})",
|
|
434
434
|
"page.cases.rules.empty": "No correlation rules configured.",
|
|
435
|
+
"page.cases.rules.expire_after_resolved": "Start expiry after case is resolved",
|
|
436
|
+
"page.cases.rules.indexes": "Indexes",
|
|
435
437
|
"page.cases.rules.no_expiry": "No expiry",
|
|
436
438
|
"page.cases.rules.query": "Match Query",
|
|
437
439
|
"page.cases.rules.timeframe": "Rule Expiry",
|
|
440
|
+
"page.cases.rules.timeframe.after_resolved": "{{days}} day(s) after resolved",
|
|
441
|
+
"page.cases.rules.timeframe.days": "{{days}} day(s)",
|
|
442
|
+
"page.cases.rules.timeframe.help": "Number of days before the rule expires",
|
|
438
443
|
"page.cases.search": "Search",
|
|
439
444
|
"page.cases.sidebar.folder.remove": "Remove folder",
|
|
440
445
|
"page.cases.sidebar.item.open": "Open item",
|
|
@@ -391,18 +391,6 @@
|
|
|
391
391
|
"page.403.title": "403: Accès interdit",
|
|
392
392
|
"page.404.description": "La page que vous recherchez est introuvable ...",
|
|
393
393
|
"page.404.title": "404: Introuvable",
|
|
394
|
-
"page.cases.observables": "Observables",
|
|
395
|
-
"page.cases.observables.empty": "Aucun observable trouvé.",
|
|
396
|
-
"page.cases.observables.filter_by_type": "Filtrer par type :",
|
|
397
|
-
"page.cases.observables.seen_in": "Vu dans",
|
|
398
|
-
"page.cases.observables.type.hash": "Hachage",
|
|
399
|
-
"page.cases.observables.type.hosts": "Hôte",
|
|
400
|
-
"page.cases.observables.type.id": "Identifiant",
|
|
401
|
-
"page.cases.observables.type.ids": "Identifiant",
|
|
402
|
-
"page.cases.observables.type.ip": "IP",
|
|
403
|
-
"page.cases.observables.type.signature": "Signature",
|
|
404
|
-
"page.cases.observables.type.uri": "URI",
|
|
405
|
-
"page.cases.observables.type.user": "Utilisateur",
|
|
406
394
|
"page.cases.created": "Créé",
|
|
407
395
|
"page.cases.dashboard": "Tableau de bord",
|
|
408
396
|
"page.cases.dashboard.alerts": "Alertes",
|
|
@@ -418,13 +406,25 @@
|
|
|
418
406
|
"page.cases.dashboard.tasks.hide_child": "Masquer les tâches des cas enfants",
|
|
419
407
|
"page.cases.dashboard.tasks.show_child": "Afficher les tâches des cas enfants",
|
|
420
408
|
"page.cases.dashboard.threat": "Menaces",
|
|
409
|
+
"page.cases.detail.escalation": "Escalade",
|
|
421
410
|
"page.cases.detail.participants": "Participants",
|
|
422
411
|
"page.cases.detail.properties": "Propriétés",
|
|
423
412
|
"page.cases.detail.status": "Statut",
|
|
424
|
-
"page.cases.detail.escalation": "Escalade",
|
|
425
413
|
"page.cases.detail.viewers": "Spectateurs actifs",
|
|
426
414
|
"page.cases.escalation": "Escalade",
|
|
427
415
|
"page.cases.folder.drop.root": "Déposer ici pour déplacer à la racine",
|
|
416
|
+
"page.cases.observables": "Observables",
|
|
417
|
+
"page.cases.observables.empty": "Aucun observable trouvé.",
|
|
418
|
+
"page.cases.observables.filter_by_type": "Filtrer par type :",
|
|
419
|
+
"page.cases.observables.seen_in": "Vu dans",
|
|
420
|
+
"page.cases.observables.type.hash": "Hachage",
|
|
421
|
+
"page.cases.observables.type.hosts": "Hôte",
|
|
422
|
+
"page.cases.observables.type.id": "Identifiant",
|
|
423
|
+
"page.cases.observables.type.ids": "Identifiant",
|
|
424
|
+
"page.cases.observables.type.ip": "IP",
|
|
425
|
+
"page.cases.observables.type.signature": "Signature",
|
|
426
|
+
"page.cases.observables.type.uri": "URI",
|
|
427
|
+
"page.cases.observables.type.user": "Utilisateur",
|
|
428
428
|
"page.cases.rules": "Règles",
|
|
429
429
|
"page.cases.rules.author": "Auteur",
|
|
430
430
|
"page.cases.rules.create": "Créer une règle",
|
|
@@ -432,9 +432,14 @@
|
|
|
432
432
|
"page.cases.rules.destination": "Chemin de destination des alertes",
|
|
433
433
|
"page.cases.rules.destination.help": "Modèle Handlebars pour le chemin de l'élément de cas (ex. alerts/{{howler.analytic}})",
|
|
434
434
|
"page.cases.rules.empty": "Aucune règle de corrélation configurée.",
|
|
435
|
+
"page.cases.rules.expire_after_resolved": "Démarrer l'expiration après la résolution du cas",
|
|
436
|
+
"page.cases.rules.indexes": "Index",
|
|
435
437
|
"page.cases.rules.no_expiry": "Sans expiration",
|
|
436
438
|
"page.cases.rules.query": "Requête de correspondance",
|
|
437
439
|
"page.cases.rules.timeframe": "Expiration de la règle",
|
|
440
|
+
"page.cases.rules.timeframe.after_resolved": "{{days}} jour(s) après résolution",
|
|
441
|
+
"page.cases.rules.timeframe.days": "{{days}} jour(s)",
|
|
442
|
+
"page.cases.rules.timeframe.help": "Nombre de jours avant l'expiration de la règle",
|
|
438
443
|
"page.cases.search": "Rechercher",
|
|
439
444
|
"page.cases.sidebar.folder.remove": "Supprimer le dossier",
|
|
440
445
|
"page.cases.sidebar.item.open": "Ouvrir l'élément",
|