@cccsaurora/howler-ui 2.19.0-cases.1111 → 2.19.0-cases.1122
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/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/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/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 +15 -6
- package/locales/fr/translation.json +15 -6
- package/models/entities/generated/Case.d.ts +1 -0
- package/package.json +2 -1
- package/tests/utils.js +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HowlerSearchResponse } from '@cccsaurora/howler-ui/api/search';
|
|
2
|
+
import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
|
|
3
|
+
import type { Event } from '@cccsaurora/howler-ui/models/entities/generated/Event';
|
|
4
|
+
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
5
|
+
export type FuzzySearchRequest = {
|
|
6
|
+
query: string;
|
|
7
|
+
indexes?: string[];
|
|
8
|
+
filters?: string[];
|
|
9
|
+
offset?: number;
|
|
10
|
+
rows?: number;
|
|
11
|
+
track_total_hits?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type FuzzySearchItem<T = Hit | Event | Case> = T & {
|
|
14
|
+
_score: number;
|
|
15
|
+
};
|
|
16
|
+
export declare const uri: () => string;
|
|
17
|
+
export declare const post: (request: FuzzySearchRequest) => Promise<HowlerSearchResponse<FuzzySearchItem>>;
|
package/api/v2/fuzzy.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { hpost, joinAllUri } from '@cccsaurora/howler-ui/api';
|
|
2
|
+
import { uri as parentUri } from '@cccsaurora/howler-ui/api/v2';
|
|
3
|
+
export const uri = () => {
|
|
4
|
+
return joinAllUri(parentUri(), 'fuzzy');
|
|
5
|
+
};
|
|
6
|
+
export const post = (request) => {
|
|
7
|
+
if (!request.query || !request.query.trim()) {
|
|
8
|
+
throw new Error('Search query is required.');
|
|
9
|
+
}
|
|
10
|
+
return hpost(joinAllUri(uri(), 'search'), request);
|
|
11
|
+
};
|
package/api/v2/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as case_ from '@cccsaurora/howler-ui/api/v2/case';
|
|
2
|
+
import * as fuzzy from '@cccsaurora/howler-ui/api/v2/fuzzy';
|
|
2
3
|
import * as search from '@cccsaurora/howler-ui/api/v2/search';
|
|
3
4
|
export declare const uri: () => string;
|
|
4
|
-
export { case_ as case, search };
|
|
5
|
+
export { case_ as case, fuzzy, search };
|
package/api/v2/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as case_ from '@cccsaurora/howler-ui/api/v2/case';
|
|
2
|
+
import * as fuzzy from '@cccsaurora/howler-ui/api/v2/fuzzy';
|
|
2
3
|
import * as search from '@cccsaurora/howler-ui/api/v2/search';
|
|
3
4
|
export const uri = () => {
|
|
4
5
|
return '/api/v2';
|
|
5
6
|
};
|
|
6
|
-
export { case_ as case, search };
|
|
7
|
+
export { case_ as case, fuzzy, search };
|
package/components/app/App.js
CHANGED
|
@@ -31,6 +31,7 @@ import Cases from '@cccsaurora/howler-ui/components/routes/cases/Cases';
|
|
|
31
31
|
import CaseDashboard from '@cccsaurora/howler-ui/components/routes/cases/detail/CaseDashboard';
|
|
32
32
|
import CaseObservables from '@cccsaurora/howler-ui/components/routes/cases/detail/CaseObservables';
|
|
33
33
|
import CaseRules from '@cccsaurora/howler-ui/components/routes/cases/detail/CaseRules';
|
|
34
|
+
import CaseSearch from '@cccsaurora/howler-ui/components/routes/cases/detail/CaseSearch';
|
|
34
35
|
import CaseTimeline from '@cccsaurora/howler-ui/components/routes/cases/detail/CaseTimeline';
|
|
35
36
|
import ItemPage from '@cccsaurora/howler-ui/components/routes/cases/detail/ItemPage';
|
|
36
37
|
import DossierEditor from '@cccsaurora/howler-ui/components/routes/dossiers/DossierEditor';
|
|
@@ -207,7 +208,7 @@ const createRouter = () => createBrowserRouter([
|
|
|
207
208
|
},
|
|
208
209
|
{
|
|
209
210
|
path: 'cases/:id',
|
|
210
|
-
element: (_jsx(ParameterProvider, { children: _jsx(CaseViewer, {}) })),
|
|
211
|
+
element: (_jsx(ParameterProvider, { defaults: { query: '', indexes: ['hit', 'event', 'case'] }, children: _jsx(CaseViewer, {}) })),
|
|
211
212
|
children: [
|
|
212
213
|
{
|
|
213
214
|
index: true,
|
|
@@ -225,6 +226,10 @@ const createRouter = () => createBrowserRouter([
|
|
|
225
226
|
path: 'rules',
|
|
226
227
|
element: _jsx(CaseRules, {})
|
|
227
228
|
},
|
|
229
|
+
{
|
|
230
|
+
path: 'search',
|
|
231
|
+
element: _jsx(CaseSearch, {})
|
|
232
|
+
},
|
|
228
233
|
{
|
|
229
234
|
path: '*',
|
|
230
235
|
element: _jsx(ItemPage, {})
|
|
@@ -32,10 +32,25 @@ export interface ParameterContextType {
|
|
|
32
32
|
setView: (index: number, view: string) => void;
|
|
33
33
|
resetViews: () => void;
|
|
34
34
|
}
|
|
35
|
+
interface SearchValues {
|
|
36
|
+
selected: string;
|
|
37
|
+
query: string;
|
|
38
|
+
sort: string;
|
|
39
|
+
span: string;
|
|
40
|
+
indexes: SearchIndex[];
|
|
41
|
+
filters: string[];
|
|
42
|
+
views: string[];
|
|
43
|
+
startDate: string;
|
|
44
|
+
endDate: string;
|
|
45
|
+
offset: number;
|
|
46
|
+
trackTotalHits: boolean;
|
|
47
|
+
}
|
|
35
48
|
export declare const ParameterContext: import("use-context-selector").Context<ParameterContextType>;
|
|
36
49
|
/**
|
|
37
50
|
* Context responsible for tracking updates to query operations in hit and view search.
|
|
38
51
|
*/
|
|
39
|
-
declare const ParameterProvider: FC<PropsWithChildren
|
|
52
|
+
declare const ParameterProvider: FC<PropsWithChildren<{
|
|
53
|
+
defaults?: Partial<SearchValues>;
|
|
54
|
+
}>>;
|
|
40
55
|
export declare const useParameterContextSelector: <Selected>(selector: (value: ParameterContextType) => Selected) => Selected;
|
|
41
56
|
export default ParameterProvider;
|
|
@@ -24,7 +24,7 @@ const PARAM_MAPPINGS = [
|
|
|
24
24
|
const ARRAY_PARAMS = [
|
|
25
25
|
{ urlKey: 'filter', stateKey: 'filters' },
|
|
26
26
|
{ urlKey: 'view', stateKey: 'views' },
|
|
27
|
-
{ urlKey: 'index', stateKey: 'indexes'
|
|
27
|
+
{ urlKey: 'index', stateKey: 'indexes' }
|
|
28
28
|
];
|
|
29
29
|
const ARRAY_URL_KEYS = new Set(ARRAY_PARAMS.map(p => p.urlKey));
|
|
30
30
|
const WRITE_THROTTLER = new Throttler(100);
|
|
@@ -85,7 +85,7 @@ const useListHandlers = (key, _setValues) => {
|
|
|
85
85
|
/**
|
|
86
86
|
* Synchronizes SearchValues state with the URL search string, and vice-versa.
|
|
87
87
|
*/
|
|
88
|
-
const useUrlSync = (values, _setValues, params, setParams, pathname, search, routeId) => {
|
|
88
|
+
const useUrlSync = (values, defaults, _setValues, params, setParams, pathname, search, routeId) => {
|
|
89
89
|
const getUrlFromState = useCallback(() => {
|
|
90
90
|
const changes = {};
|
|
91
91
|
// Scalar params: write if changed from URL, remove if back to default
|
|
@@ -94,20 +94,21 @@ const useUrlSync = (values, _setValues, params, setParams, pathname, search, rou
|
|
|
94
94
|
const urlValue = params.get(urlKey);
|
|
95
95
|
if (stateValue === urlValue)
|
|
96
96
|
return;
|
|
97
|
-
if (params.has(urlKey) && stateValue ===
|
|
97
|
+
if (params.has(urlKey) && stateValue === defaults[stateKey]) {
|
|
98
98
|
changes[urlKey] = null; // remove
|
|
99
99
|
}
|
|
100
|
-
else if (stateValue !==
|
|
100
|
+
else if (stateValue !== defaults[stateKey]) {
|
|
101
101
|
changes[urlKey] = stateValue; // write
|
|
102
102
|
}
|
|
103
103
|
});
|
|
104
104
|
// Array params: skip when state equals default and URL is already empty
|
|
105
|
-
ARRAY_PARAMS.forEach(({ urlKey, stateKey
|
|
105
|
+
ARRAY_PARAMS.forEach(({ urlKey, stateKey }) => {
|
|
106
106
|
const stateArr = values[stateKey];
|
|
107
107
|
const urlArr = params.getAll(urlKey);
|
|
108
|
+
const defaultValue = stateKey === 'indexes' ? defaults.indexes : undefined;
|
|
108
109
|
if (isEqual(stateArr, urlArr))
|
|
109
110
|
return;
|
|
110
|
-
const isDefault =
|
|
111
|
+
const isDefault = defaultValue ? isEqual(stateArr, defaultValue) : stateArr.length === 0;
|
|
111
112
|
if (!isDefault) {
|
|
112
113
|
changes[urlKey] = stateArr.length === 0 ? null : stateArr;
|
|
113
114
|
}
|
|
@@ -128,20 +129,21 @@ const useUrlSync = (values, _setValues, params, setParams, pathname, search, rou
|
|
|
128
129
|
}
|
|
129
130
|
// Drop scalar entries that already match the URL
|
|
130
131
|
return omitBy(changes, (val, key) => !ARRAY_URL_KEYS.has(key) && val == params.get(key));
|
|
131
|
-
}, [values, params, pathname]);
|
|
132
|
+
}, [values, defaults, params, pathname]);
|
|
132
133
|
const getStateFromUrl = useCallback(() => {
|
|
133
134
|
const changes = {};
|
|
134
135
|
// Scalar params: fall back to default when absent from URL
|
|
135
136
|
PARAM_MAPPINGS.forEach(([urlKey, stateKey]) => {
|
|
136
|
-
const urlValue = params.has(urlKey) ? params.get(urlKey) : (
|
|
137
|
+
const urlValue = params.has(urlKey) ? params.get(urlKey) : (defaults[stateKey] ?? undefined);
|
|
137
138
|
if (urlValue !== values[stateKey]) {
|
|
138
139
|
changes[stateKey] = urlValue;
|
|
139
140
|
}
|
|
140
141
|
});
|
|
141
142
|
// Array params: fall back to their declared default when absent from URL
|
|
142
|
-
ARRAY_PARAMS.forEach(({ urlKey, stateKey
|
|
143
|
+
ARRAY_PARAMS.forEach(({ urlKey, stateKey }) => {
|
|
143
144
|
const raw = params.getAll(urlKey);
|
|
144
|
-
const
|
|
145
|
+
const defaultValue = stateKey === 'indexes' ? defaults.indexes : undefined;
|
|
146
|
+
const resolved = (isEmpty(raw) && defaultValue ? defaultValue : uniq(raw));
|
|
145
147
|
if (!isEqual(resolved, values[stateKey])) {
|
|
146
148
|
changes[stateKey] = resolved;
|
|
147
149
|
}
|
|
@@ -155,7 +157,7 @@ const useUrlSync = (values, _setValues, params, setParams, pathname, search, rou
|
|
|
155
157
|
if (urlOffset !== values.offset)
|
|
156
158
|
changes.offset = urlOffset;
|
|
157
159
|
return omitBy(omitBy(changes, isUndefined), (val, key) => val == values[key]);
|
|
158
|
-
}, [values, params, pathname, routeId]);
|
|
160
|
+
}, [values, defaults, params, pathname, routeId]);
|
|
159
161
|
// State → URL
|
|
160
162
|
useEffect(() => {
|
|
161
163
|
const changes = getUrlFromState();
|
|
@@ -191,19 +193,18 @@ const useUrlSync = (values, _setValues, params, setParams, pathname, search, rou
|
|
|
191
193
|
/**
|
|
192
194
|
* Context responsible for tracking updates to query operations in hit and view search.
|
|
193
195
|
*/
|
|
194
|
-
const ParameterProvider = ({ children }) => {
|
|
196
|
+
const ParameterProvider = ({ children, defaults: _defaults = {} }) => {
|
|
195
197
|
const location = useLocation();
|
|
196
198
|
const routeParams = useParams();
|
|
197
199
|
const [params, setParams] = useSearchParams();
|
|
200
|
+
const defaults = useMemo(() => ({ ...DEFAULT_VALUES, ..._defaults }), [_defaults]);
|
|
198
201
|
const pendingChanges = useRef({});
|
|
199
202
|
const [values, _setValues] = useState({
|
|
200
203
|
selected: getSelectedValue(params, location.pathname, routeParams.id),
|
|
201
|
-
query: params.get('query') ??
|
|
202
|
-
sort: params.get('sort') ??
|
|
203
|
-
span: params.get('span') ??
|
|
204
|
-
indexes: params.has('index')
|
|
205
|
-
? uniq(params.getAll('index')).filter(identity)
|
|
206
|
-
: DEFAULT_VALUES.indexes,
|
|
204
|
+
query: params.get('query') ?? defaults.query,
|
|
205
|
+
sort: params.get('sort') ?? defaults.sort,
|
|
206
|
+
span: params.get('span') ?? defaults.span,
|
|
207
|
+
indexes: params.has('index') ? uniq(params.getAll('index')).filter(identity) : defaults.indexes,
|
|
207
208
|
filters: params.getAll('filter'),
|
|
208
209
|
views: params.getAll('view'),
|
|
209
210
|
startDate: params.get('start_date'),
|
|
@@ -212,7 +213,7 @@ const ParameterProvider = ({ children }) => {
|
|
|
212
213
|
trackTotalHits: (params.get('track_total_hits') ?? 'false') !== 'false'
|
|
213
214
|
});
|
|
214
215
|
// TODO: SELECTING A BUNDLE STILL CAUSES A FREAKOUT
|
|
215
|
-
useUrlSync(values, _setValues, params, setParams, location.pathname, location.search, routeParams.id);
|
|
216
|
+
useUrlSync(values, defaults, _setValues, params, setParams, location.pathname, location.search, routeParams.id);
|
|
216
217
|
const set = useCallback((key) => (value) => {
|
|
217
218
|
if (value === values[key])
|
|
218
219
|
return;
|
|
@@ -220,7 +221,7 @@ const ParameterProvider = ({ children }) => {
|
|
|
220
221
|
pendingChanges.current.selected = value;
|
|
221
222
|
}
|
|
222
223
|
else {
|
|
223
|
-
pendingChanges.current[key] = value ??
|
|
224
|
+
pendingChanges.current[key] = value ?? defaults[key] ?? null;
|
|
224
225
|
}
|
|
225
226
|
if (key === 'span' && typeof value === 'string' && !value.endsWith('custom')) {
|
|
226
227
|
pendingChanges.current.startDate = null;
|
|
@@ -230,7 +231,7 @@ const ParameterProvider = ({ children }) => {
|
|
|
230
231
|
_setValues(c => ({ ...c, ...pendingChanges.current }));
|
|
231
232
|
pendingChanges.current = {};
|
|
232
233
|
});
|
|
233
|
-
}, [values]);
|
|
234
|
+
}, [values, defaults]);
|
|
234
235
|
const setOffset = useCallback((_offset) => _setValues(c => ({ ...c, offset: parseOffset(_offset) })), []);
|
|
235
236
|
const setCustomSpan = useCallback((startDate, endDate) => _setValues(c => ({ ...c, startDate, endDate })), []);
|
|
236
237
|
const filters = useListHandlers('filters', _setValues);
|
|
@@ -252,7 +253,7 @@ const ParameterProvider = ({ children }) => {
|
|
|
252
253
|
removeIndex: indexes.remove,
|
|
253
254
|
setIndex: indexes.setAt,
|
|
254
255
|
setIndexes: indexes.setAll,
|
|
255
|
-
resetIndexes: useCallback(() => indexes.reset(
|
|
256
|
+
resetIndexes: useCallback(() => indexes.reset(defaults.indexes), [indexes, defaults]),
|
|
256
257
|
addView: views.add,
|
|
257
258
|
removeView: views.remove,
|
|
258
259
|
setView: views.setAt,
|
|
@@ -579,6 +579,18 @@ describe('ParameterContext', () => {
|
|
|
579
579
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.selected), { wrapper: Wrapper });
|
|
580
580
|
expect(hook.result.current).toBe('different_hit_id');
|
|
581
581
|
});
|
|
582
|
+
it('should not sync query when default query differs from global default', async () => {
|
|
583
|
+
const CustomDefaultWrapper = ({ children }) => {
|
|
584
|
+
return _jsx(ParameterProvider, { defaults: { query: '' }, children: children });
|
|
585
|
+
};
|
|
586
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.query), {
|
|
587
|
+
wrapper: CustomDefaultWrapper
|
|
588
|
+
});
|
|
589
|
+
expect(hook.result.current).toBe('');
|
|
590
|
+
await waitFor(() => {
|
|
591
|
+
expect(mockSetParams).not.toHaveBeenCalled();
|
|
592
|
+
});
|
|
593
|
+
});
|
|
582
594
|
});
|
|
583
595
|
describe('useParameterContextSelector', () => {
|
|
584
596
|
it('should allow selecting specific values from context', async () => {
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Typography } from '@mui/material';
|
|
3
3
|
import { Trans } from 'react-i18next';
|
|
4
4
|
const SearchTotal = ({ total, offset, pageLength, ...typographyProps }) => {
|
|
5
|
-
return (_jsx(Typography, { ...typographyProps, children: total
|
|
5
|
+
return (_jsx(Typography, { ...typographyProps, children: total < 1 ? (_jsx(Trans, { i18nKey: "search.result.showing.single", values: {
|
|
6
6
|
total: total
|
|
7
7
|
} })) : (_jsx(Trans, { i18nKey: "search.result.showing", values: {
|
|
8
8
|
total: total,
|
|
@@ -15,9 +15,9 @@ describe('SearchTotal', () => {
|
|
|
15
15
|
render(_jsx(SearchTotal, { total: 0, offset: 0, pageLength: 0 }), { wrapper: Wrapper });
|
|
16
16
|
expect(screen.getByText('No results')).toBeInTheDocument();
|
|
17
17
|
});
|
|
18
|
-
it('renders "
|
|
18
|
+
it('renders "Showing 1 to 1 of 1 results" text when total is 1', () => {
|
|
19
19
|
render(_jsx(SearchTotal, { total: 1, offset: 0, pageLength: 1 }), { wrapper: Wrapper });
|
|
20
|
-
expect(screen.getByText('
|
|
20
|
+
expect(screen.getByText('Showing 1 to 1 of 1 results')).toBeInTheDocument();
|
|
21
21
|
});
|
|
22
22
|
it('renders range text when total is greater than 1', () => {
|
|
23
23
|
render(_jsx(SearchTotal, { total: 50, offset: 0, pageLength: 25 }), { wrapper: Wrapper });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Search } from '@mui/icons-material';
|
|
3
|
+
import { CircularProgress, IconButton, InputAdornment, Stack, TextField } from '@mui/material';
|
|
4
|
+
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
5
|
+
import IndexPicker from '@cccsaurora/howler-ui/components/routes/hits/search/shared/IndexPicker';
|
|
6
|
+
import { useCallback, useState } from 'react';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import { useContextSelector } from 'use-context-selector';
|
|
9
|
+
const FuzzySearchBar = ({ onSearch, loading = false }) => {
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
const indexes = useContextSelector(ParameterContext, ctx => ctx.indexes);
|
|
12
|
+
const defaultQuery = useContextSelector(ParameterContext, ctx => ctx.query);
|
|
13
|
+
const [query, _setQuery] = useState(defaultQuery ?? '');
|
|
14
|
+
const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
|
|
15
|
+
const handleQueryChange = useCallback((e) => {
|
|
16
|
+
_setQuery(e.target.value);
|
|
17
|
+
}, [_setQuery]);
|
|
18
|
+
const handleSearch = useCallback(() => {
|
|
19
|
+
if (query.trim()) {
|
|
20
|
+
setQuery(query.trim());
|
|
21
|
+
onSearch(query.trim(), indexes);
|
|
22
|
+
}
|
|
23
|
+
}, [query, setQuery, onSearch, indexes]);
|
|
24
|
+
const handleKeyDown = useCallback((e) => {
|
|
25
|
+
if (e.key === 'Enter') {
|
|
26
|
+
handleSearch();
|
|
27
|
+
}
|
|
28
|
+
}, [handleSearch]);
|
|
29
|
+
return (_jsxs(Stack, { spacing: 1, children: [_jsx(TextField, { id: "fuzzy-search-input", size: "small", fullWidth: true, variant: "outlined", placeholder: t('search.fuzzy.placeholder'), value: query, onChange: handleQueryChange, onKeyDown: handleKeyDown, InputProps: {
|
|
30
|
+
endAdornment: (_jsx(InputAdornment, { position: "end", children: loading ? (_jsx(CircularProgress, { size: 24 })) : (_jsx(IconButton, { id: "fuzzy-search-button", onClick: handleSearch, edge: "end", disabled: !query.trim(), children: _jsx(Search, {}) })) }))
|
|
31
|
+
} }), _jsx(Stack, { direction: "row", children: _jsx(IndexPicker, { additionalOptions: [{ label: 'hit.search.index.case', value: 'case' }] }) })] }));
|
|
32
|
+
};
|
|
33
|
+
export default FuzzySearchBar;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent, {} from '@testing-library/user-event';
|
|
4
|
+
import { setupContextSelectorMock } from '@cccsaurora/howler-ui/tests/mocks';
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
setupContextSelectorMock();
|
|
7
|
+
const mockIndexPickerProps = vi.hoisted(() => ({
|
|
8
|
+
last: null
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('react-i18next', () => ({
|
|
11
|
+
useTranslation: () => ({
|
|
12
|
+
t: (key) => key
|
|
13
|
+
})
|
|
14
|
+
}));
|
|
15
|
+
vi.mock('components/routes/hits/search/shared/IndexPicker', () => ({
|
|
16
|
+
default: (props) => {
|
|
17
|
+
mockIndexPickerProps.last = props;
|
|
18
|
+
return _jsx("div", { id: "index-picker" });
|
|
19
|
+
}
|
|
20
|
+
}));
|
|
21
|
+
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
22
|
+
import FuzzySearchBar from './FuzzySearchBar';
|
|
23
|
+
const mockOnSearch = vi.fn();
|
|
24
|
+
const mockSetQuery = vi.fn();
|
|
25
|
+
const parameterContextValue = {
|
|
26
|
+
indexes: ['hit', 'case'],
|
|
27
|
+
query: '',
|
|
28
|
+
setQuery: mockSetQuery
|
|
29
|
+
};
|
|
30
|
+
const Wrapper = ({ children }) => {
|
|
31
|
+
return _jsx(ParameterContext.Provider, { value: parameterContextValue, children: children });
|
|
32
|
+
};
|
|
33
|
+
describe('FuzzySearchBar', () => {
|
|
34
|
+
let user;
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
user = userEvent.setup();
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
parameterContextValue.indexes = ['hit', 'case'];
|
|
39
|
+
parameterContextValue.query = '';
|
|
40
|
+
mockIndexPickerProps.last = null;
|
|
41
|
+
});
|
|
42
|
+
it('renders search input and index picker', () => {
|
|
43
|
+
render(_jsx(FuzzySearchBar, { onSearch: mockOnSearch }), { wrapper: Wrapper });
|
|
44
|
+
expect(screen.getByTestId('fuzzy-search-input')).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByTestId('index-picker')).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
it('disables search button when query is empty', () => {
|
|
48
|
+
render(_jsx(FuzzySearchBar, { onSearch: mockOnSearch }), { wrapper: Wrapper });
|
|
49
|
+
expect(screen.getByTestId('fuzzy-search-button')).toBeDisabled();
|
|
50
|
+
});
|
|
51
|
+
it('calls setQuery and onSearch with trimmed query and selected indexes on click', async () => {
|
|
52
|
+
render(_jsx(FuzzySearchBar, { onSearch: mockOnSearch }), { wrapper: Wrapper });
|
|
53
|
+
await user.type(screen.getByRole('textbox'), ' malware triage ');
|
|
54
|
+
await user.click(screen.getByTestId('fuzzy-search-button'));
|
|
55
|
+
expect(mockSetQuery).toHaveBeenCalledWith('malware triage');
|
|
56
|
+
expect(mockOnSearch).toHaveBeenCalledWith('malware triage', ['hit', 'case']);
|
|
57
|
+
});
|
|
58
|
+
it('triggers search on Enter key', async () => {
|
|
59
|
+
render(_jsx(FuzzySearchBar, { onSearch: mockOnSearch }), { wrapper: Wrapper });
|
|
60
|
+
await user.type(screen.getByRole('textbox'), 'suspicious ip{enter}');
|
|
61
|
+
expect(mockSetQuery).toHaveBeenCalledWith('suspicious ip');
|
|
62
|
+
expect(mockOnSearch).toHaveBeenCalledWith('suspicious ip', ['hit', 'case']);
|
|
63
|
+
});
|
|
64
|
+
it('does not call search for whitespace-only input', async () => {
|
|
65
|
+
render(_jsx(FuzzySearchBar, { onSearch: mockOnSearch }), { wrapper: Wrapper });
|
|
66
|
+
await user.type(screen.getByRole('textbox'), ' ');
|
|
67
|
+
expect(screen.getByTestId('fuzzy-search-button')).toBeDisabled();
|
|
68
|
+
expect(mockSetQuery).not.toHaveBeenCalled();
|
|
69
|
+
expect(mockOnSearch).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
it('shows loading spinner and hides search button when loading', () => {
|
|
72
|
+
render(_jsx(FuzzySearchBar, { onSearch: mockOnSearch, loading: true }), { wrapper: Wrapper });
|
|
73
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
|
74
|
+
expect(screen.queryByTestId('fuzzy-search-button')).not.toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
it('passes expected options to IndexPicker', () => {
|
|
77
|
+
render(_jsx(FuzzySearchBar, { onSearch: mockOnSearch }), { wrapper: Wrapper });
|
|
78
|
+
expect(mockIndexPickerProps.last).toEqual({
|
|
79
|
+
additionalOptions: [{ label: 'hit.search.index.case', value: 'case' }]
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
it('initializes input with default query from context', () => {
|
|
83
|
+
parameterContextValue.query = 'existing search';
|
|
84
|
+
render(_jsx(FuzzySearchBar, { onSearch: mockOnSearch }), { wrapper: Wrapper });
|
|
85
|
+
expect(screen.getByRole('textbox')).toHaveValue('existing search');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Skeleton, Stack, Typography } from '@mui/material';
|
|
3
|
+
import api from '@cccsaurora/howler-ui/api';
|
|
4
|
+
import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
|
|
5
|
+
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
6
|
+
import SearchPagination from '@cccsaurora/howler-ui/components/elements/addons/search/SearchPagination';
|
|
7
|
+
import SearchTotal from '@cccsaurora/howler-ui/components/elements/addons/search/SearchTotal';
|
|
8
|
+
import CaseCard from '@cccsaurora/howler-ui/components/elements/case/CaseCard';
|
|
9
|
+
import EventCard from '@cccsaurora/howler-ui/components/elements/event/EventCard';
|
|
10
|
+
import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
|
|
11
|
+
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
12
|
+
import FuzzySearchBar from '@cccsaurora/howler-ui/components/elements/search/FuzzySearchBar';
|
|
13
|
+
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
14
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
15
|
+
import { useOutletContext } from 'react-router-dom';
|
|
16
|
+
import { useContextSelector } from 'use-context-selector';
|
|
17
|
+
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
18
|
+
import { isCase, isEvent, isHit } from '@cccsaurora/howler-ui/utils/typeUtils';
|
|
19
|
+
const CaseSearch = () => {
|
|
20
|
+
const parentCase = useOutletContext();
|
|
21
|
+
const indexes = useContextSelector(ParameterContext, ctx => ctx.indexes);
|
|
22
|
+
const query = useContextSelector(ParameterContext, ctx => ctx.query);
|
|
23
|
+
const [hitLayout] = useMyLocalStorageItem(StorageKey.HIT_LAYOUT, HitLayout.NORMAL);
|
|
24
|
+
const [loading, setLoading] = useState(false);
|
|
25
|
+
const [response, setResponse] = useState(null);
|
|
26
|
+
const [error, setError] = useState(null);
|
|
27
|
+
const [offset, setOffset] = useState(0);
|
|
28
|
+
const caseIds = useMemo(() => parentCase
|
|
29
|
+
? [
|
|
30
|
+
parentCase.case_id,
|
|
31
|
+
...(parentCase.items ?? []).filter(item => item.type === 'case').map(item => item.value)
|
|
32
|
+
].filter((id) => !!id)
|
|
33
|
+
: [], [parentCase]);
|
|
34
|
+
const handleSearch = useCallback(async (_query, _indexes) => {
|
|
35
|
+
setLoading(true);
|
|
36
|
+
setError(null);
|
|
37
|
+
try {
|
|
38
|
+
// If no indexes specified, search across all types
|
|
39
|
+
const searchIndexes = _indexes.length > 0 ? _indexes : ['case', 'hit', 'event'];
|
|
40
|
+
// Add case_id to the filters to scope the search to this case and its sub-cases
|
|
41
|
+
const filters = parentCase?.case_id
|
|
42
|
+
? [`case_id:(${caseIds.join(' OR ')}) OR howler.related:(${caseIds.join(' OR ')})`]
|
|
43
|
+
: [];
|
|
44
|
+
setResponse(await api.v2.fuzzy.post({
|
|
45
|
+
query: _query,
|
|
46
|
+
indexes: searchIndexes,
|
|
47
|
+
rows: 25,
|
|
48
|
+
filters,
|
|
49
|
+
offset
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
setError(err.message || 'An error occurred while searching.');
|
|
54
|
+
setResponse(null);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
setLoading(false);
|
|
58
|
+
}
|
|
59
|
+
}, [caseIds, offset, parentCase?.case_id]);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (query) {
|
|
62
|
+
handleSearch(query, indexes);
|
|
63
|
+
}
|
|
64
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
65
|
+
}, [indexes, offset]);
|
|
66
|
+
return (_jsx(PageCenter, { maxWidth: "lg", textAlign: "left", children: _jsxs(Stack, { spacing: 1, children: [_jsx(FuzzySearchBar, { onSearch: handleSearch, loading: loading }), response && (_jsxs(Stack, { direction: "row", alignItems: "center", sx: { pt: 1 }, children: [_jsx(SearchTotal, { total: response.total, pageLength: response.items.length, offset: response.offset, sx: theme => ({ color: theme.palette.text.secondary, fontSize: '0.9em', fontStyle: 'italic' }) }), _jsx(Box, { flex: 1 }), _jsx(SearchPagination, { total: response.total, limit: response.rows, offset: response.offset, onChange: nextOffset => setOffset(nextOffset) })] })), error && (_jsx(Typography, { color: "error", sx: { mb: 2 }, children: error })), loading ? (_jsx(_Fragment, { children: _jsx(Skeleton, { variant: "rounded", height: 430 }) })) : ((response?.items ?? []).map(item => {
|
|
67
|
+
if (isHit(item)) {
|
|
68
|
+
return _jsx(HitCard, { id: item.howler.id, layout: hitLayout }, item.howler.id);
|
|
69
|
+
}
|
|
70
|
+
else if (isEvent(item)) {
|
|
71
|
+
return _jsx(EventCard, { id: item.howler.id, event: item }, item.howler.id);
|
|
72
|
+
}
|
|
73
|
+
else if (isCase(item)) {
|
|
74
|
+
return _jsx(CaseCard, { caseId: item.case_id, case: item }, item.case_id);
|
|
75
|
+
}
|
|
76
|
+
}))] }) }));
|
|
77
|
+
};
|
|
78
|
+
export default CaseSearch;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { DndContext, DragOverlay, MouseSensor, pointerWithin, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
|
|
3
|
-
import { CalendarMonth, Circle, Dashboard, Dataset, Rule } from '@mui/icons-material';
|
|
3
|
+
import { CalendarMonth, Circle, Dashboard, Dataset, Rule, Search } from '@mui/icons-material';
|
|
4
4
|
import { alpha, Box, Card, Chip, Divider, LinearProgress, Skeleton, Stack, Typography, useTheme } from '@mui/material';
|
|
5
5
|
import api from '@cccsaurora/howler-ui/api';
|
|
6
6
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
@@ -99,7 +99,7 @@ const CaseSidebar = ({ case: _case, update }) => {
|
|
|
99
99
|
maxHeight: 'calc(100vh - 64px)',
|
|
100
100
|
display: 'flex',
|
|
101
101
|
flexDirection: 'column'
|
|
102
|
-
}, children: [_jsxs(Card, { sx: { borderRadius: 0, px: 2, py: 1 }, children: [_case?.title ? _jsx(Typography, { variant: "body1", children: _case.title }) : _jsx(Skeleton, { height: 24 }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", divider: _jsx(Circle, { color: "disabled", sx: { fontSize: '8px' } }), children: [_jsxs(Typography, { variant: "caption", color: "textSecondary", children: [t('started'), ": ", _case?.created ? dayjs(_case.created).toString() : _jsx(Skeleton, { height: 14 })] }), _case?.escalation ? (_jsx(Chip, { color: ESCALATION_COLOR_MAP[_case.escalation], label: t(_case.escalation) })) : (_jsx(Skeleton, { height: 24 }))] })] }), _jsxs(Stack, { direction: "row", alignItems: "center", sx: navItemSx(location.pathname.endsWith(_case?.case_id)), component: Link, to: `/cases/${_case?.case_id}`, children: [_jsx(Dashboard, { fontSize: "small" }), _jsx(Typography, { variant: "body2", sx: { pl: 1, textWrap: 'nowrap' }, children: t('page.cases.dashboard') })] }), _jsxs(Stack, { direction: "row", alignItems: "center", sx: navItemSx(location.pathname.endsWith('observables')), component: Link, to: `/cases/${_case?.case_id}/observables`, children: [_jsx(Dataset, { fontSize: "small" }), _jsx(Typography, { variant: "body2", sx: { userSelect: 'none', pl: 1, textWrap: 'nowrap' }, children: t('page.cases.observables') })] }), _jsxs(Stack, { direction: "row", alignItems: "center", sx: navItemSx(location.pathname.endsWith('timeline')), component: Link, to: `/cases/${_case?.case_id}/timeline`, children: [_jsx(CalendarMonth, { fontSize: "small" }), _jsx(Typography, { variant: "body2", sx: { userSelect: 'none', pl: 1, textWrap: 'nowrap' }, children: t('page.cases.timeline') })] }), _jsxs(Stack, { direction: "row", alignItems: "center", sx: navItemSx(location.pathname.endsWith('rules')), component: Link, to: `/cases/${_case?.case_id}/rules`, children: [_jsx(Rule, {}), _jsx(Typography, { variant: "body2", sx: { userSelect: 'none', pl: 1, textWrap: 'nowrap' }, children: t('page.cases.rules') })] }), _jsx(Divider, {}), _case && (_jsx(Box, { flex: 1, overflow: "auto", width: "100%", sx: {
|
|
102
|
+
}, children: [_jsxs(Card, { sx: { borderRadius: 0, px: 2, py: 1 }, children: [_case?.title ? _jsx(Typography, { variant: "body1", children: _case.title }) : _jsx(Skeleton, { height: 24 }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", divider: _jsx(Circle, { color: "disabled", sx: { fontSize: '8px' } }), children: [_jsxs(Typography, { variant: "caption", color: "textSecondary", children: [t('started'), ": ", _case?.created ? dayjs(_case.created).toString() : _jsx(Skeleton, { height: 14 })] }), _case?.escalation ? (_jsx(Chip, { color: ESCALATION_COLOR_MAP[_case.escalation], label: t(_case.escalation) })) : (_jsx(Skeleton, { height: 24 }))] })] }), _jsxs(Stack, { direction: "row", alignItems: "center", sx: navItemSx(location.pathname.endsWith(_case?.case_id)), component: Link, to: `/cases/${_case?.case_id}`, children: [_jsx(Dashboard, { fontSize: "small" }), _jsx(Typography, { variant: "body2", sx: { pl: 1, textWrap: 'nowrap' }, children: t('page.cases.dashboard') })] }), _jsxs(Stack, { direction: "row", alignItems: "center", sx: navItemSx(location.pathname.endsWith('search')), component: Link, to: `/cases/${_case?.case_id}/search`, children: [_jsx(Search, { fontSize: "small" }), _jsx(Typography, { variant: "body2", sx: { userSelect: 'none', pl: 1, textWrap: 'nowrap' }, children: t('page.cases.search') })] }), _jsxs(Stack, { direction: "row", alignItems: "center", sx: navItemSx(location.pathname.endsWith('observables')), component: Link, to: `/cases/${_case?.case_id}/observables`, children: [_jsx(Dataset, { fontSize: "small" }), _jsx(Typography, { variant: "body2", sx: { userSelect: 'none', pl: 1, textWrap: 'nowrap' }, children: t('page.cases.observables') })] }), _jsxs(Stack, { direction: "row", alignItems: "center", sx: navItemSx(location.pathname.endsWith('timeline')), component: Link, to: `/cases/${_case?.case_id}/timeline`, children: [_jsx(CalendarMonth, { fontSize: "small" }), _jsx(Typography, { variant: "body2", sx: { userSelect: 'none', pl: 1, textWrap: 'nowrap' }, children: t('page.cases.timeline') })] }), _jsxs(Stack, { direction: "row", alignItems: "center", sx: navItemSx(location.pathname.endsWith('rules')), component: Link, to: `/cases/${_case?.case_id}/rules`, children: [_jsx(Rule, {}), _jsx(Typography, { variant: "body2", sx: { userSelect: 'none', pl: 1, textWrap: 'nowrap' }, children: t('page.cases.rules') })] }), _jsx(Divider, {}), _case && (_jsx(Box, { flex: 1, overflow: "auto", width: "100%", sx: {
|
|
103
103
|
position: 'relative',
|
|
104
104
|
borderRight: `thin solid ${theme.palette.divider}`
|
|
105
105
|
}, children: _jsxs(Box, { position: "absolute", sx: { left: 0, right: 0 }, children: [_jsx(LinearProgress, { sx: { mb: 0.5, opacity: +loading } }), _jsxs(DndContext, { sensors: sensors, collisionDetection: pointerWithin, onDragStart: handleDragStart, onDragEnd: handleDragEnd, children: [_jsx(CaseFolder, { case: _case, onItemUpdated: update }), _jsx(RootDropZone, { caseId: _case.case_id }), _jsx(DragOverlay, { dropAnimation: null, children: activeDragData && (_jsx(FolderEntry, { caseId: null, path: "", indent: 0, label: activeDragData.label, itemType: activeDragData.type })) })] })] }) }))] }));
|
|
@@ -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",
|
|
@@ -411,11 +412,11 @@
|
|
|
411
412
|
"page.cases.dashboard.target": "Targets",
|
|
412
413
|
"page.cases.dashboard.tasks": "Tasks",
|
|
413
414
|
"page.cases.dashboard.tasks.add": "Add Task",
|
|
415
|
+
"page.cases.dashboard.tasks.child.empty": "No tasks in this child case.",
|
|
414
416
|
"page.cases.dashboard.tasks.child_cases": "Child Cases",
|
|
415
417
|
"page.cases.dashboard.tasks.filter_cases": "Filter child cases",
|
|
416
|
-
"page.cases.dashboard.tasks.show_child": "Show child case tasks",
|
|
417
418
|
"page.cases.dashboard.tasks.hide_child": "Hide child case tasks",
|
|
418
|
-
"page.cases.dashboard.tasks.
|
|
419
|
+
"page.cases.dashboard.tasks.show_child": "Show child case tasks",
|
|
419
420
|
"page.cases.dashboard.threat": "Threats",
|
|
420
421
|
"page.cases.detail.participants": "Participants",
|
|
421
422
|
"page.cases.detail.properties": "Properties",
|
|
@@ -434,6 +435,7 @@
|
|
|
434
435
|
"page.cases.rules.no_expiry": "No expiry",
|
|
435
436
|
"page.cases.rules.query": "Match Query",
|
|
436
437
|
"page.cases.rules.timeframe": "Rule Expiry",
|
|
438
|
+
"page.cases.search": "Search",
|
|
437
439
|
"page.cases.sidebar.folder.remove": "Remove folder",
|
|
438
440
|
"page.cases.sidebar.item.open": "Open item",
|
|
439
441
|
"page.cases.sidebar.item.remove": "Remove item",
|
|
@@ -847,6 +849,13 @@
|
|
|
847
849
|
"rule.interval.thirty.minutes": "Every thirty minutes",
|
|
848
850
|
"rule.interval.three.hours": "Every three hours",
|
|
849
851
|
"save": "Save",
|
|
852
|
+
"search.fuzzy.no_results": "No results found.",
|
|
853
|
+
"search.fuzzy.placeholder": "Search by IP, domain, hash, email, or keyword...",
|
|
854
|
+
"search.fuzzy.results.cases": "Cases",
|
|
855
|
+
"search.fuzzy.results.events": "Events",
|
|
856
|
+
"search.fuzzy.results.hits": "Hits",
|
|
857
|
+
"search.fuzzy.title": "Fuzzy Search",
|
|
858
|
+
"search.fuzzy.total_results": "results found",
|
|
850
859
|
"search.layout.settings": "Edit search result layout",
|
|
851
860
|
"search.open": "Open Search",
|
|
852
861
|
"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",
|
|
@@ -411,11 +412,11 @@
|
|
|
411
412
|
"page.cases.dashboard.target": "Cibles",
|
|
412
413
|
"page.cases.dashboard.tasks": "Tâches",
|
|
413
414
|
"page.cases.dashboard.tasks.add": "Ajouter une tâche",
|
|
415
|
+
"page.cases.dashboard.tasks.child.empty": "Aucune tâche dans ce cas enfant.",
|
|
414
416
|
"page.cases.dashboard.tasks.child_cases": "Cas enfants",
|
|
415
417
|
"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
418
|
"page.cases.dashboard.tasks.hide_child": "Masquer les tâches des cas enfants",
|
|
418
|
-
"page.cases.dashboard.tasks.
|
|
419
|
+
"page.cases.dashboard.tasks.show_child": "Afficher les tâches des cas enfants",
|
|
419
420
|
"page.cases.dashboard.threat": "Menaces",
|
|
420
421
|
"page.cases.detail.participants": "Participants",
|
|
421
422
|
"page.cases.detail.properties": "Propriétés",
|
|
@@ -434,6 +435,7 @@
|
|
|
434
435
|
"page.cases.rules.no_expiry": "Sans expiration",
|
|
435
436
|
"page.cases.rules.query": "Requête de correspondance",
|
|
436
437
|
"page.cases.rules.timeframe": "Expiration de la règle",
|
|
438
|
+
"page.cases.search": "Rechercher",
|
|
437
439
|
"page.cases.sidebar.folder.remove": "Supprimer le dossier",
|
|
438
440
|
"page.cases.sidebar.item.open": "Ouvrir l'élément",
|
|
439
441
|
"page.cases.sidebar.item.remove": "Supprimer l'élément",
|
|
@@ -847,6 +849,13 @@
|
|
|
847
849
|
"rule.interval.thirty.minutes": "Toutes les trente minutes",
|
|
848
850
|
"rule.interval.three.hours": "Toutes les trois heures",
|
|
849
851
|
"save": "Sauvegarder",
|
|
852
|
+
"search.fuzzy.no_results": "Aucun résultat trouvé.",
|
|
853
|
+
"search.fuzzy.placeholder": "Rechercher par IP, domaine, hash, courriel ou mot-clé...",
|
|
854
|
+
"search.fuzzy.results.cases": "Cas",
|
|
855
|
+
"search.fuzzy.results.events": "Événements",
|
|
856
|
+
"search.fuzzy.results.hits": "Alertes",
|
|
857
|
+
"search.fuzzy.title": "Recherche floue",
|
|
858
|
+
"search.fuzzy.total_results": "résultats trouvés",
|
|
850
859
|
"search.layout.settings": "Modifier la présentation des résultats de recherche",
|
|
851
860
|
"search.open": "Ouvrir la recherche",
|
|
852
861
|
"search.result.showing": "Affichage de {{offset}} à {{length}} sur {{total}} articles",
|
package/package.json
CHANGED
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"internal-slot": "1.0.7"
|
|
97
97
|
},
|
|
98
98
|
"type": "module",
|
|
99
|
-
"version": "2.19.0-cases.
|
|
99
|
+
"version": "2.19.0-cases.1122",
|
|
100
100
|
"exports": {
|
|
101
101
|
"./i18n": "./i18n.js",
|
|
102
102
|
"./index.css": "./index.css",
|
|
@@ -216,6 +216,7 @@
|
|
|
216
216
|
"./components/app/providers/*": "./components/app/providers/*.js",
|
|
217
217
|
"./components/app/drawers/*": "./components/app/drawers/*.js",
|
|
218
218
|
"./components/app/hooks/*": "./components/app/hooks/*.js",
|
|
219
|
+
"./components/elements/search/*": "./components/elements/search/*.js",
|
|
219
220
|
"./components/elements/view/*": "./components/elements/view/*.js",
|
|
220
221
|
"./components/elements/event/*": "./components/elements/event/*.js",
|
|
221
222
|
"./components/elements/case/*": "./components/elements/case/*.js",
|