@cccsaurora/howler-ui 2.19.0-cases.1111 → 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/api/v2/fuzzy.d.ts +17 -0
- package/api/v2/fuzzy.js +11 -0
- package/api/v2/index.d.ts +2 -1
- package/api/v2/index.js +2 -1
- package/components/app/App.js +6 -1
- package/components/app/providers/ParameterProvider.d.ts +16 -1
- package/components/app/providers/ParameterProvider.js +23 -22
- package/components/app/providers/ParameterProvider.test.js +12 -0
- package/components/elements/addons/search/SearchTotal.js +1 -1
- package/components/elements/addons/search/SearchTotal.test.js +2 -2
- package/components/elements/display/QueryResultText.d.ts +1 -0
- package/components/elements/display/QueryResultText.js +2 -2
- package/components/elements/search/FuzzySearchBar.d.ts +7 -0
- package/components/elements/search/FuzzySearchBar.js +33 -0
- package/components/elements/search/FuzzySearchBar.test.d.ts +1 -0
- package/components/elements/search/FuzzySearchBar.test.js +87 -0
- package/components/routes/cases/detail/CaseRules.js +5 -7
- package/components/routes/cases/detail/CaseRules.test.js +18 -1
- package/components/routes/cases/detail/CaseSearch.d.ts +3 -0
- package/components/routes/cases/detail/CaseSearch.js +78 -0
- package/components/routes/cases/detail/CaseSidebar.js +2 -2
- package/components/routes/cases/detail/CreateRuleDialog.js +37 -55
- package/components/routes/cases/detail/CreateRuleDialog.test.js +47 -22
- package/components/routes/cases/detail/sidebar/CaseFolder.test.js +1 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +3 -2
- package/components/routes/hits/search/shared/IndexPicker.d.ts +7 -1
- package/components/routes/hits/search/shared/IndexPicker.js +4 -3
- package/locales/en/translation.json +33 -19
- package/locales/fr/translation.json +33 -19
- package/models/entities/generated/Case.d.ts +1 -0
- package/models/entities/generated/Rule.d.ts +3 -1
- package/package.json +2 -1
- package/tests/utils.js +1 -0
|
@@ -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
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
3
|
import { act } from 'react';
|
|
4
|
+
import { createMockCase } from '@cccsaurora/howler-ui/tests/utils';
|
|
4
5
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Mocks
|
|
@@ -51,7 +52,7 @@ import CaseFolderContextMenu, { collectAllLeaves, getOpenUrl } from './CaseFolde
|
|
|
51
52
|
// ---------------------------------------------------------------------------
|
|
52
53
|
// Fixtures
|
|
53
54
|
// ---------------------------------------------------------------------------
|
|
54
|
-
const mockCase = { case_id: 'case-1'
|
|
55
|
+
const mockCase = createMockCase({ case_id: 'case-1' });
|
|
55
56
|
const hitLeaf = { type: 'hit', value: 'hit-123', path: 'folder/hit-item' };
|
|
56
57
|
const referenceLeaf = { type: 'reference', value: 'https://example.com', path: 'folder/ref-item' };
|
|
57
58
|
const eventLeaf = { type: 'event', value: 'obs-456', path: 'folder/event-item' };
|
|
@@ -255,7 +256,7 @@ describe('CaseFolderContextMenu', () => {
|
|
|
255
256
|
});
|
|
256
257
|
});
|
|
257
258
|
it('does not call the API when case_id is missing', () => {
|
|
258
|
-
renderMenu({ _case: { title: 'No ID' }, leaf: hitLeaf });
|
|
259
|
+
renderMenu({ _case: createMockCase({ case_id: undefined, title: 'No ID' }), leaf: hitLeaf });
|
|
259
260
|
act(() => {
|
|
260
261
|
fireEvent.click(screen.getByTestId('remove-item'));
|
|
261
262
|
});
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import type { SearchIndex } from '@cccsaurora/howler-ui/api/v2/search';
|
|
2
|
+
declare const _default: import("react").NamedExoticComponent<{
|
|
3
|
+
additionalOptions?: {
|
|
4
|
+
label: string;
|
|
5
|
+
value: SearchIndex;
|
|
6
|
+
}[];
|
|
7
|
+
}>;
|
|
2
8
|
export default _default;
|
|
@@ -10,11 +10,12 @@ const FILTER_OPTIONS = [
|
|
|
10
10
|
{ label: 'hit.search.index.hit', value: 'hit' },
|
|
11
11
|
{ label: 'hit.search.index.event', value: 'event' }
|
|
12
12
|
];
|
|
13
|
-
const IndexPicker = () => {
|
|
13
|
+
const IndexPicker = ({ additionalOptions = [] }) => {
|
|
14
14
|
const { t } = useTranslation();
|
|
15
15
|
const indexes = useContextSelector(ParameterContext, ctx => ctx.indexes);
|
|
16
16
|
const setIndexes = useContextSelector(ParameterContext, ctx => ctx.setIndexes);
|
|
17
|
-
const
|
|
18
|
-
|
|
17
|
+
const allOptions = [...FILTER_OPTIONS, ...additionalOptions];
|
|
18
|
+
const selectedOptions = allOptions.filter(opt => indexes.includes(opt.value));
|
|
19
|
+
return (_jsx(ChipPopper, { icon: _jsx(FilterList, { fontSize: "small" }), label: selectedOptions.map(opt => t(opt.label)).join(', '), minWidth: "225px", slotProps: { chip: { size: 'small' } }, children: _jsx(Autocomplete, { size: "small", multiple: true, options: allOptions, value: selectedOptions, onChange: (_ev, values) => values.length > 0 && setIndexes(values.map(val => val.value)), isOptionEqualToValue: (opt, val) => opt.value === val.value, getOptionLabel: opt => t(opt.label), renderInput: params => _jsx(TextField, { ...params }) }) }));
|
|
19
20
|
};
|
|
20
21
|
export default memo(IndexPicker);
|
|
@@ -116,6 +116,7 @@
|
|
|
116
116
|
"edit": "Edit",
|
|
117
117
|
"enabled": "Enabled",
|
|
118
118
|
"event.module": "Event Module",
|
|
119
|
+
"event.open": "Open Event",
|
|
119
120
|
"event.type": "Event Type",
|
|
120
121
|
"features.warning.description": "This feature is undergoing active development, and is not yet in a finished state. You may encounter bugs or instability.",
|
|
121
122
|
"features.warning.title": "Feature In Active Development",
|
|
@@ -187,8 +188,8 @@
|
|
|
187
188
|
"hit.header.target": "Target",
|
|
188
189
|
"hit.header.threat": "Threat",
|
|
189
190
|
"hit.header.view.case": "View case {{id}}",
|
|
190
|
-
"hit.header.view.hit": "View hit {{id}}",
|
|
191
191
|
"hit.header.view.event": "View event {{id}}",
|
|
192
|
+
"hit.header.view.hit": "View hit {{id}}",
|
|
192
193
|
"hit.header.votes": "Votes: ",
|
|
193
194
|
"hit.howler.related": "{{count}} related records",
|
|
194
195
|
"hit.label": "Labels",
|
|
@@ -231,9 +232,9 @@
|
|
|
231
232
|
"hit.panel.view.layout": "Change View Panel",
|
|
232
233
|
"hit.quicksearch": "Search by assignment, analytic, detection or status",
|
|
233
234
|
"hit.related.tab.case": "Cases",
|
|
235
|
+
"hit.related.tab.event": "Events",
|
|
234
236
|
"hit.related.tab.hit": "Hits",
|
|
235
237
|
"hit.related.tab.links": "Links",
|
|
236
|
-
"hit.related.tab.event": "Events",
|
|
237
238
|
"hit.search.aggregate.button": "Create Summary",
|
|
238
239
|
"hit.search.button": "Perform search",
|
|
239
240
|
"hit.search.custom": "Custom Sort",
|
|
@@ -243,8 +244,9 @@
|
|
|
243
244
|
"hit.search.filter.label": "Lookup Filters",
|
|
244
245
|
"hit.search.filter.values": "Values",
|
|
245
246
|
"hit.search.index": "Indexes",
|
|
246
|
-
"hit.search.index.
|
|
247
|
+
"hit.search.index.case": "Cases",
|
|
247
248
|
"hit.search.index.event": "Events",
|
|
249
|
+
"hit.search.index.hit": "Hits",
|
|
248
250
|
"hit.search.invalid": "Invalid Query",
|
|
249
251
|
"hit.search.keyboard": "Keyboard shortcuts",
|
|
250
252
|
"hit.search.keyboard.no_shortcuts": "No shortcuts",
|
|
@@ -368,7 +370,6 @@
|
|
|
368
370
|
"no.data": "No Data",
|
|
369
371
|
"none": "None",
|
|
370
372
|
"normal": "Normal Priority",
|
|
371
|
-
"event.open": "Open Event",
|
|
372
373
|
"on": "on",
|
|
373
374
|
"open": "Open",
|
|
374
375
|
"operations.add_label": "Add Label",
|
|
@@ -390,18 +391,6 @@
|
|
|
390
391
|
"page.403.title": "403: Access Forbidden",
|
|
391
392
|
"page.404.description": "The page you are looking for cannot be found...",
|
|
392
393
|
"page.404.title": "404: Not found",
|
|
393
|
-
"page.cases.observables": "Observables",
|
|
394
|
-
"page.cases.observables.empty": "No observables found.",
|
|
395
|
-
"page.cases.observables.filter_by_type": "Filter by type:",
|
|
396
|
-
"page.cases.observables.seen_in": "Seen in",
|
|
397
|
-
"page.cases.observables.type.hash": "Hash",
|
|
398
|
-
"page.cases.observables.type.hosts": "Host",
|
|
399
|
-
"page.cases.observables.type.id": "ID",
|
|
400
|
-
"page.cases.observables.type.ids": "ID",
|
|
401
|
-
"page.cases.observables.type.ip": "IP",
|
|
402
|
-
"page.cases.observables.type.signature": "Signature",
|
|
403
|
-
"page.cases.observables.type.uri": "URI",
|
|
404
|
-
"page.cases.observables.type.user": "User",
|
|
405
394
|
"page.cases.created": "Created",
|
|
406
395
|
"page.cases.dashboard": "Dashboard",
|
|
407
396
|
"page.cases.dashboard.alerts": "Alerts",
|
|
@@ -411,19 +400,31 @@
|
|
|
411
400
|
"page.cases.dashboard.target": "Targets",
|
|
412
401
|
"page.cases.dashboard.tasks": "Tasks",
|
|
413
402
|
"page.cases.dashboard.tasks.add": "Add Task",
|
|
403
|
+
"page.cases.dashboard.tasks.child.empty": "No tasks in this child case.",
|
|
414
404
|
"page.cases.dashboard.tasks.child_cases": "Child Cases",
|
|
415
405
|
"page.cases.dashboard.tasks.filter_cases": "Filter child cases",
|
|
416
|
-
"page.cases.dashboard.tasks.show_child": "Show child case tasks",
|
|
417
406
|
"page.cases.dashboard.tasks.hide_child": "Hide child case tasks",
|
|
418
|
-
"page.cases.dashboard.tasks.
|
|
407
|
+
"page.cases.dashboard.tasks.show_child": "Show child case tasks",
|
|
419
408
|
"page.cases.dashboard.threat": "Threats",
|
|
409
|
+
"page.cases.detail.escalation": "Escalation",
|
|
420
410
|
"page.cases.detail.participants": "Participants",
|
|
421
411
|
"page.cases.detail.properties": "Properties",
|
|
422
412
|
"page.cases.detail.status": "Status",
|
|
423
|
-
"page.cases.detail.escalation": "Escalation",
|
|
424
413
|
"page.cases.detail.viewers": "Active Viewers",
|
|
425
414
|
"page.cases.escalation": "Escalation",
|
|
426
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",
|
|
427
428
|
"page.cases.rules": "Rules",
|
|
428
429
|
"page.cases.rules.author": "Author",
|
|
429
430
|
"page.cases.rules.create": "Create Rule",
|
|
@@ -431,9 +432,15 @@
|
|
|
431
432
|
"page.cases.rules.destination": "Alert Destination Path",
|
|
432
433
|
"page.cases.rules.destination.help": "Handlebars template for the case item path (e.g. alerts/{{howler.analytic}})",
|
|
433
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",
|
|
434
437
|
"page.cases.rules.no_expiry": "No expiry",
|
|
435
438
|
"page.cases.rules.query": "Match Query",
|
|
436
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",
|
|
443
|
+
"page.cases.search": "Search",
|
|
437
444
|
"page.cases.sidebar.folder.remove": "Remove folder",
|
|
438
445
|
"page.cases.sidebar.item.open": "Open item",
|
|
439
446
|
"page.cases.sidebar.item.remove": "Remove item",
|
|
@@ -847,6 +854,13 @@
|
|
|
847
854
|
"rule.interval.thirty.minutes": "Every thirty minutes",
|
|
848
855
|
"rule.interval.three.hours": "Every three hours",
|
|
849
856
|
"save": "Save",
|
|
857
|
+
"search.fuzzy.no_results": "No results found.",
|
|
858
|
+
"search.fuzzy.placeholder": "Search by IP, domain, hash, email, or keyword...",
|
|
859
|
+
"search.fuzzy.results.cases": "Cases",
|
|
860
|
+
"search.fuzzy.results.events": "Events",
|
|
861
|
+
"search.fuzzy.results.hits": "Hits",
|
|
862
|
+
"search.fuzzy.title": "Fuzzy Search",
|
|
863
|
+
"search.fuzzy.total_results": "results found",
|
|
850
864
|
"search.layout.settings": "Edit search result layout",
|
|
851
865
|
"search.open": "Open Search",
|
|
852
866
|
"search.result.showing": "Showing {{offset}} to {{length}} of {{total}} results",
|
|
@@ -116,6 +116,7 @@
|
|
|
116
116
|
"edit": "Modifier",
|
|
117
117
|
"enabled": "Activé",
|
|
118
118
|
"event.module": "Module d'événement",
|
|
119
|
+
"event.open": "Ouvrir événement",
|
|
119
120
|
"event.type": "Type d'événement",
|
|
120
121
|
"features.warning.description": "Cette fonctionnalité fait l'objet d'un développement actif et n'est pas encore achevée. Il est possible que vous rencontriez des problèmes ou de l'instabilité.",
|
|
121
122
|
"features.warning.title": "Fonctionnalité en développement actif",
|
|
@@ -187,8 +188,8 @@
|
|
|
187
188
|
"hit.header.target": "Cible",
|
|
188
189
|
"hit.header.threat": "Menace",
|
|
189
190
|
"hit.header.view.case": "Voir le cas {{id}}",
|
|
190
|
-
"hit.header.view.hit": "Voir l'alerte {{id}}",
|
|
191
191
|
"hit.header.view.event": "Voir l'événement {{id}}",
|
|
192
|
+
"hit.header.view.hit": "Voir l'alerte {{id}}",
|
|
192
193
|
"hit.header.votes": "Votes: ",
|
|
193
194
|
"hit.howler.related": "{{count}} enregistrements associés",
|
|
194
195
|
"hit.label": "Étiquettes",
|
|
@@ -231,9 +232,9 @@
|
|
|
231
232
|
"hit.panel.view.layout": "Modifier le panneau d'affichage",
|
|
232
233
|
"hit.quicksearch": "Recherche par affectation, analytique, détection ou état",
|
|
233
234
|
"hit.related.tab.case": "Cas",
|
|
235
|
+
"hit.related.tab.event": "Événements",
|
|
234
236
|
"hit.related.tab.hit": "Alertes",
|
|
235
237
|
"hit.related.tab.links": "Liens",
|
|
236
|
-
"hit.related.tab.event": "Événements",
|
|
237
238
|
"hit.search.aggregate.button": "Créer un sommaire",
|
|
238
239
|
"hit.search.button": "Effectuer une recherche",
|
|
239
240
|
"hit.search.custom": "Triage personnalisé",
|
|
@@ -243,8 +244,9 @@
|
|
|
243
244
|
"hit.search.filter.label": "Filtres de recherche",
|
|
244
245
|
"hit.search.filter.values": "Valeurs",
|
|
245
246
|
"hit.search.index": "Indexes",
|
|
246
|
-
"hit.search.index.
|
|
247
|
+
"hit.search.index.case": "Cas",
|
|
247
248
|
"hit.search.index.event": "Événements",
|
|
249
|
+
"hit.search.index.hit": "Hits",
|
|
248
250
|
"hit.search.invalid": "Requête invalide",
|
|
249
251
|
"hit.search.keyboard": "Raccourcis clavier",
|
|
250
252
|
"hit.search.keyboard.no_shortcuts": "Pas de raccourcis",
|
|
@@ -368,7 +370,6 @@
|
|
|
368
370
|
"no.data": "Aucune donnée",
|
|
369
371
|
"none": "Rien",
|
|
370
372
|
"normal": "Priorité normale",
|
|
371
|
-
"event.open": "Ouvrir événement",
|
|
372
373
|
"on": "sur",
|
|
373
374
|
"open": "Ouvert",
|
|
374
375
|
"operations.add_label": "Ajouter un label",
|
|
@@ -390,18 +391,6 @@
|
|
|
390
391
|
"page.403.title": "403: Accès interdit",
|
|
391
392
|
"page.404.description": "La page que vous recherchez est introuvable ...",
|
|
392
393
|
"page.404.title": "404: Introuvable",
|
|
393
|
-
"page.cases.observables": "Observables",
|
|
394
|
-
"page.cases.observables.empty": "Aucun observable trouvé.",
|
|
395
|
-
"page.cases.observables.filter_by_type": "Filtrer par type :",
|
|
396
|
-
"page.cases.observables.seen_in": "Vu dans",
|
|
397
|
-
"page.cases.observables.type.hash": "Hachage",
|
|
398
|
-
"page.cases.observables.type.hosts": "Hôte",
|
|
399
|
-
"page.cases.observables.type.id": "Identifiant",
|
|
400
|
-
"page.cases.observables.type.ids": "Identifiant",
|
|
401
|
-
"page.cases.observables.type.ip": "IP",
|
|
402
|
-
"page.cases.observables.type.signature": "Signature",
|
|
403
|
-
"page.cases.observables.type.uri": "URI",
|
|
404
|
-
"page.cases.observables.type.user": "Utilisateur",
|
|
405
394
|
"page.cases.created": "Créé",
|
|
406
395
|
"page.cases.dashboard": "Tableau de bord",
|
|
407
396
|
"page.cases.dashboard.alerts": "Alertes",
|
|
@@ -411,19 +400,31 @@
|
|
|
411
400
|
"page.cases.dashboard.target": "Cibles",
|
|
412
401
|
"page.cases.dashboard.tasks": "Tâches",
|
|
413
402
|
"page.cases.dashboard.tasks.add": "Ajouter une tâche",
|
|
403
|
+
"page.cases.dashboard.tasks.child.empty": "Aucune tâche dans ce cas enfant.",
|
|
414
404
|
"page.cases.dashboard.tasks.child_cases": "Cas enfants",
|
|
415
405
|
"page.cases.dashboard.tasks.filter_cases": "Filtrer les cas enfants",
|
|
416
|
-
"page.cases.dashboard.tasks.show_child": "Afficher les tâches des cas enfants",
|
|
417
406
|
"page.cases.dashboard.tasks.hide_child": "Masquer les tâches des cas enfants",
|
|
418
|
-
"page.cases.dashboard.tasks.
|
|
407
|
+
"page.cases.dashboard.tasks.show_child": "Afficher les tâches des cas enfants",
|
|
419
408
|
"page.cases.dashboard.threat": "Menaces",
|
|
409
|
+
"page.cases.detail.escalation": "Escalade",
|
|
420
410
|
"page.cases.detail.participants": "Participants",
|
|
421
411
|
"page.cases.detail.properties": "Propriétés",
|
|
422
412
|
"page.cases.detail.status": "Statut",
|
|
423
|
-
"page.cases.detail.escalation": "Escalade",
|
|
424
413
|
"page.cases.detail.viewers": "Spectateurs actifs",
|
|
425
414
|
"page.cases.escalation": "Escalade",
|
|
426
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",
|
|
427
428
|
"page.cases.rules": "Règles",
|
|
428
429
|
"page.cases.rules.author": "Auteur",
|
|
429
430
|
"page.cases.rules.create": "Créer une règle",
|
|
@@ -431,9 +432,15 @@
|
|
|
431
432
|
"page.cases.rules.destination": "Chemin de destination des alertes",
|
|
432
433
|
"page.cases.rules.destination.help": "Modèle Handlebars pour le chemin de l'élément de cas (ex. alerts/{{howler.analytic}})",
|
|
433
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",
|
|
434
437
|
"page.cases.rules.no_expiry": "Sans expiration",
|
|
435
438
|
"page.cases.rules.query": "Requête de correspondance",
|
|
436
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",
|
|
443
|
+
"page.cases.search": "Rechercher",
|
|
437
444
|
"page.cases.sidebar.folder.remove": "Supprimer le dossier",
|
|
438
445
|
"page.cases.sidebar.item.open": "Ouvrir l'élément",
|
|
439
446
|
"page.cases.sidebar.item.remove": "Supprimer l'élément",
|
|
@@ -847,6 +854,13 @@
|
|
|
847
854
|
"rule.interval.thirty.minutes": "Toutes les trente minutes",
|
|
848
855
|
"rule.interval.three.hours": "Toutes les trois heures",
|
|
849
856
|
"save": "Sauvegarder",
|
|
857
|
+
"search.fuzzy.no_results": "Aucun résultat trouvé.",
|
|
858
|
+
"search.fuzzy.placeholder": "Rechercher par IP, domaine, hash, courriel ou mot-clé...",
|
|
859
|
+
"search.fuzzy.results.cases": "Cas",
|
|
860
|
+
"search.fuzzy.results.events": "Événements",
|
|
861
|
+
"search.fuzzy.results.hits": "Alertes",
|
|
862
|
+
"search.fuzzy.title": "Recherche floue",
|
|
863
|
+
"search.fuzzy.total_results": "résultats trouvés",
|
|
850
864
|
"search.layout.settings": "Modifier la présentation des résultats de recherche",
|
|
851
865
|
"search.open": "Ouvrir la recherche",
|
|
852
866
|
"search.result.showing": "Affichage de {{offset}} à {{length}} sur {{total}} articles",
|