@cccsaurora/howler-ui 2.19.0-cases.1065 → 2.19.0-cases.1095
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/search/index.d.ts +2 -1
- package/api/v2/search/index.js +2 -2
- package/components/app/providers/ParameterProvider.d.ts +1 -1
- package/components/app/providers/ParameterProvider.test.js +25 -25
- package/components/app/providers/RecordProvider.d.ts +6 -6
- package/components/app/providers/RecordProvider.js +5 -1
- package/components/app/providers/RecordSearchProvider.d.ts +2 -2
- package/components/elements/ObjectDetails.d.ts +2 -2
- package/components/elements/PluginTypography.d.ts +2 -2
- package/components/elements/event/EventCard.d.ts +6 -0
- package/components/elements/{observable/ObservableCard.js → event/EventCard.js} +7 -7
- package/components/elements/{observable/ObservablePreview.d.ts → event/EventPreview.d.ts} +2 -2
- package/components/elements/event/EventPreview.js +12 -0
- package/components/elements/hit/HitCard.d.ts +2 -0
- package/components/elements/hit/HitCard.js +2 -2
- package/components/elements/hit/HitSummary.d.ts +2 -2
- package/components/elements/hit/related/RelatedRecords.js +4 -4
- package/components/elements/record/RecordComments.d.ts +2 -2
- package/components/elements/record/RecordRelated.d.ts +2 -2
- package/components/elements/record/RecordRelated.js +4 -4
- package/components/elements/record/RecordWorklog.d.ts +2 -2
- package/components/hooks/useRelatedRecords.d.ts +3 -3
- package/components/hooks/useRelatedRecords.js +2 -2
- package/components/routes/cases/detail/CaseAssets.d.ts +2 -2
- package/components/routes/cases/detail/CaseAssets.js +2 -2
- package/components/routes/cases/detail/CaseAssets.test.js +4 -4
- package/components/routes/cases/detail/CaseDashboard.js +2 -2
- package/components/routes/cases/detail/CaseRules.test.js +2 -2
- package/components/routes/cases/detail/CaseTimeline.js +5 -5
- package/components/routes/cases/detail/CaseTimeline.test.js +7 -7
- package/components/routes/cases/detail/CreateRuleDialog.js +4 -4
- package/components/routes/cases/detail/CreateRuleDialog.test.js +5 -5
- package/components/routes/cases/detail/ItemPage.js +1 -1
- package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +2 -2
- package/components/routes/cases/detail/assets/Asset.d.ts +1 -1
- package/components/routes/cases/detail/assets/Asset.test.js +2 -2
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +1 -1
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +3 -3
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +8 -8
- package/components/routes/cases/detail/sidebar/FolderEntry.js +1 -1
- package/components/routes/cases/modals/AddToCaseModal.d.ts +2 -2
- package/components/routes/cases/modals/AddToCaseModal.test.js +11 -11
- package/components/routes/cases/modals/CaseRecordRow.js +1 -1
- package/components/routes/cases/modals/CreateCaseModal.d.ts +2 -2
- package/components/routes/cases/modals/CreateCaseModal.test.js +14 -14
- package/components/routes/cases/modals/hooks.d.ts +3 -3
- package/components/routes/cases/modals/hooks.js +1 -1
- package/components/routes/cases/modals/types.d.ts +4 -1
- package/components/routes/events/EventViewer.d.ts +7 -0
- package/components/routes/events/EventViewer.js +27 -0
- package/components/routes/hits/search/SearchPane.js +3 -3
- package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -2
- package/components/routes/hits/search/grid/RecordRow.d.ts +3 -3
- package/components/routes/hits/search/grid/RecordRow.js +2 -2
- package/components/routes/hits/search/shared/IndexPicker.js +1 -1
- package/components/routes/home/ViewCard.js +4 -4
- package/components/routes/views/ViewComposer.js +2 -2
- package/locales/en/translation.json +4 -4
- package/locales/fr/translation.json +4 -4
- package/models/entities/generated/ECSEvent.d.ts +31 -0
- package/models/entities/generated/Event.d.ts +80 -26
- package/models/entities/generated/{ObservableCloud.d.ts → EventCloud.d.ts} +1 -1
- package/models/entities/generated/{ObservableDestination.d.ts → EventDestination.d.ts} +1 -1
- package/models/entities/generated/{ObservableEmail.d.ts → EventEmail.d.ts} +1 -1
- package/models/entities/generated/{ObservableFile.d.ts → EventFile.d.ts} +1 -1
- package/models/entities/generated/{ObservableHowler.d.ts → EventHowler.d.ts} +1 -1
- package/models/entities/generated/{ObservableHttp.d.ts → EventHttp.d.ts} +1 -1
- package/models/entities/generated/{ObservableObserver.d.ts → EventObserver.d.ts} +1 -1
- package/models/entities/generated/{ObservableOrganization.d.ts → EventOrganization.d.ts} +1 -1
- package/models/entities/generated/{ObservableProcess.d.ts → EventProcess.d.ts} +1 -1
- package/models/entities/generated/{ObservableSource.d.ts → EventSource.d.ts} +1 -1
- package/models/entities/generated/{ObservableThreat.d.ts → EventThreat.d.ts} +1 -1
- package/models/entities/generated/{ObservableTls.d.ts → EventTls.d.ts} +1 -1
- package/models/entities/generated/Hit.d.ts +2 -2
- package/models/entities/generated/Rule.d.ts +1 -1
- package/package.json +3 -3
- package/plugins/clue/utils.d.ts +2 -2
- package/tests/server-handlers.js +1 -1
- package/tests/utils.d.ts +2 -2
- package/tests/utils.js +3 -3
- package/utils/hitFunctions.d.ts +2 -2
- package/utils/typeUtils.d.ts +2 -2
- package/utils/typeUtils.js +2 -2
- package/components/elements/observable/ObservableCard.d.ts +0 -6
- package/components/elements/observable/ObservablePreview.js +0 -12
- package/components/routes/observables/ObservableViewer.d.ts +0 -7
- package/components/routes/observables/ObservableViewer.js +0 -27
- package/models/entities/generated/Observable.d.ts +0 -85
|
@@ -116,11 +116,11 @@ describe('CaseRules', () => {
|
|
|
116
116
|
it('renders index chips for rule indexes', () => {
|
|
117
117
|
mockCase.current = createMockCase({
|
|
118
118
|
case_id: 'case-001',
|
|
119
|
-
rules: [makeRule({ indexes: ['hit', '
|
|
119
|
+
rules: [makeRule({ indexes: ['hit', 'event'] })]
|
|
120
120
|
});
|
|
121
121
|
render(_jsx(CaseRules, {}));
|
|
122
122
|
expect(screen.getByText('hit')).toBeInTheDocument();
|
|
123
|
-
expect(screen.getByText('
|
|
123
|
+
expect(screen.getByText('event')).toBeInTheDocument();
|
|
124
124
|
});
|
|
125
125
|
it('defaults to hit chip when indexes is undefined', () => {
|
|
126
126
|
mockCase.current = createMockCase({
|
|
@@ -5,9 +5,9 @@ import { Autocomplete, Box, Chip, Divider, Skeleton, Stack, TextField, Tooltip,
|
|
|
5
5
|
import api from '@cccsaurora/howler-ui/api';
|
|
6
6
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
7
7
|
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
8
|
+
import EventCard from '@cccsaurora/howler-ui/components/elements/event/EventCard';
|
|
8
9
|
import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
|
|
9
10
|
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
10
|
-
import ObservableCard from '@cccsaurora/howler-ui/components/elements/observable/ObservableCard';
|
|
11
11
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
12
12
|
import dayjs from 'dayjs';
|
|
13
13
|
import { capitalize } from 'lodash-es';
|
|
@@ -48,7 +48,7 @@ const CaseTimeline = ({ case: providedCase, caseId }) => {
|
|
|
48
48
|
const [selectedMitres, setSelectedMitres] = useState([]);
|
|
49
49
|
const [selectedEscalations, setSelectedEscalations] = useState(['evidence']);
|
|
50
50
|
const ids = useMemo(() => (_case?.items ?? [])
|
|
51
|
-
.filter(item => ['hit', '
|
|
51
|
+
.filter(item => ['hit', 'event'].includes(item.type))
|
|
52
52
|
.map(item => item.value)
|
|
53
53
|
.filter(Boolean), [_case]);
|
|
54
54
|
const getPath = (value) => _case.items.find(item => item.value === value)?.path;
|
|
@@ -56,7 +56,7 @@ const CaseTimeline = ({ case: providedCase, caseId }) => {
|
|
|
56
56
|
if (ids.length < 1) {
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
|
-
dispatchApi(api.v2.search.facet.post(['hit', '
|
|
59
|
+
dispatchApi(api.v2.search.facet.post(['hit', 'event'], {
|
|
60
60
|
fields: ['threat.tactic.id', 'threat.technique.id', 'howler.escalation'],
|
|
61
61
|
filters: [`howler.id:(${ids.join(' OR ')})`]
|
|
62
62
|
}), { throwError: false }).then(result => {
|
|
@@ -84,7 +84,7 @@ const CaseTimeline = ({ case: providedCase, caseId }) => {
|
|
|
84
84
|
}
|
|
85
85
|
setLoading(true);
|
|
86
86
|
const filters = buildFilters(selectedMitres, selectedEscalations);
|
|
87
|
-
dispatchApi(api.v2.search.post(['hit', '
|
|
87
|
+
dispatchApi(api.v2.search.post(['hit', 'event'], {
|
|
88
88
|
query: `howler.id:(${ids.join(' OR ')})`,
|
|
89
89
|
sort: 'event.created asc',
|
|
90
90
|
rows: ids.length,
|
|
@@ -101,6 +101,6 @@ const CaseTimeline = ({ case: providedCase, caseId }) => {
|
|
|
101
101
|
if (!_case) {
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
104
|
-
return (_jsxs(Stack, { spacing: 0, sx: { height: '100%' }, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", sx: { p: 1, gap: 1 }, children: [_jsx(Tooltip, { title: t('page.cases.timeline.filter.label'), children: _jsx(FilterList, { fontSize: "small", color: "action" }) }), _jsx(Autocomplete, { multiple: true, size: "small", options: mitreOptions, value: selectedMitres, onChange: (_e, values) => setSelectedMitres(values), getOptionLabel: opt => `${opt.id} - ${opt.name}`, isOptionEqualToValue: (opt, val) => opt.id === val.id, groupBy: opt => capitalize(opt.kind), renderTags: (value, getTagProps) => value.map((opt, index) => (_createElement(Chip, { ...getTagProps({ index }), key: opt.id, size: "small", label: opt.id, color: "primary" }))), renderInput: params => (_jsx(TextField, { ...params, label: t('page.cases.timeline.filter.mitre'), sx: { minWidth: 260 } })), noOptionsText: t('page.cases.timeline.filter.mitre.empty') }), _jsx(Autocomplete, { multiple: true, size: "small", options: escalationOptions, value: selectedEscalations, onChange: (_e, value) => setSelectedEscalations(value), getOptionLabel: opt => t(`howler.escalation.${opt}`, opt), renderTags: (value, getTagProps) => value.map((opt, index) => (_createElement(Chip, { ...getTagProps({ index }), key: opt, size: "small", label: opt, color: ESCALATION_COLORS[opt] }))), renderInput: params => (_jsx(TextField, { ...params, label: t('page.cases.timeline.filter.escalation'), sx: { minWidth: 220 } })), noOptionsText: t('page.cases.timeline.filter.escalation.empty') })] }), _jsx(Divider, {}), loading ? (_jsx(Stack, { spacing: 2, sx: { px: 2, py: 1 }, children: [0, 1, 2].map(i => (_jsxs(Stack, { direction: "row", width: "100%", spacing: 1, children: [_jsx(Skeleton, { variant: "text", width: 120, height: 24 }), _jsx(Skeleton, { variant: "rounded", height: 120, sx: { flex: 1 } })] }, i))) })) : displayedEntries.length === 0 ? (_jsx(Box, { sx: { pt: 4, textAlign: 'center' }, children: _jsx(Typography, { color: "textSecondary", children: t('page.cases.timeline.empty') }) })) : (_jsx(Stack, { component: "ol", spacing: 0, sx: { px: 2, py: 1, listStyle: 'none', m: 0, overflow: 'auto' }, children: displayedEntries.map(entry => (_jsxs(Stack, { component: "li", spacing: 1, sx: { pb: 1 }, children: [_jsxs(Stack, { direction: "row", spacing: 2, alignItems: "flex-start", children: [_jsxs(Stack, { spacing: 0.5, alignItems: "end", children: [_jsx(Typography, { variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: dayjs(entry.event?.created ?? entry.timestamp).format('YYYY-MM-DD HH:mm:ss') }), entry.threat?.technique?.id && (_jsx(Tooltip, { title: `${entry.threat.technique.id}: ${config.lookups?.techniques?.[entry.threat.technique.id].name}`, children: _jsx(Typography, { component: config.lookups?.techniques?.[entry.threat.technique.id]?.url ? 'a' : undefined, href: config.lookups?.techniques?.[entry.threat.technique.id]?.url, variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: entry.threat.technique.id }) })), entry.threat?.tactic?.id && (_jsx(Tooltip, { title: `${entry.threat.tactic.id}: ${config.lookups?.tactics?.[entry.threat.tactic.id]?.name ?? t('unknown')}`, children: _jsx(Typography, { component: config.lookups?.tactics?.[entry.threat.tactic.id]?.url ? 'a' : undefined, href: config.lookups?.tactics?.[entry.threat.tactic.id]?.url, variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: entry.threat.tactic.id }) }))] }), _jsx(Box, { component: Link, to: `/cases/${_case.case_id}/${getPath(entry.howler.id)}`, sx: { flex: 1, minWidth: 0, textDecoration: 'none' }, children: isHit(entry) ? (_jsx(HitCard, { id: entry.howler.id, layout: HitLayout.DENSE, readOnly: true })) : (_jsx(
|
|
104
|
+
return (_jsxs(Stack, { spacing: 0, sx: { height: '100%' }, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", sx: { p: 1, gap: 1 }, children: [_jsx(Tooltip, { title: t('page.cases.timeline.filter.label'), children: _jsx(FilterList, { fontSize: "small", color: "action" }) }), _jsx(Autocomplete, { multiple: true, size: "small", options: mitreOptions, value: selectedMitres, onChange: (_e, values) => setSelectedMitres(values), getOptionLabel: opt => `${opt.id} - ${opt.name}`, isOptionEqualToValue: (opt, val) => opt.id === val.id, groupBy: opt => capitalize(opt.kind), renderTags: (value, getTagProps) => value.map((opt, index) => (_createElement(Chip, { ...getTagProps({ index }), key: opt.id, size: "small", label: opt.id, color: "primary" }))), renderInput: params => (_jsx(TextField, { ...params, label: t('page.cases.timeline.filter.mitre'), sx: { minWidth: 260 } })), noOptionsText: t('page.cases.timeline.filter.mitre.empty') }), _jsx(Autocomplete, { multiple: true, size: "small", options: escalationOptions, value: selectedEscalations, onChange: (_e, value) => setSelectedEscalations(value), getOptionLabel: opt => t(`howler.escalation.${opt}`, opt), renderTags: (value, getTagProps) => value.map((opt, index) => (_createElement(Chip, { ...getTagProps({ index }), key: opt, size: "small", label: opt, color: ESCALATION_COLORS[opt] }))), renderInput: params => (_jsx(TextField, { ...params, label: t('page.cases.timeline.filter.escalation'), sx: { minWidth: 220 } })), noOptionsText: t('page.cases.timeline.filter.escalation.empty') })] }), _jsx(Divider, {}), loading ? (_jsx(Stack, { spacing: 2, sx: { px: 2, py: 1 }, children: [0, 1, 2].map(i => (_jsxs(Stack, { direction: "row", width: "100%", spacing: 1, children: [_jsx(Skeleton, { variant: "text", width: 120, height: 24 }), _jsx(Skeleton, { variant: "rounded", height: 120, sx: { flex: 1 } })] }, i))) })) : displayedEntries.length === 0 ? (_jsx(Box, { sx: { pt: 4, textAlign: 'center' }, children: _jsx(Typography, { color: "textSecondary", children: t('page.cases.timeline.empty') }) })) : (_jsx(Stack, { component: "ol", spacing: 0, sx: { px: 2, py: 1, listStyle: 'none', m: 0, overflow: 'auto' }, children: displayedEntries.map(entry => (_jsxs(Stack, { component: "li", spacing: 1, sx: { pb: 1 }, children: [_jsxs(Stack, { direction: "row", spacing: 2, alignItems: "flex-start", children: [_jsxs(Stack, { spacing: 0.5, alignItems: "end", children: [_jsx(Typography, { variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: dayjs(entry.event?.created ?? entry.timestamp).format('YYYY-MM-DD HH:mm:ss') }), entry.threat?.technique?.id && (_jsx(Tooltip, { title: `${entry.threat.technique.id}: ${config.lookups?.techniques?.[entry.threat.technique.id].name}`, children: _jsx(Typography, { component: config.lookups?.techniques?.[entry.threat.technique.id]?.url ? 'a' : undefined, href: config.lookups?.techniques?.[entry.threat.technique.id]?.url, variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: entry.threat.technique.id }) })), entry.threat?.tactic?.id && (_jsx(Tooltip, { title: `${entry.threat.tactic.id}: ${config.lookups?.tactics?.[entry.threat.tactic.id]?.name ?? t('unknown')}`, children: _jsx(Typography, { component: config.lookups?.tactics?.[entry.threat.tactic.id]?.url ? 'a' : undefined, href: config.lookups?.tactics?.[entry.threat.tactic.id]?.url, variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: entry.threat.tactic.id }) }))] }), _jsx(Box, { component: Link, to: `/cases/${_case.case_id}/${getPath(entry.howler.id)}`, sx: { flex: 1, minWidth: 0, textDecoration: 'none' }, children: isHit(entry) ? (_jsx(HitCard, { id: entry.howler.id, hit: entry, layout: HitLayout.DENSE, readOnly: true })) : (_jsx(EventCard, { id: entry.howler.id, event: entry })) })] }), _jsx(Divider, { flexItem: true })] }, entry.howler.id))) }))] }));
|
|
105
105
|
};
|
|
106
106
|
export default memo(CaseTimeline);
|
|
@@ -7,7 +7,7 @@ import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers
|
|
|
7
7
|
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
8
8
|
import { createElement } from 'react';
|
|
9
9
|
import { MemoryRouter } from 'react-router-dom';
|
|
10
|
-
import {
|
|
10
|
+
import { createMockEvent, createMockHit } from '@cccsaurora/howler-ui/tests/utils';
|
|
11
11
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
12
|
import { buildFilters } from './CaseTimeline';
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
@@ -70,8 +70,8 @@ vi.mock('react-router-dom', async () => {
|
|
|
70
70
|
vi.mock('components/elements/hit/HitCard', () => ({
|
|
71
71
|
default: ({ id }) => _jsx("div", { children: `HitCard:${id}` })
|
|
72
72
|
}));
|
|
73
|
-
vi.mock('components/elements/
|
|
74
|
-
default: ({ id }) => _jsx("div", { children: `
|
|
73
|
+
vi.mock('components/elements/event/EventCard', () => ({
|
|
74
|
+
default: ({ id }) => _jsx("div", { children: `EventCard:${id}` })
|
|
75
75
|
}));
|
|
76
76
|
// Return the request object rather than a Promise so dispatchApi's first
|
|
77
77
|
// argument is JSON-serialisable (JSON.stringify(Promise) === '{}').
|
|
@@ -98,7 +98,7 @@ const mockCase = {
|
|
|
98
98
|
case_id: 'case-001',
|
|
99
99
|
items: [
|
|
100
100
|
{ type: 'hit', value: 'hit-1', path: 'hits/hit-1' },
|
|
101
|
-
{ type: '
|
|
101
|
+
{ type: 'event', value: 'obs-1', path: 'events/obs-1' }
|
|
102
102
|
]
|
|
103
103
|
};
|
|
104
104
|
const Wrapper = ({ children }) => (_jsx(ApiConfigContext.Provider, { value: { config: mockConfig, setConfig: vi.fn() }, children: _jsx(RecordContext.Provider, { value: { records: {}, loadRecords: mockLoadRecords }, children: _jsx(MemoryRouter, { initialEntries: ['/cases/case-001/timeline'], children: children }) }) }));
|
|
@@ -120,7 +120,7 @@ const mockFacetResponse = {
|
|
|
120
120
|
};
|
|
121
121
|
const mockSearchResponse = (items = [
|
|
122
122
|
createMockHit({ howler: { id: 'hit-1' }, event: { created: '2024-01-01T00:00:00Z' } }),
|
|
123
|
-
|
|
123
|
+
createMockEvent({ howler: { id: 'obs-1' } })
|
|
124
124
|
]) => ({ items, total: items.length, rows: items.length, offset: 0 });
|
|
125
125
|
describe('CaseTimeline component', () => {
|
|
126
126
|
beforeEach(() => {
|
|
@@ -137,11 +137,11 @@ describe('CaseTimeline component', () => {
|
|
|
137
137
|
render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
|
|
138
138
|
await screen.findByText('page.cases.timeline.empty');
|
|
139
139
|
});
|
|
140
|
-
it('renders HitCard for hit entries and
|
|
140
|
+
it('renders HitCard for hit entries and EventCard for event entries', async () => {
|
|
141
141
|
mockDispatchApi.mockResolvedValueOnce(mockFacetResponse).mockResolvedValueOnce(mockSearchResponse());
|
|
142
142
|
render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
|
|
143
143
|
expect(await screen.findByText('HitCard:hit-1')).toBeTruthy();
|
|
144
|
-
expect(screen.getByText('
|
|
144
|
+
expect(screen.getByText('EventCard:obs-1')).toBeTruthy();
|
|
145
145
|
});
|
|
146
146
|
it('renders the formatted event.created timestamp for hits', async () => {
|
|
147
147
|
mockDispatchApi
|
|
@@ -141,14 +141,14 @@ const CreateRuleDialog = ({ open, onClose, onSubmit }) => {
|
|
|
141
141
|
}
|
|
142
142
|
return prev.filter(i => i !== 'hit');
|
|
143
143
|
});
|
|
144
|
-
} }), label: t('hit.search.index.hit') }), _jsx(FormControlLabel, { control: _jsx(Checkbox, { id: "rule-index-
|
|
144
|
+
} }), label: t('hit.search.index.hit') }), _jsx(FormControlLabel, { control: _jsx(Checkbox, { id: "rule-index-event", checked: indexes.includes('event'), onChange: (_e, checked) => {
|
|
145
145
|
setIndexes(prev => {
|
|
146
146
|
if (checked) {
|
|
147
|
-
return prev.includes('
|
|
147
|
+
return prev.includes('event') ? prev : [...prev, 'event'];
|
|
148
148
|
}
|
|
149
|
-
return prev.filter(i => i !== '
|
|
149
|
+
return prev.filter(i => i !== 'event');
|
|
150
150
|
});
|
|
151
|
-
} }), label: t('hit.search.index.
|
|
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
152
|
textField: {
|
|
153
153
|
id: 'rule-timeframe-input',
|
|
154
154
|
fullWidth: true,
|
|
@@ -61,7 +61,7 @@ describe('CreateRuleDialog', () => {
|
|
|
61
61
|
expect(screen.getByTestId('rule-timeframe-input')).toBeInTheDocument();
|
|
62
62
|
expect(screen.getByTestId('rule-no-expiry-checkbox')).toBeInTheDocument();
|
|
63
63
|
expect(screen.getByTestId('rule-index-hit')).toBeInTheDocument();
|
|
64
|
-
expect(screen.getByTestId('rule-index-
|
|
64
|
+
expect(screen.getByTestId('rule-index-event')).toBeInTheDocument();
|
|
65
65
|
});
|
|
66
66
|
it('disables submit when query is empty', () => {
|
|
67
67
|
render(_jsx(CreateRuleDialog, { ...defaultProps }));
|
|
@@ -205,9 +205,9 @@ describe('CreateRuleDialog', () => {
|
|
|
205
205
|
it('defaults to hit index checked', () => {
|
|
206
206
|
render(_jsx(CreateRuleDialog, { ...defaultProps }));
|
|
207
207
|
const hitCheckbox = screen.getByTestId('rule-index-hit');
|
|
208
|
-
const
|
|
208
|
+
const eventCheckbox = screen.getByTestId('rule-index-event');
|
|
209
209
|
expect(hitCheckbox).toBeChecked();
|
|
210
|
-
expect(
|
|
210
|
+
expect(eventCheckbox).not.toBeChecked();
|
|
211
211
|
});
|
|
212
212
|
it('includes both indexes when both are selected', async () => {
|
|
213
213
|
const user = userEvent.setup();
|
|
@@ -216,7 +216,7 @@ describe('CreateRuleDialog', () => {
|
|
|
216
216
|
api.search.hit.post.mockReturnValue('search-request');
|
|
217
217
|
render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
|
|
218
218
|
await act(async () => {
|
|
219
|
-
await user.click(screen.getByTestId('rule-index-
|
|
219
|
+
await user.click(screen.getByTestId('rule-index-event'));
|
|
220
220
|
});
|
|
221
221
|
await act(async () => {
|
|
222
222
|
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
@@ -232,7 +232,7 @@ describe('CreateRuleDialog', () => {
|
|
|
232
232
|
});
|
|
233
233
|
await waitFor(() => {
|
|
234
234
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
235
|
-
indexes: expect.arrayContaining(['hit', '
|
|
235
|
+
indexes: expect.arrayContaining(['hit', 'event'])
|
|
236
236
|
}));
|
|
237
237
|
});
|
|
238
238
|
});
|
|
@@ -84,7 +84,7 @@ const ItemPage = ({ case: providedCase }) => {
|
|
|
84
84
|
if (!item) {
|
|
85
85
|
return _jsx(NotFoundPage, {});
|
|
86
86
|
}
|
|
87
|
-
if (item.type === 'hit' || item.type === '
|
|
87
|
+
if (item.type === 'hit' || item.type === 'event') {
|
|
88
88
|
return _jsx(InformationPane, { selected: item.value });
|
|
89
89
|
}
|
|
90
90
|
if (item.type === 'case') {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import type { Event } from '@cccsaurora/howler-ui/models/entities/generated/Event';
|
|
1
2
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
2
|
-
import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
|
|
3
3
|
declare const _default: import("react").NamedExoticComponent<{
|
|
4
4
|
icon?: string;
|
|
5
5
|
iconColor?: string;
|
|
6
6
|
field?: string;
|
|
7
|
-
records?: Partial<Hit |
|
|
7
|
+
records?: Partial<Hit | Event>[];
|
|
8
8
|
title?: string;
|
|
9
9
|
subtitle?: string;
|
|
10
10
|
}>;
|
|
@@ -4,7 +4,7 @@ export type AssetType = 'hash' | 'hosts' | 'ip' | 'user' | 'ids' | 'id' | 'uri'
|
|
|
4
4
|
export interface AssetEntry {
|
|
5
5
|
type: AssetType;
|
|
6
6
|
value: string;
|
|
7
|
-
/** IDs of the hits/
|
|
7
|
+
/** IDs of the hits/events this asset was seen in */
|
|
8
8
|
seenIn: string[];
|
|
9
9
|
}
|
|
10
10
|
declare const Asset: FC<{
|
|
@@ -49,13 +49,13 @@ describe('Asset', () => {
|
|
|
49
49
|
const _case = createMockCase({
|
|
50
50
|
items: [
|
|
51
51
|
{ path: 'alerts/my-analytic (hit-001)', type: 'hit', value: 'hit-001' },
|
|
52
|
-
{ path: '
|
|
52
|
+
{ path: 'events/obs-002', type: 'event', value: 'obs-002' },
|
|
53
53
|
{ path: 'alerts/other-analytic (hit-003)', type: 'hit', value: 'hit-003' }
|
|
54
54
|
]
|
|
55
55
|
});
|
|
56
56
|
render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ seenIn: ['hit-001', 'obs-002', 'hit-003'] }), case: _case }) }));
|
|
57
57
|
expect(screen.getByText('alerts/my-analytic (hit-001)')).toBeTruthy();
|
|
58
|
-
expect(screen.getByText('
|
|
58
|
+
expect(screen.getByText('events/obs-002')).toBeTruthy();
|
|
59
59
|
expect(screen.getByText('alerts/other-analytic (hit-003)')).toBeTruthy();
|
|
60
60
|
});
|
|
61
61
|
it('links each chip to /cases/:case_id/:path', () => {
|
|
@@ -10,7 +10,7 @@ export declare const collectAllLeaves: (tree: Tree) => Item[];
|
|
|
10
10
|
* Returns the URL to open for a given leaf item, or null if no URL applies.
|
|
11
11
|
* - reference: the item's value (an external URL)
|
|
12
12
|
* - hit: /hits/<id>
|
|
13
|
-
* -
|
|
13
|
+
* - event: /events/<id>
|
|
14
14
|
* - case: /cases/<id>
|
|
15
15
|
* - table / lead: null (no dedicated detail page)
|
|
16
16
|
*/
|
|
@@ -21,7 +21,7 @@ export const collectAllLeaves = (tree) => {
|
|
|
21
21
|
* Returns the URL to open for a given leaf item, or null if no URL applies.
|
|
22
22
|
* - reference: the item's value (an external URL)
|
|
23
23
|
* - hit: /hits/<id>
|
|
24
|
-
* -
|
|
24
|
+
* - event: /events/<id>
|
|
25
25
|
* - case: /cases/<id>
|
|
26
26
|
* - table / lead: null (no dedicated detail page)
|
|
27
27
|
*/
|
|
@@ -33,8 +33,8 @@ export const getOpenUrl = (leaf) => {
|
|
|
33
33
|
if (type === 'hit') {
|
|
34
34
|
return leaf.value ? `/hits/${leaf.value}` : null;
|
|
35
35
|
}
|
|
36
|
-
if (type === '
|
|
37
|
-
return leaf.value ? `/
|
|
36
|
+
if (type === 'event') {
|
|
37
|
+
return leaf.value ? `/events/${leaf.value}` : null;
|
|
38
38
|
}
|
|
39
39
|
if (type === 'case') {
|
|
40
40
|
return leaf.value ? `/cases/${leaf.value}` : null;
|
|
@@ -54,7 +54,7 @@ import CaseFolderContextMenu, { collectAllLeaves, getOpenUrl } from './CaseFolde
|
|
|
54
54
|
const mockCase = { case_id: 'case-1', title: 'Test Case', items: [] };
|
|
55
55
|
const hitLeaf = { type: 'hit', value: 'hit-123', path: 'folder/hit-item' };
|
|
56
56
|
const referenceLeaf = { type: 'reference', value: 'https://example.com', path: 'folder/ref-item' };
|
|
57
|
-
const
|
|
57
|
+
const eventLeaf = { type: 'event', value: 'obs-456', path: 'folder/event-item' };
|
|
58
58
|
const caseLeaf = { type: 'case', value: 'nested-case-id', path: 'folder/case-item' };
|
|
59
59
|
const tableLeaf = { type: 'table', value: 'table-789', path: 'folder/table-item' };
|
|
60
60
|
const leadLeaf = { type: 'lead', value: 'lead-999', path: 'folder/lead-item' };
|
|
@@ -119,8 +119,8 @@ describe('getOpenUrl', () => {
|
|
|
119
119
|
it('returns /hits/<id> for a hit item', () => {
|
|
120
120
|
expect(getOpenUrl(hitLeaf)).toBe('/hits/hit-123');
|
|
121
121
|
});
|
|
122
|
-
it('returns /
|
|
123
|
-
expect(getOpenUrl(
|
|
122
|
+
it('returns /events/<id> for an event item', () => {
|
|
123
|
+
expect(getOpenUrl(eventLeaf)).toBe('/events/obs-456');
|
|
124
124
|
});
|
|
125
125
|
it('returns /cases/<id> for a case item', () => {
|
|
126
126
|
expect(getOpenUrl(caseLeaf)).toBe('/cases/nested-case-id');
|
|
@@ -159,8 +159,8 @@ describe('CaseFolderContextMenu', () => {
|
|
|
159
159
|
expect(screen.getByTestId('open-item')).toBeInTheDocument();
|
|
160
160
|
expect(screen.getByTestId('remove-item')).toBeInTheDocument();
|
|
161
161
|
});
|
|
162
|
-
it('shows "Open item" and "Remove item" for an
|
|
163
|
-
renderMenu({ leaf:
|
|
162
|
+
it('shows "Open item" and "Remove item" for an event leaf', () => {
|
|
163
|
+
renderMenu({ leaf: eventLeaf });
|
|
164
164
|
expect(screen.getByTestId('open-item')).toBeInTheDocument();
|
|
165
165
|
expect(screen.getByTestId('remove-item')).toBeInTheDocument();
|
|
166
166
|
});
|
|
@@ -219,12 +219,12 @@ describe('CaseFolderContextMenu', () => {
|
|
|
219
219
|
});
|
|
220
220
|
expect(window.open).toHaveBeenCalledWith('https://example.com', '_blank', 'noopener noreferrer');
|
|
221
221
|
});
|
|
222
|
-
it('calls window.open with the
|
|
223
|
-
renderMenu({ leaf:
|
|
222
|
+
it('calls window.open with the event URL', () => {
|
|
223
|
+
renderMenu({ leaf: eventLeaf });
|
|
224
224
|
act(() => {
|
|
225
225
|
fireEvent.click(screen.getByTestId('open-item'));
|
|
226
226
|
});
|
|
227
|
-
expect(window.open).toHaveBeenCalledWith('/
|
|
227
|
+
expect(window.open).toHaveBeenCalledWith('/events/obs-456', '_blank', 'noopener noreferrer');
|
|
228
228
|
});
|
|
229
229
|
it('calls window.open with the case URL', () => {
|
|
230
230
|
renderMenu({ leaf: caseLeaf });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import type { Event } from '@cccsaurora/howler-ui/models/entities/generated/Event';
|
|
1
2
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
2
|
-
import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
|
|
3
3
|
import { type FC } from 'react';
|
|
4
4
|
declare const AddToCaseModal: FC<{
|
|
5
|
-
records: (Hit |
|
|
5
|
+
records: (Hit | Event)[];
|
|
6
6
|
}>;
|
|
7
7
|
export default AddToCaseModal;
|
|
@@ -5,7 +5,7 @@ import userEvent, {} from '@testing-library/user-event';
|
|
|
5
5
|
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
6
6
|
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
7
7
|
import { I18nextProvider } from 'react-i18next';
|
|
8
|
-
import { createMockCase,
|
|
8
|
+
import { createMockCase, createMockEvent, createMockHit } from '@cccsaurora/howler-ui/tests/utils';
|
|
9
9
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
10
|
import AddToCaseModal from './AddToCaseModal';
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
@@ -49,7 +49,7 @@ const MOCK_HIT_1 = createMockHit({
|
|
|
49
49
|
const MOCK_HIT_2 = createMockHit({
|
|
50
50
|
howler: { id: 'hit-002', analytic: 'AnalyticTwo', status: 'open' }
|
|
51
51
|
});
|
|
52
|
-
const
|
|
52
|
+
const MOCK_EVENT = createMockEvent({
|
|
53
53
|
howler: { id: 'obs-001' }
|
|
54
54
|
});
|
|
55
55
|
// ---------------------------------------------------------------------------
|
|
@@ -114,12 +114,12 @@ describe('AddToCaseModal', () => {
|
|
|
114
114
|
const titleInput = screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
|
|
115
115
|
expect(titleInput).toHaveValue(`${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`);
|
|
116
116
|
});
|
|
117
|
-
it('pre-populates title for an
|
|
118
|
-
renderModal([
|
|
117
|
+
it('pre-populates title for an event with Event and id', async () => {
|
|
118
|
+
renderModal([MOCK_EVENT]);
|
|
119
119
|
await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
|
|
120
120
|
await selectCase(user, 'Case Alpha');
|
|
121
121
|
const titleInput = screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
|
|
122
|
-
expect(titleInput).toHaveValue(`
|
|
122
|
+
expect(titleInput).toHaveValue(`Event (${MOCK_EVENT.howler.id})`);
|
|
123
123
|
});
|
|
124
124
|
it('title input is editable', async () => {
|
|
125
125
|
renderModal([MOCK_HIT_1]);
|
|
@@ -161,13 +161,13 @@ describe('AddToCaseModal', () => {
|
|
|
161
161
|
expect(titleInputs[0]).toHaveValue('Edited');
|
|
162
162
|
expect(titleInputs[1]).toHaveValue(`${MOCK_HIT_2.howler.analytic} (${MOCK_HIT_2.howler.id})`);
|
|
163
163
|
});
|
|
164
|
-
it('mixed hit and
|
|
165
|
-
renderModal([MOCK_HIT_1,
|
|
164
|
+
it('mixed hit and event records each get correct default titles', async () => {
|
|
165
|
+
renderModal([MOCK_HIT_1, MOCK_EVENT]);
|
|
166
166
|
await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
|
|
167
167
|
await selectCase(user, 'Case Alpha');
|
|
168
168
|
const titleInputs = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
|
|
169
169
|
expect(titleInputs[0]).toHaveValue(`${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`);
|
|
170
|
-
expect(titleInputs[1]).toHaveValue(`
|
|
170
|
+
expect(titleInputs[1]).toHaveValue(`Event (${MOCK_EVENT.howler.id})`);
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
173
|
// -------------------------------------------------------------------------
|
|
@@ -260,14 +260,14 @@ describe('AddToCaseModal', () => {
|
|
|
260
260
|
path: `investigations/${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`
|
|
261
261
|
}));
|
|
262
262
|
});
|
|
263
|
-
it('uses
|
|
263
|
+
it('uses event __index for event records', async () => {
|
|
264
264
|
const api = (await import('api')).default;
|
|
265
|
-
renderModal([
|
|
265
|
+
renderModal([MOCK_EVENT]);
|
|
266
266
|
await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
|
|
267
267
|
await selectCase(user, 'Case Alpha');
|
|
268
268
|
await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
|
|
269
269
|
await waitFor(() => expect(mockClose).toHaveBeenCalled());
|
|
270
|
-
expect(api.v2.case.items.post).toHaveBeenCalledWith('case-a', expect.objectContaining({ type: '
|
|
270
|
+
expect(api.v2.case.items.post).toHaveBeenCalledWith('case-a', expect.objectContaining({ type: 'event' }));
|
|
271
271
|
});
|
|
272
272
|
});
|
|
273
273
|
// -------------------------------------------------------------------------
|
|
@@ -10,6 +10,6 @@ const CaseRecordRow = ({ entry, folderOptions = [], onTitleChange, onPathChange
|
|
|
10
10
|
const { record, path, title } = entry;
|
|
11
11
|
const fullPath = path ? `${path}/${title}` : title;
|
|
12
12
|
const pathError = path.startsWith('/') || path.endsWith('/');
|
|
13
|
-
return (_jsxs(Accordion, { variant: "outlined", defaultExpanded: true, sx: { flexShrink: 0 }, children: [_jsx(AccordionSummary, { expandIcon: _jsx(KeyboardArrowDown, {}), sx: { px: 1, minHeight: '48px !important', '& > *': { margin: '0 !important' } }, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, width: "100%", children: [_jsx(Typography, { variant: "body2", fontWeight: 500, sx: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }, children: isHit(record) ? record.howler.analytic : '
|
|
13
|
+
return (_jsxs(Accordion, { variant: "outlined", defaultExpanded: true, sx: { flexShrink: 0 }, children: [_jsx(AccordionSummary, { expandIcon: _jsx(KeyboardArrowDown, {}), sx: { px: 1, minHeight: '48px !important', '& > *': { margin: '0 !important' } }, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, width: "100%", children: [_jsx(Typography, { variant: "body2", fontWeight: 500, sx: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }, children: isHit(record) ? record.howler.analytic : 'Event' }), isHit(record) && _jsx(EscalationChip, { hit: record, layout: HitLayout.DENSE }), isHit(record) && _jsx(Chip, { label: record.howler.status, size: "small", color: "primary", sx: { flexShrink: 0 } }), _jsx(Typography, { variant: "caption", color: "textSecondary", sx: { flexShrink: 0 }, children: record.howler.id })] }) }), _jsx(AccordionDetails, { children: _jsxs(Stack, { spacing: 1, children: [_jsx(Autocomplete, { freeSolo: true, disablePortal: true, options: folderOptions, value: path, onInputChange: (_ev, newVal) => onPathChange(newVal), renderInput: params => (_jsx(TextField, { ...params, size: "small", placeholder: t('modal.cases.add_to_case.select_path'), fullWidth: true, error: pathError, helperText: pathError ? t('modal.cases.add_to_case.path_invalid') : undefined })) }), _jsx(TextField, { size: "small", fullWidth: true, placeholder: t('modal.cases.add_to_case.title'), value: title, onChange: ev => onTitleChange(ev.target.value) }), title && (_jsx(Typography, { variant: "caption", color: "textSecondary", children: t('modal.cases.add_to_case.full_path', { path: fullPath }) }))] }) })] }));
|
|
14
14
|
};
|
|
15
15
|
export default CaseRecordRow;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import type { Event } from '@cccsaurora/howler-ui/models/entities/generated/Event';
|
|
1
2
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
2
|
-
import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
|
|
3
3
|
import { type FC } from 'react';
|
|
4
4
|
declare const CreateCaseModal: FC<{
|
|
5
|
-
records: (Hit |
|
|
5
|
+
records: (Hit | Event)[];
|
|
6
6
|
}>;
|
|
7
7
|
export default CreateCaseModal;
|
|
@@ -6,7 +6,7 @@ import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers
|
|
|
6
6
|
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
7
7
|
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
8
8
|
import { I18nextProvider } from 'react-i18next';
|
|
9
|
-
import {
|
|
9
|
+
import { createMockEvent, createMockHit } from '@cccsaurora/howler-ui/tests/utils';
|
|
10
10
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
11
|
import CreateCaseModal from './CreateCaseModal';
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -48,7 +48,7 @@ const MOCK_HIT_1 = createMockHit({
|
|
|
48
48
|
const MOCK_HIT_2 = createMockHit({
|
|
49
49
|
howler: { id: 'hit-002', analytic: 'AnalyticTwo', status: 'open' }
|
|
50
50
|
});
|
|
51
|
-
const
|
|
51
|
+
const MOCK_EVENT = createMockEvent({
|
|
52
52
|
howler: { id: 'obs-001' }
|
|
53
53
|
});
|
|
54
54
|
const MOCK_CONFIG = {
|
|
@@ -115,19 +115,19 @@ describe('CreateCaseModal', () => {
|
|
|
115
115
|
expect(screen.getByText(MOCK_HIT_1.howler.analytic)).toBeInTheDocument();
|
|
116
116
|
expect(screen.getByText(MOCK_HIT_2.howler.analytic)).toBeInTheDocument();
|
|
117
117
|
});
|
|
118
|
-
it('renders a row for an
|
|
119
|
-
renderModal([
|
|
120
|
-
expect(screen.getByText('
|
|
118
|
+
it('renders a row for an event record', () => {
|
|
119
|
+
renderModal([MOCK_EVENT]);
|
|
120
|
+
expect(screen.getByText('Event')).toBeInTheDocument();
|
|
121
121
|
});
|
|
122
122
|
it('pre-populates the title for a hit with analytic and id', () => {
|
|
123
123
|
renderModal([MOCK_HIT_1]);
|
|
124
124
|
const titleInputs = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
|
|
125
125
|
expect(titleInputs[0]).toHaveValue(`${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`);
|
|
126
126
|
});
|
|
127
|
-
it('pre-populates the title for an
|
|
128
|
-
renderModal([
|
|
127
|
+
it('pre-populates the title for an event with Event and id', () => {
|
|
128
|
+
renderModal([MOCK_EVENT]);
|
|
129
129
|
const titleInput = screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
|
|
130
|
-
expect(titleInput).toHaveValue(`
|
|
130
|
+
expect(titleInput).toHaveValue(`Event (${MOCK_EVENT.howler.id})`);
|
|
131
131
|
});
|
|
132
132
|
it('shows the alert placement section label when there are records', () => {
|
|
133
133
|
renderModal([MOCK_HIT_1]);
|
|
@@ -302,14 +302,14 @@ describe('CreateCaseModal', () => {
|
|
|
302
302
|
path: `investigations/${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`
|
|
303
303
|
}));
|
|
304
304
|
});
|
|
305
|
-
it('uses
|
|
305
|
+
it('uses event __index for event records', async () => {
|
|
306
306
|
const api = (await import('api')).default;
|
|
307
307
|
mockDispatchApi.mockResolvedValue({ case_id: 'new-case-id' });
|
|
308
|
-
renderModal([
|
|
308
|
+
renderModal([MOCK_EVENT]);
|
|
309
309
|
await fillCaseMetadata(user);
|
|
310
310
|
await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
|
|
311
311
|
await waitFor(() => expect(mockClose).toHaveBeenCalled());
|
|
312
|
-
expect(api.v2.case.items.post).toHaveBeenCalledWith('new-case-id', expect.objectContaining({ type: '
|
|
312
|
+
expect(api.v2.case.items.post).toHaveBeenCalledWith('new-case-id', expect.objectContaining({ type: 'event' }));
|
|
313
313
|
});
|
|
314
314
|
it('does not call items.post when there are no records', async () => {
|
|
315
315
|
const api = (await import('api')).default;
|
|
@@ -348,11 +348,11 @@ describe('CreateCaseModal', () => {
|
|
|
348
348
|
expect(titleInputs[0]).toHaveValue('Edited Title');
|
|
349
349
|
expect(titleInputs[1]).toHaveValue(`${MOCK_HIT_2.howler.analytic} (${MOCK_HIT_2.howler.id})`);
|
|
350
350
|
});
|
|
351
|
-
it('mixed hit and
|
|
352
|
-
renderModal([MOCK_HIT_1,
|
|
351
|
+
it('mixed hit and event records each get correct default titles', () => {
|
|
352
|
+
renderModal([MOCK_HIT_1, MOCK_EVENT]);
|
|
353
353
|
const titleInputs = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
|
|
354
354
|
expect(titleInputs[0]).toHaveValue(`${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`);
|
|
355
|
-
expect(titleInputs[1]).toHaveValue(`
|
|
355
|
+
expect(titleInputs[1]).toHaveValue(`Event (${MOCK_EVENT.howler.id})`);
|
|
356
356
|
});
|
|
357
357
|
});
|
|
358
358
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
|
|
2
|
+
import type { Event } from '@cccsaurora/howler-ui/models/entities/generated/Event';
|
|
2
3
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
3
|
-
import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
|
|
4
4
|
import type { RecordEntry } from './types';
|
|
5
|
-
export declare const defaultTitle: (record: Hit |
|
|
5
|
+
export declare const defaultTitle: (record: Hit | Event) => string;
|
|
6
6
|
export declare const useFolderOptions: (selectedCase: Case | null) => string[];
|
|
7
|
-
export declare const useRecordEntries: (records: (Hit |
|
|
7
|
+
export declare const useRecordEntries: (records: (Hit | Event)[]) => readonly [RecordEntry[], (index: number, field: "title" | "path", value: string) => void];
|
|
@@ -3,7 +3,7 @@ export const defaultTitle = (record) => {
|
|
|
3
3
|
if (record.__index === 'hit') {
|
|
4
4
|
return `${record.howler.analytic} (${record.howler.id})`;
|
|
5
5
|
}
|
|
6
|
-
return `
|
|
6
|
+
return `Event (${record.howler.id})`;
|
|
7
7
|
};
|
|
8
8
|
export const useFolderOptions = (selectedCase) => {
|
|
9
9
|
return useMemo(() => {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Skeleton } from '@mui/material';
|
|
3
|
+
import api from '@cccsaurora/howler-ui/api';
|
|
4
|
+
import ObjectDetails from '@cccsaurora/howler-ui/components/elements/ObjectDetails';
|
|
5
|
+
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
|
+
const EventViewer = ({ event: provided, eventId }) => {
|
|
8
|
+
const { dispatchApi } = useMyApi();
|
|
9
|
+
const [event, setEvent] = useState(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (provided) {
|
|
12
|
+
setEvent(provided);
|
|
13
|
+
}
|
|
14
|
+
}, [provided]);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (eventId) {
|
|
17
|
+
dispatchApi(api.v2.search.post('event', { query: `howler.id:${eventId}`, rows: 1 }), {
|
|
18
|
+
throwError: false
|
|
19
|
+
}).then(res => setEvent(res.items[0]));
|
|
20
|
+
}
|
|
21
|
+
}, [dispatchApi, eventId]);
|
|
22
|
+
if (!event) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
return _jsx(Box, { p: 1, children: event ? _jsx(ObjectDetails, { obj: event }) : _jsx(Skeleton, { height: 120 }) });
|
|
26
|
+
};
|
|
27
|
+
export default EventViewer;
|
|
@@ -14,9 +14,9 @@ import VSBoxContent from '@cccsaurora/howler-ui/components/elements/addons/layou
|
|
|
14
14
|
import VSBoxHeader from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBoxHeader';
|
|
15
15
|
import SearchPagination from '@cccsaurora/howler-ui/components/elements/addons/search/SearchPagination';
|
|
16
16
|
import SearchTotal from '@cccsaurora/howler-ui/components/elements/addons/search/SearchTotal';
|
|
17
|
+
import EventCard from '@cccsaurora/howler-ui/components/elements/event/EventCard';
|
|
17
18
|
import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
|
|
18
19
|
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
19
|
-
import ObservableCard from '@cccsaurora/howler-ui/components/elements/observable/ObservableCard';
|
|
20
20
|
import RecordContextMenu from '@cccsaurora/howler-ui/components/elements/record/RecordContextMenu';
|
|
21
21
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
22
22
|
import useRecordSelection from '@cccsaurora/howler-ui/components/hooks/useRecordSelection';
|
|
@@ -26,7 +26,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
26
26
|
import { Link } from 'react-router-dom';
|
|
27
27
|
import { useContextSelector } from 'use-context-selector';
|
|
28
28
|
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
29
|
-
import {
|
|
29
|
+
import { isEvent, isHit } from '@cccsaurora/howler-ui/utils/typeUtils';
|
|
30
30
|
import LayoutSettings from './LayoutSettings';
|
|
31
31
|
import QuerySettings from './QuerySettings';
|
|
32
32
|
import RecordQuery from './RecordQuery';
|
|
@@ -73,7 +73,7 @@ const Item = memo(({ record, onClick }) => {
|
|
|
73
73
|
boxShadow: `0px 0px 5px 2px ${theme.palette.primary.main}`
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
], children: [isHit(record) && _jsx(HitCard, { id: record.howler.id, layout: layout }),
|
|
76
|
+
], children: [isHit(record) && _jsx(HitCard, { id: record.howler.id, layout: layout }), isEvent(record) && _jsx(EventCard, { id: record.howler.id, event: record })] }));
|
|
77
77
|
});
|
|
78
78
|
const SearchPane = () => {
|
|
79
79
|
const { t } = useTranslation();
|