@cccsaurora/howler-ui 2.13.0-dev.163 → 2.13.0-dev.169
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/json/JSONViewer.d.ts +2 -0
- package/components/elements/display/json/JSONViewer.js +6 -13
- package/components/routes/advanced/QueryBuilder.js +44 -8
- package/components/routes/hits/search/InformationPane.js +41 -37
- package/locales/en/translation.json +7 -2
- package/locales/fr/translation.json +7 -2
- package/package.json +1 -1
- package/utils/stringUtils.d.ts +1 -0
- package/utils/stringUtils.js +9 -0
|
@@ -8,10 +8,11 @@ import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/us
|
|
|
8
8
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
9
|
import { useTranslation } from 'react-i18next';
|
|
10
10
|
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
11
|
+
import { validateRegex } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
11
12
|
import Throttler from '@cccsaurora/howler-ui/utils/Throttler';
|
|
12
13
|
import { removeEmpty, searchObject } from '@cccsaurora/howler-ui/utils/utils';
|
|
13
14
|
const THROTTLER = new Throttler(150);
|
|
14
|
-
const JSONViewer = ({ data, collapse = true }) => {
|
|
15
|
+
const JSONViewer = ({ data, collapse = true, hideSearch = false, filter }) => {
|
|
15
16
|
const { t } = useTranslation();
|
|
16
17
|
const { isDark } = useAppTheme();
|
|
17
18
|
const [compact] = useMyLocalStorageItem(StorageKey.COMPACT_JSON, true);
|
|
@@ -21,19 +22,11 @@ const JSONViewer = ({ data, collapse = true }) => {
|
|
|
21
22
|
useEffect(() => {
|
|
22
23
|
THROTTLER.debounce(() => {
|
|
23
24
|
const filteredData = removeEmpty(data, compact);
|
|
24
|
-
const searchedData = searchObject(filteredData, query, flat);
|
|
25
|
+
const searchedData = searchObject(filteredData, filter ?? query, flat);
|
|
25
26
|
setResult(searchedData);
|
|
26
27
|
});
|
|
27
|
-
}, [compact, data, flat, query]);
|
|
28
|
-
const hasError = useMemo(() =>
|
|
29
|
-
try {
|
|
30
|
-
new RegExp(query);
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
catch (e) {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
}, [query]);
|
|
28
|
+
}, [compact, data, filter, flat, query]);
|
|
29
|
+
const hasError = useMemo(() => !validateRegex(filter ?? query), [query, filter]);
|
|
37
30
|
const shouldCollapse = useCallback((field) => {
|
|
38
31
|
return (field.name !== 'root' && field.type !== 'object') || field.namespace.length > 3;
|
|
39
32
|
}, []);
|
|
@@ -48,6 +41,6 @@ const JSONViewer = ({ data, collapse = true }) => {
|
|
|
48
41
|
// Type declaration is wrong - this is a valid prop
|
|
49
42
|
displayArrayKey: !compact
|
|
50
43
|
} })), [collapse, compact, isDark, result, shouldCollapse]);
|
|
51
|
-
return data ? (_jsxs(Stack, { direction: "column", spacing: 1, sx: { '& > div:first-of-type': { mt: 1, mr: 0.5 } }, children: [_jsx(Phrase, { value: query, onChange: setQuery, error: hasError, label: t('json.viewer.search.label'), placeholder: t('json.viewer.search.prompt'), disabled: !result, endAdornment: _jsx(IconButton, { onClick: () => setQuery(''), children: _jsx(Clear, {}) }) }), renderer] })) : (_jsx(Skeleton, { width: "100%", height: "95%", variant: "rounded" }));
|
|
44
|
+
return data ? (_jsxs(Stack, { direction: "column", spacing: 1, sx: { '& > div:first-of-type': { mt: 1, mr: 0.5 } }, children: [!hideSearch && (_jsx(Phrase, { value: query, onChange: setQuery, error: hasError, label: t('json.viewer.search.label'), placeholder: t('json.viewer.search.prompt'), disabled: !result, endAdornment: _jsx(IconButton, { onClick: () => setQuery(''), children: _jsx(Clear, {}) }) })), renderer] })) : (_jsx(Skeleton, { width: "100%", height: "95%", variant: "rounded" }));
|
|
52
45
|
};
|
|
53
46
|
export default JSONViewer;
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useMonaco } from '@monaco-editor/react';
|
|
4
4
|
import { OpenInNew, PlayArrowOutlined, SsidChart } from '@mui/icons-material';
|
|
5
5
|
import { Alert, AlertTitle, Autocomplete, Box, Card, Checkbox, Chip, CircularProgress, FormControlLabel, IconButton, ListItemText, Slider, Stack, TextField, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material';
|
|
6
|
+
import Popper, {} from '@mui/material/Popper';
|
|
6
7
|
import api from '@cccsaurora/howler-ui/api';
|
|
7
8
|
import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
|
|
8
9
|
import { parseEvent } from '@cccsaurora/howler-ui/commons/components/utils/keyboard';
|
|
@@ -61,6 +62,10 @@ falsepositives:
|
|
|
61
62
|
level: informational
|
|
62
63
|
`
|
|
63
64
|
};
|
|
65
|
+
const LUCENE_QUERY_OPTIONS = ['default', 'facet', 'groupby'];
|
|
66
|
+
const CustomPopper = (props) => {
|
|
67
|
+
return _jsx(Popper, { ...props, style: { width: 'fit-content' }, placement: "bottom-start" });
|
|
68
|
+
};
|
|
64
69
|
const QueryBuilder = () => {
|
|
65
70
|
const { t } = useTranslation();
|
|
66
71
|
const theme = useTheme();
|
|
@@ -73,6 +78,8 @@ const QueryBuilder = () => {
|
|
|
73
78
|
const [type, setType] = useState('lucene');
|
|
74
79
|
const [loading, setLoading] = useState(false);
|
|
75
80
|
const [query, setQuery] = useState(DEFAULT_VALUES.lucene);
|
|
81
|
+
const [queryType, setQueryType] = useState(LUCENE_QUERY_OPTIONS[0]);
|
|
82
|
+
const [groupByField, setGroupByField] = useState(null);
|
|
76
83
|
const [allFields, setAllFields] = useState(true);
|
|
77
84
|
const [fields, setFields] = useState(['howler.id']);
|
|
78
85
|
const [response, setResponse] = useState(null);
|
|
@@ -90,10 +97,25 @@ const QueryBuilder = () => {
|
|
|
90
97
|
};
|
|
91
98
|
let result;
|
|
92
99
|
if (type === 'lucene') {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
if (queryType === 'facet') {
|
|
101
|
+
result = await api.search.facet.hit.post({
|
|
102
|
+
query: sanitizeMultilineLucene(query),
|
|
103
|
+
rows: STEPS[rows],
|
|
104
|
+
fields
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
else if (queryType === 'groupby') {
|
|
108
|
+
result = await api.search.grouped.hit.post(groupByField, {
|
|
109
|
+
query: sanitizeMultilineLucene(query),
|
|
110
|
+
...searchProperties
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
result = await api.search.hit.post({
|
|
115
|
+
query: sanitizeMultilineLucene(query),
|
|
116
|
+
...searchProperties
|
|
117
|
+
});
|
|
118
|
+
}
|
|
97
119
|
}
|
|
98
120
|
else if (type === 'eql') {
|
|
99
121
|
result = await api.search.hit.eql.post({
|
|
@@ -116,7 +138,7 @@ const QueryBuilder = () => {
|
|
|
116
138
|
finally {
|
|
117
139
|
setLoading(false);
|
|
118
140
|
}
|
|
119
|
-
}, [allFields, fields, query, rows, type]);
|
|
141
|
+
}, [allFields, fields, groupByField, query, queryType, rows, type]);
|
|
120
142
|
const onKeyDown = useCallback(event => {
|
|
121
143
|
const parsedEvent = parseEvent(event);
|
|
122
144
|
if (parsedEvent.isCtrl && parsedEvent.isEnter) {
|
|
@@ -146,6 +168,12 @@ const QueryBuilder = () => {
|
|
|
146
168
|
maxHeight: '85vh'
|
|
147
169
|
}));
|
|
148
170
|
}, [query, response, showModal, showWarningMessage, t, type]);
|
|
171
|
+
const searchDisabled = useMemo(() => type === 'lucene' && queryType === 'groupby' && !groupByField, [groupByField, queryType, type]);
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (type !== 'lucene' && queryType !== 'default') {
|
|
174
|
+
setQueryType('default');
|
|
175
|
+
}
|
|
176
|
+
}, [queryType, type]);
|
|
149
177
|
useEffect(() => {
|
|
150
178
|
if (!monaco) {
|
|
151
179
|
return;
|
|
@@ -190,13 +218,21 @@ const QueryBuilder = () => {
|
|
|
190
218
|
display: 'flex',
|
|
191
219
|
alignSelf: 'stretch'
|
|
192
220
|
}
|
|
193
|
-
}, children: [smallButtons ? (_jsx(Tooltip, { title: t('route.actions.execute'), children: _jsx(IconButton, { size: "small", sx: { alignSelf: 'start' }, color: "success", onClick: execute, children: loading ? _jsx(CircularProgress, { size: 18, color: "success" }) : _jsx(PlayArrowOutlined, { color:
|
|
221
|
+
}, children: [smallButtons ? (_jsx(Tooltip, { title: t('route.actions.execute'), children: _jsx(IconButton, { size: "small", sx: { alignSelf: 'start' }, color: "success", onClick: execute, disabled: searchDisabled, children: loading ? (_jsx(CircularProgress, { size: 18, color: "success" })) : (_jsx(PlayArrowOutlined, { color: searchDisabled ? 'disabled' : 'success' })) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", startIcon: loading ? (_jsx(CircularProgress, { size: 18, color: "success" })) : (_jsx(PlayArrowOutlined, { color: searchDisabled ? 'disabled' : 'success', sx: { '& path': { stroke: 'currentcolor', strokeWidth: '1px' } } })), color: "success", onClick: execute, disabled: searchDisabled, children: t('route.actions.execute') })), _jsxs(Stack, { direction: compactLayout ? 'column' : 'row', spacing: 1, children: [_jsx(Autocomplete, { value: type, onChange: (__, value) => setType(value), options: QUERY_TYPES, getOptionLabel: option => t(`route.advanced.query.${option}`), renderOption: (props, option) => (_jsx(ListItemText, { ...props, sx: { flexDirection: 'column', alignItems: 'start !important' }, primary: t(`route.advanced.query.${option}`), secondary: t(`route.advanced.query.${option}.description`) })), renderInput: params => (_jsx(TextField, { ...params, size: "small", label: t('route.advanced.type'), sx: { minWidth: '250px' } })), sx: [
|
|
194
222
|
!compactLayout && {
|
|
195
223
|
height: '100%',
|
|
196
224
|
'& .MuiFormControl-root': { height: '100%', '& > div': { height: '100%' } }
|
|
197
225
|
}
|
|
198
|
-
], slotProps: { paper: { sx: { minWidth: '600px' } } } }), _jsx(Card, { variant: "outlined", sx: { flex: 1, maxWidth: '350px', minWidth: '210px' }, children: _jsxs(Stack, { spacing: 0.5, sx: { px: 1, alignItems: 'start' }, children: [_jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: [t('route.advanced.rows'), ": ", STEPS[rows]] }), _jsx(Slider, { size: "small", valueLabelDisplay: "off", value: rows, onChange: (_, value) => setRows(value), min: 0, max: 9, step: 1, marks: true, track: false, sx: { py: 0.5 } })] }) })] }), allFields ? (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: allFields, onChange: (__, checked) => setAllFields(checked) }), label: t('route.advanced.fields.all'), sx: { '& > span': { color: 'text.secondary' }, alignSelf: 'start' } })) : (_jsx(Autocomplete, { fullWidth: true, renderTags: values => values.length <= 3 ? (_jsx(Stack, { direction: "row", spacing: 0.5, children: values.map(_value => (_jsx(Chip, { size: "small", label: _value }, _value))) })) : (_jsx(Tooltip, { title: _jsx(Stack, { spacing: 1, children: values.map(_value => (_jsx("span", { children: _value }, _value))) }), children: _jsx(Chip, { size: "small", label: values.length }) })), multiple: true, size: "small", options: fieldOptions, value: fields, onChange: (__, values) => (values.length > 0 ? setFields(values) : setAllFields(true)), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.fields') }), sx: { maxWidth: '500px', width: '20vw', minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown })), _jsx(FlexOne, {}), type === 'lucene' &&
|
|
199
|
-
(smallButtons ? (_jsx(Tooltip, { title: t('route.advanced.open'), children: _jsx(IconButton, { color: "primary", sx: { alignSelf: 'center' }, component: Link, disabled: !response, to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}`, children: _jsx(OpenInNew, { fontSize: "small" }) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", startIcon: _jsx(OpenInNew, {}), component: Link, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, children: t('route.advanced.open') }))), smallButtons ? (_jsx(Tooltip, { title: response ? t('route.advanced.create.rule') : t('route.advanced.create.rule.disabled'), children: _jsx(IconButton, { size: "small", sx: { alignSelf: 'center' }, color: "info", onClick: onCreateRule, disabled: !response, children: _jsx(SsidChart, {}) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", color: "info", startIcon: _jsx(SsidChart, {}), onClick: onCreateRule, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, tooltip: !response && t('route.advanced.create.rule.disabled'), children: t('route.advanced.create.rule') }))] }), _jsxs(Box, { width: "100%", height: "calc(100vh - 112px)", sx: { position: 'relative', overflow: 'hidden', borderTop: `thin solid ${theme.palette.divider}` }, children: [_jsx(Box, { sx: {
|
|
226
|
+
], slotProps: { paper: { sx: { minWidth: '600px' } } } }), _jsx(Card, { variant: "outlined", sx: { flex: 1, maxWidth: '350px', minWidth: '210px' }, children: _jsxs(Stack, { spacing: 0.5, sx: { px: 1, alignItems: 'start' }, children: [_jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: [t('route.advanced.rows'), ": ", STEPS[rows]] }), _jsx(Slider, { size: "small", valueLabelDisplay: "off", value: rows, onChange: (_, value) => setRows(value), min: 0, max: 9, step: 1, marks: true, track: false, sx: { py: 0.5 } })] }) })] }), type === 'lucene' && (_jsx(Autocomplete, { size: "small", getOptionLabel: opt => t(`route.advanced.query.type.${opt}`), options: LUCENE_QUERY_OPTIONS, value: queryType, onChange: (_event, value) => setQueryType(value), renderInput: params => (_jsx(TextField, { ...params, label: t('route.advanced.query.type'), sx: { minWidth: '200px' } })) })), queryType === 'groupby' && (_jsx(Autocomplete, { size: "small", options: fieldOptions, value: groupByField, onChange: (__, value) => setGroupByField(value), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.pivot.field') }), sx: { minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), allFields && queryType !== 'facet' ? (_jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: allFields, onChange: (__, checked) => setAllFields(checked) }), label: t('route.advanced.fields.all'), sx: { '& > span': { color: 'text.secondary' }, alignSelf: 'start' } })) : (_jsx(Autocomplete, { fullWidth: true, renderTags: values => values.length <= 3 ? (_jsx(Stack, { direction: "row", spacing: 0.5, children: values.map(_value => (_jsx(Chip, { size: "small", label: _value }, _value))) })) : (_jsx(Tooltip, { title: _jsx(Stack, { spacing: 1, children: values.map(_value => (_jsx("span", { children: _value }, _value))) }), children: _jsx(Chip, { size: "small", label: values.length }) })), multiple: true, size: "small", options: fieldOptions, value: fields, onChange: (__, values) => (values.length > 0 ? setFields(values) : setAllFields(true)), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.fields') }), sx: { maxWidth: '500px', width: '20vw', minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), _jsx(FlexOne, {}), type === 'lucene' &&
|
|
227
|
+
(smallButtons ? (_jsx(Tooltip, { title: t('route.advanced.open'), children: _jsx(IconButton, { color: "primary", sx: { alignSelf: 'center' }, component: Link, disabled: !response, to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}`, children: _jsx(OpenInNew, { fontSize: "small" }) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", startIcon: _jsx(OpenInNew, {}), component: Link, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, children: t('route.advanced.open') }))), smallButtons ? (_jsx(Tooltip, { title: response ? t('route.advanced.create.rule') : t('route.advanced.create.rule.disabled'), children: _jsx(IconButton, { size: "small", sx: { alignSelf: 'center' }, color: "info", onClick: onCreateRule, disabled: !response, children: _jsx(SsidChart, {}) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", color: "info", startIcon: _jsx(SsidChart, {}), onClick: onCreateRule, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, tooltip: !response && t('route.advanced.create.rule.disabled'), children: t('route.advanced.create.rule') }))] }), _jsxs(Box, { width: "100%", height: "calc(100vh - 112px)", sx: { position: 'relative', overflow: 'hidden', borderTop: `thin solid ${theme.palette.divider}` }, children: [_jsx(Box, { sx: {
|
|
228
|
+
position: 'absolute',
|
|
229
|
+
top: 0,
|
|
230
|
+
left: 0,
|
|
231
|
+
bottom: 0,
|
|
232
|
+
right: `calc(50% + 7px - ${x}px)`,
|
|
233
|
+
pt: 1,
|
|
234
|
+
display: 'flex'
|
|
235
|
+
}, children: _jsx(QueryEditor, { query: query, setQuery: setQuery, language: type, height: "100%" }) }), _jsx(Box, { sx: {
|
|
200
236
|
position: 'absolute',
|
|
201
237
|
top: 0,
|
|
202
238
|
bottom: 0,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Clear, Code, Comment, DataObject, History, LinkSharp, OpenInNew, QueryStats } from '@mui/icons-material';
|
|
3
|
-
import { Badge, Box, Divider, Skeleton, Stack, Tab, Tabs, Tooltip, useTheme } from '@mui/material';
|
|
3
|
+
import { Badge, Box, Divider, IconButton, Skeleton, Stack, Tab, Tabs, Tooltip, useTheme } from '@mui/material';
|
|
4
4
|
import TuiIconButton from '@cccsaurora/howler-ui/components/elements/addons/buttons/CustomIconButton';
|
|
5
5
|
import { Icon } from '@iconify/react/dist/iconify.js';
|
|
6
6
|
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
@@ -11,6 +11,7 @@ import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/Fle
|
|
|
11
11
|
import VSBox from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBox';
|
|
12
12
|
import VSBoxContent from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBoxContent';
|
|
13
13
|
import VSBoxHeader from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBoxHeader';
|
|
14
|
+
import Phrase from '@cccsaurora/howler-ui/components/elements/addons/search/phrase/Phrase';
|
|
14
15
|
import BundleButton from '@cccsaurora/howler-ui/components/elements/display/icons/BundleButton';
|
|
15
16
|
import SocketBadge from '@cccsaurora/howler-ui/components/elements/display/icons/SocketBadge';
|
|
16
17
|
import JSONViewer from '@cccsaurora/howler-ui/components/elements/display/json/JSONViewer';
|
|
@@ -38,6 +39,7 @@ import { usePluginStore } from 'react-pluggable';
|
|
|
38
39
|
import { useLocation } from 'react-router-dom';
|
|
39
40
|
import { useContextSelector } from 'use-context-selector';
|
|
40
41
|
import { getUserList } from '@cccsaurora/howler-ui/utils/hitFunctions';
|
|
42
|
+
import { validateRegex } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
41
43
|
import { tryParse } from '@cccsaurora/howler-ui/utils/utils';
|
|
42
44
|
import LeadRenderer from '../view/LeadRenderer';
|
|
43
45
|
const InformationPane = ({ onClose }) => {
|
|
@@ -55,6 +57,7 @@ const InformationPane = ({ onClose }) => {
|
|
|
55
57
|
const [tab, setTab] = useState('overview');
|
|
56
58
|
const [loading, setLoading] = useState(false);
|
|
57
59
|
const [dossiers, setDossiers] = useState([]);
|
|
60
|
+
const [filter, setFilter] = useState('');
|
|
58
61
|
const users = useMyUserList(userIds);
|
|
59
62
|
const hit = useContextSelector(HitContext, ctx => ctx.hits[selected]);
|
|
60
63
|
howlerPluginStore.plugins.forEach(plugin => {
|
|
@@ -128,8 +131,8 @@ const InformationPane = ({ onClose }) => {
|
|
|
128
131
|
overview: () => _jsx(HitOverview, { hit: hit }),
|
|
129
132
|
details: () => _jsx(HitDetails, { hit: hit }),
|
|
130
133
|
hit_comments: () => _jsx(HitComments, { hit: hit, users: users }),
|
|
131
|
-
hit_raw: () => _jsx(JSONViewer, { data: !loading && hit }),
|
|
132
|
-
hit_data: () => (_jsx(JSONViewer, { data: !loading && hit?.howler?.data?.map(entry => tryParse(entry)), collapse: false })),
|
|
134
|
+
hit_raw: () => _jsx(JSONViewer, { data: !loading && hit, hideSearch: true, filter: filter }),
|
|
135
|
+
hit_data: () => (_jsx(JSONViewer, { data: !loading && hit?.howler?.data?.map(entry => tryParse(entry)), collapse: false, hideSearch: true, filter: filter })),
|
|
133
136
|
hit_worklog: () => _jsx(HitWorklog, { hit: !loading && hit, users: users }),
|
|
134
137
|
hit_aggregate: () => _jsx(HitSummary, { query: `howler.bundles:(${hit?.howler?.id})` }),
|
|
135
138
|
hit_related: () => _jsx(HitRelated, { hit: hit }),
|
|
@@ -139,7 +142,8 @@ const InformationPane = ({ onClose }) => {
|
|
|
139
142
|
() => _jsx(LeadRenderer, { lead: _lead, hit: hit })
|
|
140
143
|
])))
|
|
141
144
|
}[tab]?.();
|
|
142
|
-
}, [dossiers, hit, loading, tab, users]);
|
|
145
|
+
}, [dossiers, filter, hit, loading, tab, users]);
|
|
146
|
+
const hasError = useMemo(() => !validateRegex(filter), [filter]);
|
|
143
147
|
return (_jsxs(VSBox, { top: 10, sx: { height: '100%', flex: 1 }, children: [_jsxs(Stack, { direction: "column", flex: 1, sx: { overflowY: 'auto', flexGrow: 1 }, position: "relative", spacing: 1, ml: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, flexShrink: 0, pr: 2, sx: [hit?.howler?.is_bundle && { position: 'absolute', top: 1, right: 0, zIndex: 1100 }], children: [_jsx(FlexOne, {}), onClose && !location.pathname.startsWith('/bundles') && (_jsx(TuiIconButton, { size: "small", onClick: onClose, tooltip: t('hit.panel.details.exit'), children: _jsx(Clear, {}) })), _jsx(SocketBadge, { size: "small" }), analytic && (_jsx(TuiIconButton, { size: "small", tooltip: t('hit.panel.analytic.open'), disabled: !analytic || loading, route: `/analytics/${analytic.analytic_id}`, children: _jsx(QueryStats, {}) })), hit?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles, disabled: loading }), !!hit && !hit.howler.is_bundle && (_jsx(TuiIconButton, { tooltip: t('hit.panel.open'), href: `/hits/${selected}`, disabled: !hit || loading, size: "small", target: "_blank", children: _jsx(OpenInNew, {}) }))] }), _jsx(Box, { pr: 2, children: header }), !!hit &&
|
|
144
148
|
!hit.howler.is_bundle &&
|
|
145
149
|
(!loading ? (_jsxs(_Fragment, { children: [_jsx(HitOutline, { hit: hit, layout: HitLayout.DENSE }), _jsx(HitLabels, { hit: hit })] })) : (_jsx(Skeleton, { height: 124 }))), (hit?.howler?.links?.length > 0 ||
|
|
@@ -149,39 +153,39 @@ const InformationPane = ({ onClose }) => {
|
|
|
149
153
|
.slice(0, 3)
|
|
150
154
|
.map(l => _jsx(RelatedLink, { compact: true, ...l }, l.href)), dossiers.flatMap(_dossier => (_dossier.pivots ?? []).map((_pivot, index) => (
|
|
151
155
|
// eslint-disable-next-line react/no-array-index-key
|
|
152
|
-
_jsx(PivotLink, { pivot: _pivot, hit: hit, compact: true }, _dossier.dossier_id + index))))] })),
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
156
|
+
_jsx(PivotLink, { pivot: _pivot, hit: hit, compact: true }, _dossier.dossier_id + index))))] })), _jsxs(VSBoxHeader, { ml: -1, mr: -1, pb: 1, sx: { top: '0px' }, children: [_jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: {
|
|
157
|
+
display: 'flex',
|
|
158
|
+
flexDirection: 'row',
|
|
159
|
+
pr: 2,
|
|
160
|
+
alignItems: 'center',
|
|
161
|
+
position: 'relative',
|
|
162
|
+
'& > .MuiTabScrollButton-root': {
|
|
163
|
+
position: 'absolute',
|
|
164
|
+
top: 0,
|
|
165
|
+
bottom: 0,
|
|
166
|
+
zIndex: 5,
|
|
167
|
+
backgroundColor: theme.palette.background.paper,
|
|
168
|
+
'&:not(.Mui-disabled)': {
|
|
169
|
+
opacity: 1
|
|
170
|
+
},
|
|
171
|
+
'&:first-of-type': {
|
|
172
|
+
left: 0
|
|
173
|
+
},
|
|
174
|
+
'&:last-of-type': {
|
|
175
|
+
right: 0
|
|
176
|
+
}
|
|
172
177
|
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
'
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
, { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_lead.icon && _jsx(Icon, { icon: _lead.icon }), _jsx("span", { children: i18n.language === 'en' ? _lead.label.en : _lead.label.fr })] }), value: `external-lead:${dossierIndex}:${leadIndex}`, onClick: () => setTab(`external-lead:${dossierIndex}:${leadIndex}`) }, `external-lead:${dossierIndex}:${leadIndex}`)))), _jsx(FlexOne, {}), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.data'), children: _jsx(DataObject, {}) }), value: "hit_data", onClick: () => setTab('hit_data'), disabled: !hit?.howler?.data }), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.json'), children: _jsx(Code, {}) }), value: "hit_raw", onClick: () => setTab('hit_raw') }), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.worklog'), children: _jsx(History, {}) }), value: "hit_worklog", onClick: () => setTab('hit_worklog') }), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.related'), children: _jsx(LinkSharp, {}) }), value: "hit_related", onClick: () => setTab('hit_related') })] }) }), _jsx(ErrorBoundary, { children: _jsx(VSBoxContent, { mr: -1, ml: -1, height: "100%", children: _jsx(Stack, { height: "100%", flex: 1, children: tabContent }) }) })] }), !!hit && hit?.howler && (_jsxs(Box, { pr: 2, bgcolor: theme.palette.background.default, position: "relative", children: [_jsx(Divider, { orientation: "horizontal" }), _jsx(HitActions, { hit: hit })] }))] }));
|
|
178
|
+
}, variant: "scrollable", children: [_jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.comments'), children: _jsx(Badge, { sx: {
|
|
179
|
+
'& > .MuiBadge-badge': {
|
|
180
|
+
backgroundColor: theme.palette.divider,
|
|
181
|
+
zIndex: 1,
|
|
182
|
+
right: theme.spacing(-0.5)
|
|
183
|
+
},
|
|
184
|
+
'& > svg': { zIndex: 2 }
|
|
185
|
+
}, badgeContent: hit?.howler.comment?.length ?? 0, children: _jsx(Comment, {}) }) }), value: "hit_comments", onClick: () => setTab('hit_comments') }), hit?.howler?.is_bundle && (_jsx(Tab, { label: t('hit.viewer.aggregate'), value: "hit_aggregate", onClick: () => setTab('hit_aggregate') })), hasOverview && (_jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') })), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
|
|
186
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
187
|
+
, { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [lead.icon && _jsx(Icon, { icon: lead.icon }), _jsx("span", { children: i18n.language === 'en' ? lead.label.en : lead.label.fr })] }), value: 'lead:' + index, onClick: () => setTab('lead:' + index) }, 'lead:' + index))), dossiers.flatMap((_dossier, dossierIndex) => _dossier.leads?.map((_lead, leadIndex) => (_jsx(Tab
|
|
188
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
189
|
+
, { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_lead.icon && _jsx(Icon, { icon: _lead.icon }), _jsx("span", { children: i18n.language === 'en' ? _lead.label.en : _lead.label.fr })] }), value: `external-lead:${dossierIndex}:${leadIndex}`, onClick: () => setTab(`external-lead:${dossierIndex}:${leadIndex}`) }, `external-lead:${dossierIndex}:${leadIndex}`)))), _jsx(FlexOne, {}), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.data'), children: _jsx(DataObject, {}) }), value: "hit_data", onClick: () => setTab('hit_data'), disabled: !hit?.howler?.data }), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.json'), children: _jsx(Code, {}) }), value: "hit_raw", onClick: () => setTab('hit_raw') }), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.worklog'), children: _jsx(History, {}) }), value: "hit_worklog", onClick: () => setTab('hit_worklog') }), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.related'), children: _jsx(LinkSharp, {}) }), value: "hit_related", onClick: () => setTab('hit_related') })] }), ['hit_raw', 'hit_data'].includes(tab) && (_jsx(Phrase, { sx: { mt: 1, pr: 1 }, value: filter, onChange: setFilter, error: hasError, label: t('json.viewer.search.label'), placeholder: t('json.viewer.search.prompt'), endAdornment: _jsx(IconButton, { onClick: () => setFilter(''), children: _jsx(Clear, {}) }) }))] }), _jsx(ErrorBoundary, { children: _jsx(VSBoxContent, { mr: -1, ml: -1, height: "100%", children: _jsx(Stack, { height: "100%", flex: 1, children: tabContent }) }) })] }), !!hit && hit?.howler && (_jsxs(Box, { pr: 2, bgcolor: theme.palette.background.default, position: "relative", children: [_jsx(Divider, { orientation: "horizontal" }), _jsx(HitActions, { hit: hit })] }))] }));
|
|
186
190
|
};
|
|
187
191
|
export default InformationPane;
|
|
@@ -431,8 +431,13 @@
|
|
|
431
431
|
"route.advanced.query.yaml": "Sigma Rule",
|
|
432
432
|
"route.advanced.query.yaml.description": "Sigma is a generic and open signature format that allows you to describe relevant log events in a straightforward manner. The rule format is very flexible, easy to write and applicable to any type of log file.",
|
|
433
433
|
"route.advanced.result.title": "Howler Advanced Search",
|
|
434
|
-
"route.advanced.result.description": "
|
|
435
|
-
"route.advanced.type": "Rule Type",
|
|
434
|
+
"route.advanced.result.description": "Execute a query to show results here.",
|
|
435
|
+
"route.advanced.rule.type": "Rule Type",
|
|
436
|
+
"route.advanced.pivot.field": "Group By Field",
|
|
437
|
+
"route.advanced.query.type": "Query Type",
|
|
438
|
+
"route.advanced.query.type.default": "Default",
|
|
439
|
+
"route.advanced.query.type.facet": "Facet",
|
|
440
|
+
"route.advanced.query.type.groupby": "Group By",
|
|
436
441
|
"route.analytics": "Analytics",
|
|
437
442
|
"route.analytics.deleted": "Deleted Rule!",
|
|
438
443
|
"route.analytics.detections": "Detections:",
|
|
@@ -431,8 +431,13 @@
|
|
|
431
431
|
"route.advanced.query.yaml": "Règle Sigma",
|
|
432
432
|
"route.advanced.query.yaml.description": "Sigma est un format de signature générique et ouvert qui vous permet de décrire des événements de journal pertinents de manière directe. Le format de règle est très flexible, facile à écrire et applicable à tout type de fichier journal.",
|
|
433
433
|
"route.advanced.result.title": "Recherche avancée de howler",
|
|
434
|
-
"route.advanced.result.description": "
|
|
435
|
-
"route.advanced.type": "Type de règle",
|
|
434
|
+
"route.advanced.result.description": "Exécutez une requête pour afficher les résultats ici.",
|
|
435
|
+
"route.advanced.rule.type": "Type de règle",
|
|
436
|
+
"route.advanced.pivot.field": "Champ à regrouper par",
|
|
437
|
+
"route.advanced.query.type": "Type de requête",
|
|
438
|
+
"route.advanced.query.type.default": "Défaut",
|
|
439
|
+
"route.advanced.query.type.facet": "Facette",
|
|
440
|
+
"route.advanced.query.type.groupby": "Regrouper",
|
|
436
441
|
"route.analytics": "Analyses",
|
|
437
442
|
"route.analytics.deleted": "Règle supprimée!",
|
|
438
443
|
"route.analytics.detections": "Détections:",
|
package/package.json
CHANGED
package/utils/stringUtils.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export declare const safeFieldValueURI: (data: string | number | boolean) => str
|
|
|
5
5
|
export declare const sanitizeLuceneQuery: (query: string) => string;
|
|
6
6
|
export declare const safeStringPropertyCompare: (propertyPath: string) => (a: unknown, b: unknown) => any;
|
|
7
7
|
export declare const sanitizeMultilineLucene: (query: string) => string;
|
|
8
|
+
export declare const validateRegex: (regex: string) => boolean;
|
package/utils/stringUtils.js
CHANGED
|
@@ -36,3 +36,12 @@ export const safeStringPropertyCompare = (propertyPath) => {
|
|
|
36
36
|
export const sanitizeMultilineLucene = (query) => {
|
|
37
37
|
return query.replace(/#.+/g, '').replace(/\n{2,}/, '\n');
|
|
38
38
|
};
|
|
39
|
+
export const validateRegex = (regex) => {
|
|
40
|
+
try {
|
|
41
|
+
new RegExp(regex);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
};
|