@cccsaurora/howler-ui 3.1.0-dev.1712 → 3.1.0-dev.1716
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.
|
@@ -14,6 +14,7 @@ import { memo, useCallback, useContext, useEffect, useState } from 'react';
|
|
|
14
14
|
import { useTranslation } from 'react-i18next';
|
|
15
15
|
import { useContextSelector } from 'use-context-selector';
|
|
16
16
|
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
17
|
+
import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
17
18
|
import { isHit } from '@cccsaurora/howler-ui/utils/typeUtils';
|
|
18
19
|
import { getTimeRange, notNil } from '@cccsaurora/howler-ui/utils/utils';
|
|
19
20
|
import PluginChip from '../PluginChip';
|
|
@@ -30,7 +31,7 @@ const HitSummary = ({ response, onStart, onComplete }) => {
|
|
|
30
31
|
const error = useContextSelector(RecordSearchContext, ctx => ctx.error);
|
|
31
32
|
const getFilters = useContextSelector(RecordSearchContext, ctx => ctx.getFilters);
|
|
32
33
|
const query = useContextSelector(ParameterContext, ctx => ctx.query);
|
|
33
|
-
const
|
|
34
|
+
const addFilter = useContextSelector(ParameterContext, ctx => ctx.addFilter);
|
|
34
35
|
const views = useContextSelector(ParameterContext, ctx => ctx.views);
|
|
35
36
|
const [loading, setLoading] = useState(false);
|
|
36
37
|
const [customKeys, setCustomKeys] = useState([]);
|
|
@@ -122,9 +123,9 @@ const HitSummary = ({ response, onStart, onComplete }) => {
|
|
|
122
123
|
response?.items,
|
|
123
124
|
query
|
|
124
125
|
]);
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
}, [
|
|
126
|
+
const addAggregationFilter = useCallback((key, value) => {
|
|
127
|
+
addFilter(`${key}:${value}`);
|
|
128
|
+
}, [addFilter]);
|
|
128
129
|
useEffect(() => {
|
|
129
130
|
if ((!query && (views?.length ?? 0) < 1) || searching || error) {
|
|
130
131
|
return;
|
|
@@ -136,9 +137,9 @@ const HitSummary = ({ response, onStart, onComplete }) => {
|
|
|
136
137
|
.filter(key => !isEmpty(aggregateResults[key]))
|
|
137
138
|
.flatMap(key => [
|
|
138
139
|
_jsx(Fade, { in: true, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Typography, { variant: "body1", children: key }, key + '-title'), keyCounts[key]?.count < 0 ? (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", t('hit.summary.adhoc.custom'), ")"] })) : (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", keyCounts[key]?.count ?? '?', " ", t('references'), ")"] })), _jsx(Tooltip, { title: _jsxs(Stack, { children: [_jsx(Typography, { variant: "caption", children: t('hit.summary.aggregate.sources') }), keyCounts[key]?.sources.map(source => (_jsx(Typography, { variant: "caption", children: source }, source))) ?? '?'] }), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }) }, key + '-refs'),
|
|
139
|
-
_jsx(Fade, { in: true, children: hitFields.find(f => f.key === key)?.type !== 'date' ? (_jsx(Box, { sx: theme => ({ ml: `${theme.spacing(1)} !important`, alignSelf: 'start' }), children: _jsx(Grid, { container: true, sx: theme => ({ mr: 1, mt: theme.spacing(-1) }), spacing: 1, children: Object.keys(aggregateResults[key]).map(item => (_jsx(Grid, { size: 'auto', children: _jsx(PluginChip, { context: "summary", size: "small", variant: "filled", value: item, label: `${item} (${aggregateResults[key][item]})`, onClick: () =>
|
|
140
|
+
_jsx(Fade, { in: true, children: hitFields.find(f => f.key === key)?.type !== 'date' ? (_jsx(Box, { sx: theme => ({ ml: `${theme.spacing(1)} !important`, alignSelf: 'start' }), children: _jsx(Grid, { container: true, sx: theme => ({ mr: 1, mt: theme.spacing(-1) }), spacing: 1, children: Object.keys(aggregateResults[key]).map(item => (_jsx(Grid, { size: 'auto', children: _jsx(PluginChip, { context: "summary", size: "small", variant: "filled", value: item, label: `${item} (${aggregateResults[key][item]})`, onClick: () => addAggregationFilter(key, `"${sanitizeLuceneQuery(item)}"`), field: key }) }, item))) }, key + '-list') })) : (_jsx(Chip, { size: "small", sx: theme => ({ ml: `${theme.spacing(1)} !important`, alignSelf: 'start' }), label: getTimeRange(Object.keys(aggregateResults[key]))
|
|
140
141
|
.map(d => new Date(d).toLocaleString())
|
|
141
|
-
.join(' - '), onClick: () =>
|
|
142
|
+
.join(' - '), onClick: () => addAggregationFilter(key, `[${getTimeRange(Object.keys(aggregateResults[key])).join(' TO ')}]`) })) }, key + '-results')
|
|
142
143
|
])] })] }));
|
|
143
144
|
};
|
|
144
145
|
export default memo(HitSummary);
|
|
@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
5
5
|
const mockDispatchApi = vi.hoisted(() => vi.fn());
|
|
6
6
|
const mockShowErrorMessage = vi.hoisted(() => vi.fn());
|
|
7
7
|
const mockSetQuery = vi.hoisted(() => vi.fn());
|
|
8
|
+
const mockAddFilter = vi.hoisted(() => vi.fn());
|
|
8
9
|
const mockGetFilters = vi.hoisted(() => vi.fn().mockResolvedValue(['status:open']));
|
|
9
10
|
const mockGetMatchingTemplate = vi.hoisted(() => vi.fn());
|
|
10
11
|
const fieldContextToken = vi.hoisted(() => ({ name: 'field-context' }));
|
|
@@ -23,7 +24,7 @@ vi.mock('@mui/material', () => ({
|
|
|
23
24
|
Autocomplete: ({ onChange, renderInput }) => (_jsxs("div", { children: [renderInput({}), _jsx("button", { onClick: () => onChange?.(null, ['custom.field']), children: "add-custom-field" })] })),
|
|
24
25
|
Box: ({ children }) => _jsx("div", { children: children }),
|
|
25
26
|
Button: ({ children, onClick, disabled }) => (_jsx("button", { onClick: onClick, disabled: disabled, children: children })),
|
|
26
|
-
Chip: ({ label, onClick }) => _jsx("button", { onClick: onClick, children: label }),
|
|
27
|
+
Chip: ({ label, onClick }) => (_jsx("button", { "aria-label": "date-aggregation-chip", onClick: onClick, children: label })),
|
|
27
28
|
CircularProgress: () => _jsx("div", { children: "loading" }),
|
|
28
29
|
Divider: () => _jsx("div", { children: "divider" }),
|
|
29
30
|
Fade: ({ children }) => _jsx("div", { children: children }),
|
|
@@ -124,6 +125,7 @@ describe('HitSummary', () => {
|
|
|
124
125
|
parameterContextValue = {
|
|
125
126
|
query: 'status:open',
|
|
126
127
|
setQuery: mockSetQuery,
|
|
128
|
+
addFilter: mockAddFilter,
|
|
127
129
|
views: []
|
|
128
130
|
};
|
|
129
131
|
recordSearchContextValue = {
|
|
@@ -134,16 +136,17 @@ describe('HitSummary', () => {
|
|
|
134
136
|
mockDispatchApi.mockReset();
|
|
135
137
|
mockShowErrorMessage.mockReset();
|
|
136
138
|
mockSetQuery.mockReset();
|
|
139
|
+
mockAddFilter.mockReset();
|
|
137
140
|
mockGetFilters.mockClear();
|
|
138
141
|
mockGetMatchingTemplate.mockReset();
|
|
139
142
|
});
|
|
140
|
-
it('aggregates template keys
|
|
143
|
+
it('aggregates template keys and adds aggregation filters without changing the search query', async () => {
|
|
141
144
|
mockGetMatchingTemplate
|
|
142
145
|
.mockResolvedValueOnce({ analytic: 'Analytic A', detection: 'Rule A', keys: ['severity', 'howler.id'] })
|
|
143
146
|
.mockResolvedValueOnce({ analytic: 'Analytic A', detection: 'Rule B', keys: ['event.created'] });
|
|
144
147
|
mockDispatchApi
|
|
145
148
|
.mockResolvedValueOnce({
|
|
146
|
-
severity: { high: 2, low: 1 },
|
|
149
|
+
severity: { high: 2, low: 1, 'critical:high': 3 },
|
|
147
150
|
'event.created': { '2026-01-01T00:00:00Z': 1, '2026-01-02T00:00:00Z': 1 }
|
|
148
151
|
})
|
|
149
152
|
.mockResolvedValueOnce({
|
|
@@ -166,14 +169,25 @@ describe('HitSummary', () => {
|
|
|
166
169
|
expect(screen.getByText('hit-graph')).toBeInTheDocument();
|
|
167
170
|
expect(screen.getByText('severity')).toBeInTheDocument();
|
|
168
171
|
fireEvent.click(screen.getByText('high (2)'));
|
|
169
|
-
expect(
|
|
172
|
+
expect(mockAddFilter).toHaveBeenCalledWith('severity:"high"');
|
|
173
|
+
fireEvent.click(screen.getByText('critical:high (3)'));
|
|
174
|
+
expect(mockAddFilter).toHaveBeenCalledWith('severity:"critical\\:high"');
|
|
175
|
+
expect(mockSetQuery).not.toHaveBeenCalled();
|
|
176
|
+
fireEvent.click(screen.getByLabelText('date-aggregation-chip'));
|
|
177
|
+
expect(mockAddFilter).toHaveBeenCalledWith('event.created:[2026-01-01T00:00:00Z TO 2026-01-02T00:00:00Z]');
|
|
178
|
+
expect(mockSetQuery).not.toHaveBeenCalled();
|
|
179
|
+
mockGetFilters.mockResolvedValue([
|
|
180
|
+
'status:open',
|
|
181
|
+
'severity:"high"',
|
|
182
|
+
'event.created:[2026-01-01T00:00:00Z TO 2026-01-02T00:00:00Z]'
|
|
183
|
+
]);
|
|
170
184
|
fireEvent.click(screen.getByText('add-custom-field'));
|
|
171
185
|
fireEvent.click(screen.getByText('button.aggregate'));
|
|
172
186
|
await waitFor(() => expect(mockDispatchApi).toHaveBeenLastCalledWith({
|
|
173
187
|
fields: ['custom.field'],
|
|
174
188
|
query: 'status:open',
|
|
175
189
|
rows: 25,
|
|
176
|
-
filters: ['status:open']
|
|
190
|
+
filters: ['status:open', 'severity:"high"', 'event.created:[2026-01-01T00:00:00Z TO 2026-01-02T00:00:00Z]']
|
|
177
191
|
}, { throwError: false, logError: true, showError: false }));
|
|
178
192
|
expect(screen.getByText('custom.field')).toBeInTheDocument();
|
|
179
193
|
});
|