@cccsaurora/howler-ui 2.18.0-dev.686 → 2.18.0-dev.688

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.
Files changed (28) hide show
  1. package/api/v2/case/index.d.ts +2 -0
  2. package/api/v2/case/index.js +2 -0
  3. package/api/v2/case/items.d.ts +5 -0
  4. package/api/v2/case/items.js +12 -0
  5. package/components/elements/case/CaseCard.d.ts +4 -0
  6. package/components/elements/case/CaseCard.js +5 -2
  7. package/components/elements/record/RecordContextMenu.js +14 -2
  8. package/components/elements/record/RecordContextMenu.test.js +56 -1
  9. package/components/routes/cases/detail/AlertPanel.js +2 -2
  10. package/components/routes/cases/detail/CaseAssets.js +5 -2
  11. package/components/routes/cases/detail/CaseAssets.test.js +22 -18
  12. package/components/routes/cases/detail/CaseDashboard.js +4 -1
  13. package/components/routes/cases/detail/ItemPage.js +5 -5
  14. package/components/routes/cases/detail/RelatedCasePanel.js +2 -2
  15. package/components/routes/cases/detail/aggregates/SourceAggregate.js +4 -1
  16. package/components/routes/cases/detail/assets/Asset.js +1 -1
  17. package/components/routes/cases/detail/assets/Asset.test.js +5 -5
  18. package/components/routes/cases/detail/sidebar/CaseFolder.js +9 -8
  19. package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
  20. package/components/routes/cases/modals/AddToCaseModal.js +62 -0
  21. package/components/routes/cases/modals/ResolveModal.js +4 -1
  22. package/locales/en/translation.json +5 -0
  23. package/locales/fr/translation.json +5 -0
  24. package/models/entities/generated/Item.d.ts +1 -1
  25. package/package.json +113 -113
  26. package/tests/server-handlers.js +6 -1
  27. package/tests/utils.d.ts +2 -0
  28. package/tests/utils.js +12 -0
@@ -1,6 +1,8 @@
1
+ import * as items from '@cccsaurora/howler-ui/api/v2/case/items';
1
2
  import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
3
  export declare const uri: (id?: string) => string;
3
4
  export declare const get: (id: string) => Promise<Case>;
4
5
  export declare const post: (newData: Partial<Case>) => Promise<Case>;
5
6
  export declare const put: (id: string, _case: Partial<Case>) => Promise<Case>;
6
7
  export declare const del: (id: string) => Promise<void>;
8
+ export { items };
@@ -1,6 +1,7 @@
1
1
  // eslint-disable-next-line import/no-cycle
2
2
  import { hdelete, hget, hpost, hput, joinAllUri, joinUri } from '@cccsaurora/howler-ui/api';
3
3
  import { uri as parentUri } from '@cccsaurora/howler-ui/api/v2';
4
+ import * as items from '@cccsaurora/howler-ui/api/v2/case/items';
4
5
  export const uri = (id) => {
5
6
  return id ? joinAllUri(parentUri(), 'case', id) : joinUri(parentUri(), 'case');
6
7
  };
@@ -16,3 +17,4 @@ export const put = (id, _case) => {
16
17
  export const del = (id) => {
17
18
  return hdelete(uri(id));
18
19
  };
20
+ export { items };
@@ -0,0 +1,5 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import type { Item } from '@cccsaurora/howler-ui/models/entities/generated/Item';
3
+ export declare const uri: (id: string) => string;
4
+ export declare const post: (id: string, newData: Item) => Promise<Case>;
5
+ export declare const del: (id: string, value: string) => Promise<void>;
@@ -0,0 +1,12 @@
1
+ // eslint-disable-next-line import/no-cycle
2
+ import { hdelete, hpost, joinUri } from '@cccsaurora/howler-ui/api';
3
+ import { uri as parentUri } from '@cccsaurora/howler-ui/api/v2/case';
4
+ export const uri = (id) => {
5
+ return joinUri(parentUri(id), 'items');
6
+ };
7
+ export const post = (id, newData) => {
8
+ return hpost(uri(id), newData);
9
+ };
10
+ export const del = (id, value) => {
11
+ return hdelete(uri(id), { value });
12
+ };
@@ -1,8 +1,12 @@
1
+ import { type CardProps } from '@mui/material';
1
2
  import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
3
  import { type FC } from 'react';
3
4
  declare const CaseCard: FC<{
4
5
  case?: Case;
5
6
  caseId?: string;
6
7
  className?: string;
8
+ slotProps?: {
9
+ card?: CardProps;
10
+ };
7
11
  }>;
8
12
  export default CaseCard;
@@ -14,7 +14,7 @@ import { twitterShort } from '@cccsaurora/howler-ui/utils/utils';
14
14
  const STATUS_COLORS = {
15
15
  resolved: 'success'
16
16
  };
17
- const CaseCard = ({ case: providedCase, caseId, className }) => {
17
+ const CaseCard = ({ case: providedCase, caseId, className, slotProps }) => {
18
18
  const { t } = useTranslation();
19
19
  const { dispatchApi } = useMyApi();
20
20
  const theme = useTheme();
@@ -32,7 +32,10 @@ const CaseCard = ({ case: providedCase, caseId, className }) => {
32
32
  if (!_case) {
33
33
  return _jsx(Skeleton, { variant: "rounded", height: 250, sx: { mb: 1 }, className: className });
34
34
  }
35
- return (_jsx(Card, { variant: "outlined", sx: { p: 1, mb: 1, borderColor: theme.palette[STATUS_COLORS[_case.status]]?.main }, className: className, children: _jsx(Stack, { direction: "row", alignItems: "start", spacing: 1, children: _jsxs(Stack, { sx: { flex: 1 }, spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", display: "flex", alignItems: "start", children: _case.title }), _jsx(StatusIcon, { status: _case.status }), _jsx("div", { style: { flex: 1 } }), _case.start && _case.end && (_jsx(Tooltip, { title: dayjs(_case.updated).toString(), children: _jsx(Chip, { icon: _jsx(HourglassBottom, { fontSize: "small" }), size: "small", label: twitterShort(_case.start) + ' - ' + twitterShort(_case.end) }) })), _jsx(Tooltip, { title: dayjs(_case.updated).toString(), children: _jsx(Chip, { icon: _jsx(UpdateOutlined, { fontSize: "small" }), size: "small", label: twitterShort(_case.updated) }) })] }), _jsx(Typography, { variant: "caption", color: "textSecondary", children: _case.summary.trim().split('\n')[0] }), _case.participants?.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Divider, { flexItem: true }), _jsx(Stack, { direction: "row", spacing: 1, children: _case.participants?.map(participant => (_jsx(HowlerAvatar, { sx: { height: '20px', width: '20px' }, userId: participant }, participant))) })] })), _jsx(Divider, { flexItem: true }), _jsxs(Grid, { container: true, spacing: 1, children: [_case.targets?.map(indicator => (_jsx(Grid, { item: true, children: _jsx(PluginChip, { size: "small", color: "primary", context: "casecard", variant: "outlined", value: indicator, label: indicator }) }, indicator))), _case.targets?.length > 0 && (_case.indicators?.length > 0 || _case.threats?.length > 0) && (_jsx(Grid, { item: true, children: _jsx(Divider, { orientation: "vertical" }) })), _case.indicators?.map(indicator => (_jsx(Grid, { item: true, children: _jsx(PluginChip, { variant: "outlined", context: "casecard", value: indicator, label: indicator }) }, indicator))), _case.indicators?.length > 0 && _case.threats?.length > 0 && (_jsx(Grid, { item: true, children: _jsx(Divider, { orientation: "vertical" }) })), _case.threats?.map(indicator => (_jsx(Grid, { item: true, children: _jsx(PluginChip, { size: "small", color: "warning", variant: "outlined", context: "casecard", value: indicator, label: indicator }) }, indicator)))] }), _case.tasks?.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Divider, { flexItem: true }), _jsxs(Stack, { spacing: 0.5, alignItems: "start", children: [_case.tasks.some(task => task.complete) && (_jsx(Chip, { size: "small", color: "success", icon: _jsx(CheckCircleOutline, {}), label: `${countBy(_case.tasks, task => task.complete).true} ${t('complete')}` })), _case.tasks
35
+ return (_jsx(Card, { variant: "outlined", ...slotProps?.card, sx: [
36
+ { p: 1, mb: 1, borderColor: theme.palette[STATUS_COLORS[_case.status]]?.main },
37
+ ...(Array.isArray(slotProps?.card?.sx) ? slotProps.card.sx : slotProps?.card?.sx ? [slotProps.card.sx] : [])
38
+ ], className: className, children: _jsx(Stack, { direction: "row", alignItems: "start", spacing: 1, children: _jsxs(Stack, { sx: { flex: 1 }, spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", display: "flex", alignItems: "start", children: _case.title }), _jsx(StatusIcon, { status: _case.status }), _jsx("div", { style: { flex: 1 } }), _case.start && _case.end && (_jsx(Tooltip, { title: dayjs(_case.updated).toString(), children: _jsx(Chip, { icon: _jsx(HourglassBottom, { fontSize: "small" }), size: "small", label: twitterShort(_case.start) + ' - ' + twitterShort(_case.end) }) })), _jsx(Tooltip, { title: dayjs(_case.updated).toString(), children: _jsx(Chip, { icon: _jsx(UpdateOutlined, { fontSize: "small" }), size: "small", label: twitterShort(_case.updated) }) })] }), _jsx(Typography, { variant: "caption", color: "textSecondary", children: _case.summary.trim().split('\n')[0] }), _case.participants?.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Divider, { flexItem: true }), _jsx(Stack, { direction: "row", spacing: 1, children: _case.participants?.map(participant => (_jsx(HowlerAvatar, { sx: { height: '20px', width: '20px' }, userId: participant }, participant))) })] })), _jsx(Divider, { flexItem: true }), _jsxs(Grid, { container: true, spacing: 1, children: [_case.targets?.map(indicator => (_jsx(Grid, { item: true, children: _jsx(PluginChip, { size: "small", color: "primary", context: "casecard", variant: "outlined", value: indicator, label: indicator }) }, indicator))), _case.targets?.length > 0 && (_case.indicators?.length > 0 || _case.threats?.length > 0) && (_jsx(Grid, { item: true, children: _jsx(Divider, { orientation: "vertical" }) })), _case.indicators?.map(indicator => (_jsx(Grid, { item: true, children: _jsx(PluginChip, { variant: "outlined", context: "casecard", value: indicator, label: indicator }) }, indicator))), _case.indicators?.length > 0 && _case.threats?.length > 0 && (_jsx(Grid, { item: true, children: _jsx(Divider, { orientation: "vertical" }) })), _case.threats?.map(indicator => (_jsx(Grid, { item: true, children: _jsx(PluginChip, { size: "small", color: "warning", variant: "outlined", context: "casecard", value: indicator, label: indicator }) }, indicator)))] }), _case.tasks?.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Divider, { flexItem: true }), _jsxs(Stack, { spacing: 0.5, alignItems: "start", children: [_case.tasks.some(task => task.complete) && (_jsx(Chip, { size: "small", color: "success", icon: _jsx(CheckCircleOutline, {}), label: `${countBy(_case.tasks, task => task.complete).true} ${t('complete')}` })), _case.tasks
36
39
  .filter(task => !task.complete)
37
40
  .map(task => (_jsx(Chip, { icon: _jsx(RadioButtonUnchecked, {}), label: task.summary }, task.id)))] })] }))] }) }) }, _case.case_id));
38
41
  };
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { AddCircleOutline, Assignment, Edit, HowToVote, OpenInNew, QueryStats, RemoveCircleOutline, SettingsSuggest, Terminal } from '@mui/icons-material';
2
+ import { AddCircleOutline, Assignment, CreateNewFolder, Edit, HowToVote, OpenInNew, QueryStats, RemoveCircleOutline, SettingsSuggest, Terminal } from '@mui/icons-material';
3
3
  import api from '@cccsaurora/howler-ui/api';
4
4
  import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
5
5
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
6
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
6
7
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
7
8
  import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
8
9
  import ContextMenu, {} from '@cccsaurora/howler-ui/components/elements/ContextMenu';
@@ -10,6 +11,7 @@ import { TOP_ROW, VOTE_OPTIONS } from '@cccsaurora/howler-ui/components/elements
10
11
  import useHitActions from '@cccsaurora/howler-ui/components/hooks/useHitActions';
11
12
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
12
13
  import useMyActionFunctions from '@cccsaurora/howler-ui/components/routes/action/useMyActionFunctions';
14
+ import AddToCaseModal from '@cccsaurora/howler-ui/components/routes/cases/modals/AddToCaseModal';
13
15
  import { capitalize, get, groupBy, isEmpty, toString } from 'lodash-es';
14
16
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
15
17
  import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
@@ -41,6 +43,7 @@ const RecordContextMenu = ({ children, getSelectedId, Component }) => {
41
43
  const { dispatchApi } = useMyApi();
42
44
  const { executeAction } = useMyActionFunctions();
43
45
  const { config } = useContext(ApiConfigContext);
46
+ const { showModal } = useContext(ModalContext);
44
47
  const pluginStore = usePluginStore();
45
48
  const { getMatchingAnalytic, getMatchingTemplate } = useMatchers();
46
49
  const query = useContextSelector(ParameterContext, ctx => ctx?.query);
@@ -227,9 +230,18 @@ const RecordContextMenu = ({ children, getSelectedId, Component }) => {
227
230
  });
228
231
  }
229
232
  }
233
+ result.push({ kind: 'divider', id: 'add-to-case-divider' });
234
+ result.push({
235
+ kind: 'item',
236
+ id: 'add-to-case',
237
+ icon: _jsx(CreateNewFolder, {}),
238
+ label: t('modal.cases.add_to_case'),
239
+ disabled: !record,
240
+ onClick: () => showModal(_jsx(AddToCaseModal, { records: records }))
241
+ });
230
242
  return result;
231
243
  // eslint-disable-next-line react-hooks/exhaustive-deps
232
- }, [record, analytic, template, entries, rowStatus, actions, query, t, setQuery, executeAction]);
244
+ }, [record, analytic, template, entries, rowStatus, actions, query, t, setQuery, executeAction, showModal, records]);
233
245
  return (_jsx(ContextMenu, { id: "contextMenu", Component: Component, onOpen: onOpen, onClose: () => setAnalytic(null), items: items, children: children }));
234
246
  };
235
247
  export default RecordContextMenu;
@@ -47,6 +47,7 @@ vi.mock('components/app/hooks/useMatchers', () => ({
47
47
  getMatchingTemplate: mockGetMatchingTemplate
48
48
  }))
49
49
  }));
50
+ const mockShowModal = vi.fn();
50
51
  const mockDispatchApi = vi.fn();
51
52
  vi.mock('components/hooks/useMyApi', () => ({
52
53
  default: vi.fn(() => ({
@@ -72,6 +73,9 @@ vi.mock('plugins/store', () => ({
72
73
  plugins: ['plugin1']
73
74
  }
74
75
  }));
76
+ vi.mock('components/routes/cases/modals/AddToCaseModal', () => ({
77
+ default: () => null
78
+ }));
75
79
  // Mock MUI components
76
80
  vi.mock('@mui/material', async () => {
77
81
  const actual = await vi.importActual('@mui/material');
@@ -91,6 +95,7 @@ vi.mock('@mui/material', async () => {
91
95
  });
92
96
  // Import component after mocks
93
97
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
98
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
94
99
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
95
100
  import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
96
101
  import i18n from '@cccsaurora/howler-ui/i18n';
@@ -114,7 +119,7 @@ const mockRecordContext = {
114
119
  const mockParameterContext = { query: DEFAULT_QUERY, setQuery: vi.fn() };
115
120
  // Test wrapper
116
121
  const Wrapper = ({ children }) => {
117
- return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(RecordContext.Provider, { value: mockRecordContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: children }) }) }) }));
122
+ return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(ModalContext.Provider, { value: { showModal: mockShowModal }, children: _jsx(RecordContext.Provider, { value: mockRecordContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: children }) }) }) }) }));
118
123
  };
119
124
  describe('HitContextMenu', () => {
120
125
  let user;
@@ -893,4 +898,54 @@ describe('HitContextMenu', () => {
893
898
  expect(mockPluginStoreExecuteFunction).toHaveBeenCalled();
894
899
  });
895
900
  });
901
+ describe('Add to Case Menu Item', () => {
902
+ it('should render "Add to Case" item in the menu', async () => {
903
+ act(() => {
904
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
905
+ fireEvent.contextMenu(contextMenuWrapper);
906
+ });
907
+ await waitFor(() => {
908
+ expect(screen.getByText('Add to Case')).toBeInTheDocument();
909
+ });
910
+ });
911
+ it('should enable "Add to Case" when a record is present', async () => {
912
+ act(() => {
913
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
914
+ fireEvent.contextMenu(contextMenuWrapper);
915
+ });
916
+ await waitFor(() => {
917
+ const menuItems = screen.getAllByRole('menuitem');
918
+ const addToCaseItem = menuItems.find(item => item.textContent?.includes('Add to Case'));
919
+ expect(addToCaseItem).toHaveAttribute('aria-disabled', 'false');
920
+ });
921
+ });
922
+ it('should disable "Add to Case" when record is null', async () => {
923
+ act(() => {
924
+ mockRecordContext.records['test-hit-1'] = null;
925
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
926
+ fireEvent.contextMenu(contextMenuWrapper);
927
+ });
928
+ await waitFor(() => {
929
+ const menuItems = screen.getAllByRole('menuitem');
930
+ const addToCaseItem = menuItems.find(item => item.textContent?.includes('Add to Case'));
931
+ expect(addToCaseItem).toHaveAttribute('aria-disabled', 'true');
932
+ });
933
+ });
934
+ it('should call showModal with an AddToCaseModal element when clicked', async () => {
935
+ act(() => {
936
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
937
+ fireEvent.contextMenu(contextMenuWrapper);
938
+ });
939
+ await waitFor(() => {
940
+ expect(screen.getByText('Add to Case')).toBeInTheDocument();
941
+ });
942
+ await act(async () => {
943
+ await user.click(screen.getByText('Add to Case'));
944
+ });
945
+ await waitFor(() => {
946
+ expect(mockShowModal).toHaveBeenCalledOnce();
947
+ expect(mockShowModal).toHaveBeenCalledWith(expect.objectContaining({ type: expect.any(Function) }));
948
+ });
949
+ });
950
+ });
896
951
  });
@@ -15,7 +15,7 @@ const AlertPanel = ({ case: _case }) => {
15
15
  return _jsx(Skeleton, { height: 240 });
16
16
  }
17
17
  return (_jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.alerts') }), _jsx(Pagination, { count: alertPages.length, page: alertPage, onChange: (_, page) => setAlertPage(page) })] }), _jsx(Divider, {}), alertPages?.length > 0 &&
18
- alertPages[alertPage - 1].map(item => (_jsxs(Box, { position: "relative", children: [_jsx(HitCard, { layout: HitLayout.DENSE, id: item.id }), _jsx(Box, { component: Link, to: item.path, sx: {
18
+ alertPages[alertPage - 1].map(item => (_jsxs(Box, { position: "relative", children: [_jsx(HitCard, { layout: HitLayout.DENSE, id: item.value }), _jsx(Box, { component: Link, to: item.path, sx: {
19
19
  position: 'absolute',
20
20
  top: 0,
21
21
  left: 0,
@@ -28,6 +28,6 @@ const AlertPanel = ({ case: _case }) => {
28
28
  background: theme.palette.divider,
29
29
  border: `thin solid ${theme.palette.primary.light}`
30
30
  }
31
- } })] }, item.id)))] }));
31
+ } })] }, item.path)))] }));
32
32
  };
33
33
  export default AlertPanel;
@@ -59,7 +59,10 @@ const CaseAssets = ({ case: providedCase, caseId }) => {
59
59
  const { case: _case } = useCase({ case: providedCase ?? routeCase, caseId });
60
60
  const [records, setRecords] = useState(null);
61
61
  const [activeFilters, setActiveFilters] = useState(new Set());
62
- const ids = useMemo(() => (_case?.items ?? []).filter(item => ['hit', 'observable'].includes(item.type)).map(item => item.id), [_case?.items]);
62
+ const ids = useMemo(() => (_case?.items ?? [])
63
+ .filter(item => ['hit', 'observable'].includes(item.type))
64
+ .map(item => item.value)
65
+ .filter(val => !!val), [_case?.items]);
63
66
  useEffect(() => {
64
67
  if (ids.length < 1) {
65
68
  setRecords([]);
@@ -96,6 +99,6 @@ const CaseAssets = ({ case: providedCase, caseId }) => {
96
99
  if (!_case) {
97
100
  return null;
98
101
  }
99
- return (_jsxs(Grid, { container: true, spacing: 2, px: 2, children: [_jsx(Grid, { item: true, xs: 12, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, flexWrap: "wrap", children: [_jsx(Typography, { variant: "subtitle2", color: "text.secondary", children: t('page.cases.assets.filter_by_type') }), records === null ? (_jsx(Skeleton, { width: 240, height: 32 })) : (assetTypes.map(type => (_jsx(Chip, { label: t(`page.cases.assets.type.${type}`), size: "small", onClick: () => toggleFilter(type), color: activeFilters.has(type) ? 'primary' : 'default', variant: activeFilters.has(type) ? 'filled' : 'outlined' }, type))))] }) }), filteredAssets === null ? (Array.from({ length: 6 }, (_, i) => (_jsx(Grid, { item: true, xs: 12, sm: 6, md: 4, xl: 3, children: _jsx(Skeleton, { height: 100 }) }, `skeleton-${i}`)))) : filteredAssets.length === 0 ? (_jsx(Grid, { item: true, xs: 12, children: _jsx(Typography, { color: "text.secondary", children: t('page.cases.assets.empty') }) })) : (filteredAssets.map(asset => (_jsx(Grid, { item: true, xs: 12, md: 6, xl: 4, children: _jsx(Asset, { asset: asset, case: _case }) }, `${asset.type}:${asset.value}`))))] }));
102
+ return (_jsxs(Grid, { container: true, spacing: 2, px: 2, children: [_jsx(Grid, { item: true, xs: 12, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, flexWrap: "wrap", children: [_jsx(Typography, { variant: "subtitle2", color: "text.secondary", children: t('page.cases.assets.filter_by_type') }), records === null ? (_jsx(Skeleton, { width: 240, height: 32 })) : (assetTypes.map(type => (_jsx(Chip, { label: t(`page.cases.assets.type.${type}`), size: "small", onClick: () => toggleFilter(type), color: activeFilters.has(type) ? 'primary' : 'default', variant: activeFilters.has(type) ? 'filled' : 'outlined' }, type))))] }) }), records === null ? (Array.from({ length: 6 }, (_, i) => (_jsx(Grid, { item: true, xs: 12, sm: 6, md: 4, xl: 3, children: _jsx(Skeleton, { height: 100 }) }, `skeleton-${i}`)))) : filteredAssets.length === 0 ? (_jsx(Grid, { item: true, xs: 12, children: _jsx(Typography, { color: "text.secondary", children: t('page.cases.assets.empty') }) })) : (filteredAssets.map(asset => (_jsx(Grid, { item: true, xs: 12, md: 6, xl: 4, children: _jsx(Asset, { asset: asset, case: _case }) }, `${asset.type}:${asset.value}`))))] }));
100
103
  };
101
104
  export default CaseAssets;
@@ -4,44 +4,48 @@ import { act, render, screen } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
5
5
  import { createElement } from 'react';
6
6
  import { MemoryRouter } from 'react-router-dom';
7
+ import { createMockHit, createMockObservable } from '@cccsaurora/howler-ui/tests/utils';
7
8
  import { describe, expect, it, vi } from 'vitest';
8
9
  import { buildAssetEntries } from './CaseAssets';
9
10
  // ---------------------------------------------------------------------------
10
11
  // Pure logic tests — no React needed
11
12
  // ---------------------------------------------------------------------------
12
- const makeHit = (id, related) => ({
13
- howler: { id },
14
- related
15
- });
16
- const makeObservable = (id, related) => ({
17
- howler: { id },
18
- related
19
- });
20
13
  describe('buildAssetEntries', () => {
21
14
  it('returns an empty array for records with no related field', () => {
22
- expect(buildAssetEntries([makeHit('h1', undefined)])).toEqual([]);
15
+ expect(buildAssetEntries([createMockHit({ howler: { id: 'h1' } })])).toEqual([]);
23
16
  });
24
17
  it('extracts a single IP from a hit', () => {
25
- const result = buildAssetEntries([makeHit('h1', { ip: ['1.2.3.4'] })]);
18
+ const result = buildAssetEntries([createMockHit({ howler: { id: 'h1' }, related: { ip: ['1.2.3.4'] } })]);
26
19
  expect(result).toHaveLength(1);
27
20
  expect(result[0]).toEqual({ type: 'ip', value: '1.2.3.4', seenIn: ['h1'] });
28
21
  });
29
22
  it('extracts multiple fields from a single record', () => {
30
- const result = buildAssetEntries([makeHit('h1', { ip: ['1.2.3.4'], user: ['alice'] })]);
23
+ const result = buildAssetEntries([
24
+ createMockHit({ howler: { id: 'h1' }, related: { ip: ['1.2.3.4'], user: ['alice'] } })
25
+ ]);
31
26
  const types = result.map(a => a.type).sort();
32
27
  expect(types).toEqual(['ip', 'user']);
33
28
  });
34
29
  it('deduplicates the same asset value across multiple records', () => {
35
- const result = buildAssetEntries([makeHit('h1', { ip: ['1.2.3.4'] }), makeObservable('obs1', { ip: ['1.2.3.4'] })]);
30
+ const result = buildAssetEntries([
31
+ createMockHit({ howler: { id: 'h1' }, related: { ip: ['1.2.3.4'] } }),
32
+ createMockObservable({ howler: { id: 'obs1' }, related: { ip: ['1.2.3.4'] } })
33
+ ]);
36
34
  expect(result).toHaveLength(1);
37
35
  expect(result[0].seenIn).toEqual(['h1', 'obs1']);
38
36
  });
39
37
  it('keeps distinct asset values as separate entries', () => {
40
- const result = buildAssetEntries([makeHit('h1', { ip: ['1.2.3.4'] }), makeHit('h2', { ip: ['5.6.7.8'] })]);
38
+ const result = buildAssetEntries([
39
+ createMockHit({ howler: { id: 'h1' }, related: { ip: ['1.2.3.4'] } }),
40
+ createMockHit({ howler: { id: 'h2' }, related: { ip: ['5.6.7.8'] } })
41
+ ]);
41
42
  expect(result).toHaveLength(2);
42
43
  });
43
44
  it('does not duplicate seenIn ids when the same record appears twice for the same asset', () => {
44
- const result = buildAssetEntries([makeHit('h1', { ip: ['1.2.3.4'] }), makeHit('h1', { ip: ['1.2.3.4'] })]);
45
+ const result = buildAssetEntries([
46
+ createMockHit({ howler: { id: 'h1' }, related: { ip: ['1.2.3.4'] } }),
47
+ createMockHit({ howler: { id: 'h1' }, related: { ip: ['1.2.3.4'] } })
48
+ ]);
45
49
  expect(result[0].seenIn).toEqual(['h1']);
46
50
  });
47
51
  it('skips records with no howler.id', () => {
@@ -49,7 +53,7 @@ describe('buildAssetEntries', () => {
49
53
  expect(buildAssetEntries([noId])).toEqual([]);
50
54
  });
51
55
  it('handles the scalar `id` field on Related', () => {
52
- const result = buildAssetEntries([makeHit('h1', { id: 'some-id' })]);
56
+ const result = buildAssetEntries([createMockHit({ howler: { id: 'h1' }, related: { id: 'some-id' } })]);
53
57
  expect(result).toHaveLength(1);
54
58
  expect(result[0]).toEqual({ type: 'id', value: 'some-id', seenIn: ['h1'] });
55
59
  });
@@ -62,7 +66,7 @@ describe('buildAssetEntries', () => {
62
66
  uri: ['https://example.com'],
63
67
  signature: ['rule-X']
64
68
  };
65
- const result = buildAssetEntries([makeHit('h1', related)]);
69
+ const result = buildAssetEntries([createMockHit({ howler: { id: 'h1' }, related })]);
66
70
  const types = result.map(a => a.type).sort();
67
71
  expect(types).toEqual(['hash', 'hosts', 'ids', 'signature', 'uri', 'user']);
68
72
  });
@@ -80,8 +84,8 @@ vi.mock('../hooks/useCase', () => ({
80
84
  const mockCase = {
81
85
  case_id: 'case-001',
82
86
  items: [
83
- { id: 'hit-1', type: 'hit', path: 'hit-1' },
84
- { id: 'obs-1', type: 'observable', path: 'obs-1' }
87
+ { type: 'hit', value: 'hit-1' },
88
+ { type: 'observable', value: 'obs-1' }
85
89
  ]
86
90
  };
87
91
  const Wrapper = ({ children }) => createElement(MemoryRouter, { initialEntries: ['/cases/case-001/assets'] }, children);
@@ -33,7 +33,10 @@ const CaseDashboard = ({ case: providedCase, caseId }) => {
33
33
  const routeCase = useOutletContext();
34
34
  const { case: _case, updateCase } = useCase({ case: providedCase ?? routeCase, caseId });
35
35
  const [records, setRecords] = useState(null);
36
- const ids = useMemo(() => (_case?.items ?? []).filter(item => ['hit', 'observable'].includes(item.type)).map(item => item.id), [_case?.items]);
36
+ const ids = useMemo(() => (_case?.items ?? [])
37
+ .filter(item => ['hit', 'observable'].includes(item.type))
38
+ .map(item => item.value)
39
+ .filter(val => !!val), [_case?.items]);
37
40
  useEffect(() => {
38
41
  if (ids?.length < 1) {
39
42
  return;
@@ -50,14 +50,14 @@ const ItemPage = ({ case: providedCase }) => {
50
50
  }
51
51
  return;
52
52
  }
53
- if (!matchedNestedCase.id) {
53
+ if (!matchedNestedCase.value) {
54
54
  if (!cancelled) {
55
55
  setItem(null);
56
56
  setLoading(false);
57
57
  }
58
58
  return;
59
59
  }
60
- const nextCase = await dispatchApi(api.v2.case.get(matchedNestedCase.id), { throwError: false });
60
+ const nextCase = await dispatchApi(api.v2.case.get(matchedNestedCase.value), { throwError: false });
61
61
  if (!nextCase) {
62
62
  if (!cancelled) {
63
63
  setItem(null);
@@ -86,13 +86,13 @@ const ItemPage = ({ case: providedCase }) => {
86
86
  return _jsx(NotFoundPage, {});
87
87
  }
88
88
  if (item.type === 'hit') {
89
- return _jsx(InformationPane, { selected: item.id });
89
+ return _jsx(InformationPane, { selected: item.value });
90
90
  }
91
91
  if (item.type === 'observable') {
92
- return _jsx(ObservableViewer, { observableId: item.id });
92
+ return _jsx(ObservableViewer, { observableId: item.value });
93
93
  }
94
94
  if (item.type === 'case') {
95
- return _jsx(CaseDashboard, { caseId: item.id });
95
+ return _jsx(CaseDashboard, { caseId: item.value });
96
96
  }
97
97
  return _jsx("h1", { children: JSON.stringify(item) });
98
98
  };
@@ -13,7 +13,7 @@ const RelatedCasePanel = ({ case: _case }) => {
13
13
  if (!_case) {
14
14
  return _jsx(Skeleton, { height: 240 });
15
15
  }
16
- return (_jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.cases') }), _jsx(Pagination, { count: casePages.length, page: casePage, onChange: (_, page) => setCasePage(page) })] }), _jsx(Divider, {}), casePages[casePage - 1]?.map(item => (_jsxs(Box, { position: "relative", children: [_jsx(CaseCard, { caseId: item.id }), _jsx(Box, { component: Link, to: item.path, sx: {
16
+ return (_jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.cases') }), _jsx(Pagination, { count: casePages.length, page: casePage, onChange: (_, page) => setCasePage(page) })] }), _jsx(Divider, {}), casePages[casePage - 1]?.map(item => (_jsxs(Box, { position: "relative", children: [_jsx(CaseCard, { caseId: item.value }), _jsx(Box, { component: Link, to: item.path, sx: {
17
17
  position: 'absolute',
18
18
  top: 0,
19
19
  left: 0,
@@ -26,6 +26,6 @@ const RelatedCasePanel = ({ case: _case }) => {
26
26
  background: theme.palette.divider,
27
27
  border: `thin solid ${theme.palette.primary.light}`
28
28
  }
29
- } })] }, item.id)))] }));
29
+ } })] }, item.path)))] }));
30
30
  };
31
31
  export default RelatedCasePanel;
@@ -9,7 +9,10 @@ const SourceAggregate = ({ case: providedCase }) => {
9
9
  const { dispatchApi } = useMyApi();
10
10
  const { case: _case } = useCase({ case: providedCase });
11
11
  const [analytics, setAnalytics] = useState([]);
12
- const hitIds = useMemo(() => _case?.items.filter(item => item.type === 'hit').map(item => item.id), [_case?.items]);
12
+ const hitIds = useMemo(() => _case?.items
13
+ .filter(item => item.type === 'hit')
14
+ .map(item => item.value)
15
+ .filter(value => !!value), [_case?.items]);
13
16
  useEffect(() => {
14
17
  dispatchApi(api.v2.search.post('hit', { query: `howler.id:(${hitIds.join(' OR ')})`, fl: 'howler.analytic' }))
15
18
  .then(response => response?.items.map(hit => hit.howler.analytic) ?? [])
@@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
5
5
  const Asset = ({ asset, case: _case }) => {
6
6
  const { t } = useTranslation();
7
7
  return (_jsx(Card, { sx: { height: '100%' }, children: _jsx(CardContent, { children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Chip, { size: "small", label: t(`page.cases.assets.type.${asset.type}`), color: "primary", variant: "outlined" }), _jsx(Typography, { variant: "body2", sx: { wordBreak: 'break-all', fontFamily: 'monospace' }, children: asset.value })] }), asset.seenIn.length > 0 && (_jsxs(Stack, { spacing: 0.5, children: [_jsx(Typography, { variant: "caption", color: "text.secondary", children: t('page.cases.assets.seen_in') }), _jsx(Stack, { direction: "row", flexWrap: "wrap", gap: 0.5, children: asset.seenIn.map(id => {
8
- const entry = _case.items.find(item => item.id === id);
8
+ const entry = _case.items.find(item => item.value === id);
9
9
  return (_jsx(Chip, { clickable: true, size: "small", label: entry.path, variant: "outlined", component: Link, to: `/cases/${_case.case_id}/${entry.path}` }, id));
10
10
  }) })] }))] }) }) }));
11
11
  };
@@ -40,7 +40,7 @@ describe('Asset', () => {
40
40
  });
41
41
  it('renders "Seen in" label when seenIn has entries', () => {
42
42
  const _case = createMockCase({
43
- items: [{ id: 'hit-001', path: 'alerts/test-analytic (hit-001)', type: 'hit', value: 'hit-001' }]
43
+ items: [{ path: 'alerts/test-analytic (hit-001)', type: 'hit', value: 'hit-001' }]
44
44
  });
45
45
  render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ seenIn: ['hit-001'] }), case: _case }) }));
46
46
  expect(screen.getByText('page.cases.assets.seen_in')).toBeTruthy();
@@ -48,9 +48,9 @@ describe('Asset', () => {
48
48
  it('renders a chip labelled with entry.path for each seenIn id', () => {
49
49
  const _case = createMockCase({
50
50
  items: [
51
- { id: 'hit-001', path: 'alerts/my-analytic (hit-001)', type: 'hit', value: 'hit-001' },
52
- { id: 'obs-002', path: 'observables/obs-002', type: 'observable', value: 'obs-002' },
53
- { id: 'hit-003', path: 'alerts/other-analytic (hit-003)', type: 'hit', value: 'hit-003' }
51
+ { path: 'alerts/my-analytic (hit-001)', type: 'hit', value: 'hit-001' },
52
+ { path: 'observables/obs-002', type: 'observable', value: 'obs-002' },
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 }) }));
@@ -61,7 +61,7 @@ describe('Asset', () => {
61
61
  it('links each chip to /cases/:case_id/:path', () => {
62
62
  const _case = createMockCase({
63
63
  case_id: 'case-abc',
64
- items: [{ id: 'hit-001', path: 'alerts/my-analytic (hit-001)', type: 'hit', value: 'hit-001' }]
64
+ items: [{ path: 'alerts/my-analytic (hit-001)', type: 'hit', value: 'hit-001' }]
65
65
  });
66
66
  render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ seenIn: ['hit-001'] }), case: _case }) }));
67
67
  const link = screen.getByText('alerts/my-analytic (hit-001)').closest('a');
@@ -26,10 +26,10 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
26
26
  const [hitMetadata, setHitMetadata] = useState({});
27
27
  const tree = useMemo(() => folder || buildTree(_case?.items), [folder, _case?.items]);
28
28
  const currentRootCaseId = rootCaseId || _case?.case_id;
29
- const hitIds = useMemo(() => tree.leaves
30
- ?.filter(l => l.type?.toLowerCase() === 'hit')
31
- .map(l => l.id)
32
- .filter(Boolean) ?? [], [tree.leaves]);
29
+ const hitIds = useMemo(() => _case?.items
30
+ .filter(item => item.type === 'hit')
31
+ .map(item => item.value)
32
+ .filter(value => !!value), [_case?.items]);
33
33
  useEffect(() => {
34
34
  if (hitIds.length < 1) {
35
35
  return;
@@ -55,16 +55,17 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
55
55
  return undefined;
56
56
  };
57
57
  const toggleCase = useCallback((item, itemKey) => {
58
- const resolvedKey = itemKey || item.path || item.id;
59
- if (!resolvedKey)
58
+ const resolvedKey = itemKey || item.path || item.value;
59
+ if (!resolvedKey) {
60
60
  return;
61
+ }
61
62
  const prev = caseStates[resolvedKey] ?? { open: false, loading: false, data: null };
62
63
  const shouldOpen = !prev.open;
63
- const shouldFetch = shouldOpen && !!item.id && !prev.data && !prev.loading;
64
+ const shouldFetch = shouldOpen && !!item.value && !prev.data && !prev.loading;
64
65
  setCaseStates(current => ({ ...current, [resolvedKey]: { ...prev, open: shouldOpen, loading: shouldFetch } }));
65
66
  if (!shouldFetch)
66
67
  return;
67
- dispatchApi(api.v2.case.get(item.id), { throwError: false })
68
+ dispatchApi(api.v2.case.get(item.value), { throwError: false })
68
69
  .then(caseResponse => {
69
70
  if (!caseResponse)
70
71
  return;
@@ -0,0 +1,7 @@
1
+ import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
+ import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
3
+ import { type FC } from 'react';
4
+ declare const AddToCaseModal: FC<{
5
+ records: (Hit | Observable)[];
6
+ }>;
7
+ export default AddToCaseModal;
@@ -0,0 +1,62 @@
1
+ import { createElement as _createElement } from "react";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Autocomplete, Button, Stack, TextField, Typography } from '@mui/material';
4
+ import api from '@cccsaurora/howler-ui/api';
5
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
6
+ import CaseCard from '@cccsaurora/howler-ui/components/elements/case/CaseCard';
7
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
8
+ import { useContext, useEffect, useMemo, useState } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+ const AddToCaseModal = ({ records }) => {
11
+ const { t } = useTranslation();
12
+ const { dispatchApi } = useMyApi();
13
+ const { close } = useContext(ModalContext);
14
+ const [cases, setCases] = useState([]);
15
+ const [selectedCase, setSelectedCase] = useState(null);
16
+ const [path, setPath] = useState('');
17
+ const [title, setTitle] = useState('');
18
+ useEffect(() => {
19
+ dispatchApi(api.search.case.post({ query: 'case_id:*', rows: 100 }), { throwError: false }).then(result => {
20
+ if (result) {
21
+ setCases(result.items);
22
+ }
23
+ });
24
+ }, [dispatchApi]);
25
+ const folderOptions = useMemo(() => {
26
+ if (!selectedCase?.items) {
27
+ return [];
28
+ }
29
+ const paths = new Set();
30
+ for (const item of selectedCase.items) {
31
+ if (!item.path) {
32
+ continue;
33
+ }
34
+ const parts = item.path.split('/');
35
+ parts.pop();
36
+ for (let i = 1; i <= parts.length; i++) {
37
+ paths.add(parts.slice(0, i).join('/'));
38
+ }
39
+ }
40
+ return Array.from(paths).sort();
41
+ }, [selectedCase]);
42
+ const fullPath = path ? `${path}/${title}` : title;
43
+ const isValid = !!selectedCase && !!title;
44
+ const onSubmit = async () => {
45
+ if (!selectedCase || records?.length < 1) {
46
+ return;
47
+ }
48
+ await dispatchApi(api.v2.case.items.post(selectedCase.case_id, {
49
+ path: fullPath,
50
+ value: records[0].howler.id,
51
+ type: records[0].__index
52
+ }));
53
+ close();
54
+ };
55
+ // TODO: No support currently for multiple records
56
+ return (_jsxs(Stack, { spacing: 2, p: 2, sx: { minWidth: 'min(800px, 60vw)', height: '100%' }, children: [_jsx(Typography, { variant: "h4", children: t('modal.cases.add_to_case') }), _jsx(Autocomplete, { options: cases, getOptionLabel: option => option.title ?? option.case_id ?? '', isOptionEqualToValue: (option, value) => option.case_id === value.case_id, value: selectedCase, disablePortal: true, onChange: (_ev, newVal) => {
57
+ setSelectedCase(newVal);
58
+ setPath('');
59
+ }, renderOption: (props, option) => (_createElement("li", { ...props, key: option.case_id, style: { ...props.style, display: 'flex', justifyContent: 'stretch', alignItems: 'stretch' } },
60
+ _jsx(CaseCard, { case: option, slotProps: { card: { sx: { width: '100%' } } } }))), renderInput: params => (_jsx(TextField, { ...params, size: "small", placeholder: t('modal.cases.add_to_case.select_case'), fullWidth: true })) }), selectedCase && (_jsxs(_Fragment, { children: [_jsx(Autocomplete, { freeSolo: true, disablePortal: true, options: folderOptions, value: path, onInputChange: (_ev, newVal) => setPath(newVal), renderInput: params => (_jsx(TextField, { ...params, size: "small", placeholder: t('modal.cases.add_to_case.select_path'), fullWidth: true })) }), _jsx(TextField, { size: "small", placeholder: t('modal.cases.add_to_case.title'), value: title, onChange: ev => setTitle(ev.target.value), fullWidth: true }), title && (_jsx(Typography, { variant: "caption", color: "textSecondary", children: t('modal.cases.add_to_case.full_path', { path: fullPath }) }))] })), _jsx("div", { style: { flex: 1 } }), _jsxs(Stack, { direction: "row", spacing: 1, alignSelf: "end", children: [_jsx(Button, { variant: "outlined", color: "error", onClick: close, children: t('cancel') }), _jsx(Button, { variant: "outlined", color: "success", disabled: !isValid, onClick: onSubmit, children: t('confirm') })] })] }));
61
+ };
62
+ export default AddToCaseModal;
@@ -24,7 +24,10 @@ const ResolveModal = ({ case: _case, onConfirm }) => {
24
24
  const [rationale, setRationale] = useState('');
25
25
  const [assessment, setAssessment] = useState(null);
26
26
  const [hits, setHits] = useState([]);
27
- const hitIds = useMemo(() => uniq((_case?.items ?? []).filter(item => item.type === 'hit').map(item => item.id)), [_case?.items]);
27
+ const hitIds = useMemo(() => uniq((_case?.items ?? [])
28
+ .filter(item => item.type === 'hit')
29
+ .map(item => item.value)
30
+ .filter(Boolean)), [_case?.items]);
28
31
  const { assess } = useHitActions(hits);
29
32
  useEffect(() => {
30
33
  (async () => {
@@ -312,6 +312,11 @@
312
312
  "modal.action.title": "Save Action",
313
313
  "modal.cases.resolve": "Resolve Case",
314
314
  "modal.cases.resolve.description": "When resolving a case, you must either assess all open alerts, or add an assessment to the alerts.",
315
+ "modal.cases.add_to_case": "Add to Case",
316
+ "modal.cases.add_to_case.select_case": "Search Cases",
317
+ "modal.cases.add_to_case.select_path": "Select Folder Path",
318
+ "modal.cases.add_to_case.title": "Item Title",
319
+ "modal.cases.add_to_case.full_path": "Full path: {{path}}",
315
320
  "modal.confirm.delete.description": "Are you sure you want to delete this item?",
316
321
  "modal.confirm.delete.title": "Confirm Deletion",
317
322
  "modal.rationale.description": "Provide a rationale that succinctly explains to other analysts why you are confident in this assessment.",
@@ -312,6 +312,11 @@
312
312
  "modal.action.title": "Enregistrer l'action",
313
313
  "modal.cases.resolve": "Résoudre le cas",
314
314
  "modal.cases.resolve.description": "Lors de la résolution d'un cas, vous devez soit évaluer toutes les alertes ouvertes, soit ajouter une évaluation aux alertes.",
315
+ "modal.cases.add_to_case": "Ajouter au cas",
316
+ "modal.cases.add_to_case.select_case": "Rechercher des cas",
317
+ "modal.cases.add_to_case.select_path": "Sélectionner le chemin du dossier",
318
+ "modal.cases.add_to_case.title": "Titre de l'élément",
319
+ "modal.cases.add_to_case.full_path": "Chemin complet : {{path}}",
315
320
  "modal.confirm.delete.description": "Êtes-vous sûr de vouloir supprimer cet élément ?",
316
321
  "modal.confirm.delete.title": "Confirmer la suppression",
317
322
  "modal.rationale.description": "Fournissez une justification qui explique succinctement aux autres analystes les raisons pour lesquelles vous êtes confiant dans cette évaluation.",
@@ -2,8 +2,8 @@
2
2
  * NOTE: This is an auto-generated file. Don't edit this manually.
3
3
  */
4
4
  export interface Item {
5
- id?: string;
6
5
  path?: string;
7
6
  type?: string;
8
7
  value?: string;
8
+ visible?: boolean;
9
9
  }
package/package.json CHANGED
@@ -101,183 +101,183 @@
101
101
  "internal-slot": "1.0.7"
102
102
  },
103
103
  "type": "module",
104
- "version": "2.18.0-dev.686",
104
+ "version": "2.18.0-dev.688",
105
105
  "exports": {
106
106
  "./i18n": "./i18n.js",
107
107
  "./index.css": "./index.css",
108
108
  "./components/*": "./components/*.js",
109
- "./branding/*": "./branding/*.js",
110
- "./tests/*": "./tests/*.js",
111
- "./commons/*": "./commons/*.js",
109
+ "./rest/*": "./rest/*.js",
110
+ "./rest": "./rest/index.js",
111
+ "./plugins/*": "./plugins/*.js",
112
+ "./models/*": "./models/*.js",
112
113
  "./utils/*": "./utils/*.js",
113
114
  "./utils/*.json": "./utils/*.json",
115
+ "./branding/*": "./branding/*.js",
116
+ "./tests/*": "./tests/*.js",
114
117
  "./locales/*.json": "./locales/*.json",
118
+ "./commons/*": "./commons/*.js",
115
119
  "./api/*": "./api/*.js",
116
120
  "./api": "./api/index.js",
117
- "./plugins/*": "./plugins/*.js",
118
- "./models/*": "./models/*.js",
119
- "./rest/*": "./rest/*.js",
120
- "./rest": "./rest/index.js",
121
- "./components/logins/*": "./components/logins/*.js",
121
+ "./components/routes/*": "./components/routes/*.js",
122
122
  "./components/app/*": "./components/app/*.js",
123
- "./components/elements/*": "./components/elements/*.js",
124
123
  "./components/hooks/*": "./components/hooks/*.js",
125
- "./components/routes/*": "./components/routes/*.js",
126
- "./components/logins/auth/*": "./components/logins/auth/*.js",
127
- "./components/logins/hooks/*": "./components/logins/hooks/*.js",
128
- "./components/app/drawers/*": "./components/app/drawers/*.js",
129
- "./components/app/providers/*": "./components/app/providers/*.js",
130
- "./components/app/hooks/*": "./components/app/hooks/*.js",
131
- "./components/elements/display/*": "./components/elements/display/*.js",
132
- "./components/elements/observable/*": "./components/elements/observable/*.js",
133
- "./components/elements/hit/*": "./components/elements/hit/*.js",
134
- "./components/elements/record/*": "./components/elements/record/*.js",
135
- "./components/elements/view/*": "./components/elements/view/*.js",
136
- "./components/elements/case/*": "./components/elements/case/*.js",
137
- "./components/elements/addons/*": "./components/elements/addons/*.js",
138
- "./components/elements/display/handlebars/*": "./components/elements/display/handlebars/*.js",
139
- "./components/elements/display/modals/*": "./components/elements/display/modals/*.js",
140
- "./components/elements/display/features/*": "./components/elements/display/features/*.js",
141
- "./components/elements/display/icons/*": "./components/elements/display/icons/*.js",
142
- "./components/elements/display/json/*": "./components/elements/display/json/*.js",
143
- "./components/elements/display/markdownPlugins/*.md": "./components/elements/display/markdownPlugins/*.md.js",
144
- "./components/elements/display/icons/svg/*": "./components/elements/display/icons/svg/*.js",
145
- "./components/elements/hit/actions/*": "./components/elements/hit/actions/*.js",
146
- "./components/elements/hit/related/*": "./components/elements/hit/related/*.js",
147
- "./components/elements/hit/elements/*": "./components/elements/hit/elements/*.js",
148
- "./components/elements/hit/outlines/*": "./components/elements/hit/outlines/*.js",
149
- "./components/elements/hit/aggregate/*": "./components/elements/hit/aggregate/*.js",
150
- "./components/elements/hit/outlines/al/*": "./components/elements/hit/outlines/al/*.js",
151
- "./components/elements/addons/buttons/*": "./components/elements/addons/buttons/*.js",
152
- "./components/elements/addons/buttons": "./components/elements/addons/buttons/index.js",
153
- "./components/elements/addons/lists/*": "./components/elements/addons/lists/*.js",
154
- "./components/elements/addons/lists": "./components/elements/addons/lists/index.js",
155
- "./components/elements/addons/search/*": "./components/elements/addons/search/*.js",
156
- "./components/elements/addons/layout/*": "./components/elements/addons/layout/*.js",
157
- "./components/elements/addons/lists/table/*": "./components/elements/addons/lists/table/*.js",
158
- "./components/elements/addons/lists/table": "./components/elements/addons/lists/table/index.js",
159
- "./components/elements/addons/lists/hooks/*": "./components/elements/addons/lists/hooks/*.js",
160
- "./components/elements/addons/search/phrase/*": "./components/elements/addons/search/phrase/*.js",
161
- "./components/elements/addons/search/phrase": "./components/elements/addons/search/phrase/index.js",
162
- "./components/elements/addons/search/phrase/word/*": "./components/elements/addons/search/phrase/word/*.js",
163
- "./components/elements/addons/search/phrase/word/consumers/*": "./components/elements/addons/search/phrase/word/consumers/*.js",
164
- "./components/elements/addons/layout/vsbox/*": "./components/elements/addons/layout/vsbox/*.js",
165
- "./components/routes/home/*": "./components/routes/home/*.js",
166
- "./components/routes/home": "./components/routes/home/index.js",
124
+ "./components/elements/*": "./components/elements/*.js",
125
+ "./components/logins/*": "./components/logins/*.js",
126
+ "./components/routes/admin/*": "./components/routes/admin/*.js",
127
+ "./components/routes/hits/*": "./components/routes/hits/*.js",
167
128
  "./components/routes/action/*": "./components/routes/action/*.js",
168
- "./components/routes/templates/*": "./components/routes/templates/*.js",
169
129
  "./components/routes/dossiers/*": "./components/routes/dossiers/*.js",
170
- "./components/routes/overviews/*": "./components/routes/overviews/*.js",
171
- "./components/routes/views/*": "./components/routes/views/*.js",
172
- "./components/routes/hits/*": "./components/routes/hits/*.js",
173
- "./components/routes/analytics/*": "./components/routes/analytics/*.js",
174
- "./components/routes/advanced/*": "./components/routes/advanced/*.js",
175
- "./components/routes/help/*": "./components/routes/help/*.js",
176
- "./components/routes/admin/*": "./components/routes/admin/*.js",
177
130
  "./components/routes/settings/*": "./components/routes/settings/*.js",
131
+ "./components/routes/advanced/*": "./components/routes/advanced/*.js",
132
+ "./components/routes/templates/*": "./components/routes/templates/*.js",
133
+ "./components/routes/analytics/*": "./components/routes/analytics/*.js",
178
134
  "./components/routes/observables/*": "./components/routes/observables/*.js",
135
+ "./components/routes/help/*": "./components/routes/help/*.js",
179
136
  "./components/routes/cases/*": "./components/routes/cases/*.js",
180
- "./components/routes/action/edit/*": "./components/routes/action/edit/*.js",
137
+ "./components/routes/overviews/*": "./components/routes/overviews/*.js",
138
+ "./components/routes/home/*": "./components/routes/home/*.js",
139
+ "./components/routes/home": "./components/routes/home/index.js",
140
+ "./components/routes/views/*": "./components/routes/views/*.js",
141
+ "./components/routes/admin/users/*": "./components/routes/admin/users/*.js",
142
+ "./components/routes/hits/view/*": "./components/routes/hits/view/*.js",
143
+ "./components/routes/hits/search/*": "./components/routes/hits/search/*.js",
144
+ "./components/routes/hits/search/shared/*": "./components/routes/hits/search/shared/*.js",
145
+ "./components/routes/hits/search/grid/*": "./components/routes/hits/search/grid/*.js",
181
146
  "./components/routes/action/view/*": "./components/routes/action/view/*.js",
147
+ "./components/routes/action/edit/*": "./components/routes/action/edit/*.js",
182
148
  "./components/routes/action/shared/*": "./components/routes/action/shared/*.js",
183
149
  "./components/routes/action/view/markdown/*.md": "./components/routes/action/view/markdown/*.md.js",
184
- "./components/routes/overviews/template/*": "./components/routes/overviews/template/*.js",
185
- "./components/routes/hits/search/*": "./components/routes/hits/search/*.js",
186
- "./components/routes/hits/view/*": "./components/routes/hits/view/*.js",
187
- "./components/routes/hits/search/grid/*": "./components/routes/hits/search/grid/*.js",
188
- "./components/routes/hits/search/shared/*": "./components/routes/hits/search/shared/*.js",
189
150
  "./components/routes/analytics/widgets/*": "./components/routes/analytics/widgets/*.js",
190
- "./components/routes/help/components/*": "./components/routes/help/components/*.js",
191
151
  "./components/routes/help/markdown/*.md": "./components/routes/help/markdown/*.md.js",
152
+ "./components/routes/help/components/*": "./components/routes/help/components/*.js",
192
153
  "./components/routes/help/markdown/fr/*.md": "./components/routes/help/markdown/fr/*.md.js",
193
154
  "./components/routes/help/markdown/en/*.md": "./components/routes/help/markdown/en/*.md.js",
194
- "./components/routes/admin/users/*": "./components/routes/admin/users/*.js",
195
155
  "./components/routes/cases/modals/*": "./components/routes/cases/modals/*.js",
196
156
  "./components/routes/cases/hooks/*": "./components/routes/cases/hooks/*.js",
197
157
  "./components/routes/cases/detail/*": "./components/routes/cases/detail/*.js",
198
158
  "./components/routes/cases/detail/sidebar/*": "./components/routes/cases/detail/sidebar/*.js",
199
159
  "./components/routes/cases/detail/assets/*": "./components/routes/cases/detail/assets/*.js",
200
160
  "./components/routes/cases/detail/aggregates/*": "./components/routes/cases/detail/aggregates/*.js",
161
+ "./components/routes/overviews/template/*": "./components/routes/overviews/template/*.js",
162
+ "./components/app/hooks/*": "./components/app/hooks/*.js",
163
+ "./components/app/drawers/*": "./components/app/drawers/*.js",
164
+ "./components/app/providers/*": "./components/app/providers/*.js",
165
+ "./components/elements/view/*": "./components/elements/view/*.js",
166
+ "./components/elements/addons/*": "./components/elements/addons/*.js",
167
+ "./components/elements/record/*": "./components/elements/record/*.js",
168
+ "./components/elements/case/*": "./components/elements/case/*.js",
169
+ "./components/elements/display/*": "./components/elements/display/*.js",
170
+ "./components/elements/hit/*": "./components/elements/hit/*.js",
171
+ "./components/elements/observable/*": "./components/elements/observable/*.js",
172
+ "./components/elements/addons/search/*": "./components/elements/addons/search/*.js",
173
+ "./components/elements/addons/lists/*": "./components/elements/addons/lists/*.js",
174
+ "./components/elements/addons/lists": "./components/elements/addons/lists/index.js",
175
+ "./components/elements/addons/buttons/*": "./components/elements/addons/buttons/*.js",
176
+ "./components/elements/addons/buttons": "./components/elements/addons/buttons/index.js",
177
+ "./components/elements/addons/layout/*": "./components/elements/addons/layout/*.js",
178
+ "./components/elements/addons/search/phrase/*": "./components/elements/addons/search/phrase/*.js",
179
+ "./components/elements/addons/search/phrase": "./components/elements/addons/search/phrase/index.js",
180
+ "./components/elements/addons/search/phrase/word/*": "./components/elements/addons/search/phrase/word/*.js",
181
+ "./components/elements/addons/search/phrase/word/consumers/*": "./components/elements/addons/search/phrase/word/consumers/*.js",
182
+ "./components/elements/addons/lists/hooks/*": "./components/elements/addons/lists/hooks/*.js",
183
+ "./components/elements/addons/lists/table/*": "./components/elements/addons/lists/table/*.js",
184
+ "./components/elements/addons/lists/table": "./components/elements/addons/lists/table/index.js",
185
+ "./components/elements/addons/layout/vsbox/*": "./components/elements/addons/layout/vsbox/*.js",
186
+ "./components/elements/display/json/*": "./components/elements/display/json/*.js",
187
+ "./components/elements/display/modals/*": "./components/elements/display/modals/*.js",
188
+ "./components/elements/display/features/*": "./components/elements/display/features/*.js",
189
+ "./components/elements/display/markdownPlugins/*.md": "./components/elements/display/markdownPlugins/*.md.js",
190
+ "./components/elements/display/handlebars/*": "./components/elements/display/handlebars/*.js",
191
+ "./components/elements/display/icons/*": "./components/elements/display/icons/*.js",
192
+ "./components/elements/display/icons/svg/*": "./components/elements/display/icons/svg/*.js",
193
+ "./components/elements/hit/outlines/*": "./components/elements/hit/outlines/*.js",
194
+ "./components/elements/hit/related/*": "./components/elements/hit/related/*.js",
195
+ "./components/elements/hit/actions/*": "./components/elements/hit/actions/*.js",
196
+ "./components/elements/hit/elements/*": "./components/elements/hit/elements/*.js",
197
+ "./components/elements/hit/aggregate/*": "./components/elements/hit/aggregate/*.js",
198
+ "./components/elements/hit/outlines/al/*": "./components/elements/hit/outlines/al/*.js",
199
+ "./components/logins/hooks/*": "./components/logins/hooks/*.js",
200
+ "./components/logins/auth/*": "./components/logins/auth/*.js",
201
+ "./plugins/clue/*": "./plugins/clue/*.js",
202
+ "./plugins/clue": "./plugins/clue/index.js",
203
+ "./plugins/clue/components/*": "./plugins/clue/components/*.js",
204
+ "./plugins/clue/locales/*": "./plugins/clue/locales/*.js",
205
+ "./models/socket/*": "./models/socket/*.js",
206
+ "./models/entities/*": "./models/entities/*.js",
207
+ "./models/entities/generated/*": "./models/entities/generated/*.js",
208
+ "./locales/fr/*.json": "./locales/fr/*.json",
209
+ "./locales/en/*.json": "./locales/en/*.json",
210
+ "./locales/fr/help/*.json": "./locales/fr/help/*.json",
211
+ "./locales/en/help/*.json": "./locales/en/help/*.json",
201
212
  "./commons/components/*": "./commons/components/*.js",
202
213
  "./commons/components/breadcrumbs/*": "./commons/components/breadcrumbs/*.js",
203
- "./commons/components/app/*": "./commons/components/app/*.js",
204
- "./commons/components/utils/*": "./commons/components/utils/*.js",
205
214
  "./commons/components/notification/*": "./commons/components/notification/*.js",
206
215
  "./commons/components/notification": "./commons/components/notification/index.js",
216
+ "./commons/components/topnav/*": "./commons/components/topnav/*.js",
217
+ "./commons/components/app/*": "./commons/components/app/*.js",
207
218
  "./commons/components/display/*": "./commons/components/display/*.js",
208
219
  "./commons/components/leftnav/*": "./commons/components/leftnav/*.js",
209
- "./commons/components/search/*": "./commons/components/search/*.js",
210
220
  "./commons/components/pages/*": "./commons/components/pages/*.js",
211
- "./commons/components/topnav/*": "./commons/components/topnav/*.js",
212
- "./commons/components/app/providers/*": "./commons/components/app/providers/*.js",
213
- "./commons/components/app/hooks/*": "./commons/components/app/hooks/*.js",
214
- "./commons/components/app/hooks": "./commons/components/app/hooks/index.js",
215
- "./commons/components/utils/hooks/*": "./commons/components/utils/hooks/*.js",
221
+ "./commons/components/utils/*": "./commons/components/utils/*.js",
222
+ "./commons/components/search/*": "./commons/components/search/*.js",
216
223
  "./commons/components/notification/elements/*": "./commons/components/notification/elements/*.js",
217
224
  "./commons/components/notification/elements/item/*": "./commons/components/notification/elements/item/*.js",
225
+ "./commons/components/app/hooks/*": "./commons/components/app/hooks/*.js",
226
+ "./commons/components/app/hooks": "./commons/components/app/hooks/index.js",
227
+ "./commons/components/app/providers/*": "./commons/components/app/providers/*.js",
218
228
  "./commons/components/display/hooks/*": "./commons/components/display/hooks/*.js",
219
229
  "./commons/components/pages/hooks/*": "./commons/components/pages/hooks/*.js",
220
- "./locales/fr/*.json": "./locales/fr/*.json",
221
- "./locales/en/*.json": "./locales/en/*.json",
222
- "./locales/fr/help/*.json": "./locales/fr/help/*.json",
223
- "./locales/en/help/*.json": "./locales/en/help/*.json",
224
- "./api/overview/*": "./api/overview/*.js",
225
- "./api/overview": "./api/overview/index.js",
226
- "./api/v2/*": "./api/v2/*.js",
227
- "./api/v2": "./api/v2/index.js",
230
+ "./commons/components/utils/hooks/*": "./commons/components/utils/hooks/*.js",
228
231
  "./api/action/*": "./api/action/*.js",
229
232
  "./api/action": "./api/action/index.js",
230
- "./api/auth/*": "./api/auth/*.js",
231
- "./api/auth": "./api/auth/index.js",
233
+ "./api/v2/*": "./api/v2/*.js",
234
+ "./api/v2": "./api/v2/index.js",
235
+ "./api/view/*": "./api/view/*.js",
236
+ "./api/view": "./api/view/index.js",
232
237
  "./api/notebook/*": "./api/notebook/*.js",
233
238
  "./api/notebook": "./api/notebook/index.js",
234
- "./api/template/*": "./api/template/*.js",
235
- "./api/template": "./api/template/index.js",
236
239
  "./api/analytic/*": "./api/analytic/*.js",
237
240
  "./api/analytic": "./api/analytic/index.js",
241
+ "./api/hit/*": "./api/hit/*.js",
242
+ "./api/hit": "./api/hit/index.js",
243
+ "./api/overview/*": "./api/overview/*.js",
244
+ "./api/overview": "./api/overview/index.js",
245
+ "./api/configs/*": "./api/configs/*.js",
246
+ "./api/configs": "./api/configs/index.js",
238
247
  "./api/user/*": "./api/user/*.js",
239
248
  "./api/user": "./api/user/index.js",
240
249
  "./api/dossier/*": "./api/dossier/*.js",
241
250
  "./api/dossier": "./api/dossier/index.js",
251
+ "./api/auth/*": "./api/auth/*.js",
252
+ "./api/auth": "./api/auth/index.js",
242
253
  "./api/search/*": "./api/search/*.js",
243
254
  "./api/search": "./api/search/index.js",
244
- "./api/configs/*": "./api/configs/*.js",
245
- "./api/configs": "./api/configs/index.js",
246
- "./api/hit/*": "./api/hit/*.js",
247
- "./api/hit": "./api/hit/index.js",
248
- "./api/view/*": "./api/view/*.js",
249
- "./api/view": "./api/view/index.js",
250
- "./api/v2/search/*": "./api/v2/search/*.js",
251
- "./api/v2/search": "./api/v2/search/index.js",
255
+ "./api/template/*": "./api/template/*.js",
256
+ "./api/template": "./api/template/index.js",
252
257
  "./api/v2/case/*": "./api/v2/case/*.js",
253
258
  "./api/v2/case": "./api/v2/case/index.js",
254
- "./api/analytic/comments/*": "./api/analytic/comments/*.js",
255
- "./api/analytic/comments": "./api/analytic/comments/index.js",
259
+ "./api/v2/search/*": "./api/v2/search/*.js",
260
+ "./api/v2/search": "./api/v2/search/index.js",
256
261
  "./api/analytic/notebooks/*": "./api/analytic/notebooks/*.js",
257
262
  "./api/analytic/notebooks": "./api/analytic/notebooks/index.js",
263
+ "./api/analytic/comments/*": "./api/analytic/comments/*.js",
264
+ "./api/analytic/comments": "./api/analytic/comments/index.js",
265
+ "./api/hit/comments/*": "./api/hit/comments/*.js",
266
+ "./api/hit/comments": "./api/hit/comments/index.js",
258
267
  "./api/user/avatar/*": "./api/user/avatar/*.js",
259
268
  "./api/user/avatar": "./api/user/avatar/index.js",
260
- "./api/search/eql/*": "./api/search/eql/*.js",
261
269
  "./api/search/histogram/*": "./api/search/histogram/*.js",
262
270
  "./api/search/histogram": "./api/search/histogram/index.js",
263
271
  "./api/search/facet/*": "./api/search/facet/*.js",
264
272
  "./api/search/facet": "./api/search/facet/index.js",
265
- "./api/search/fields/*": "./api/search/fields/*.js",
266
- "./api/search/fields": "./api/search/fields/index.js",
273
+ "./api/search/eql/*": "./api/search/eql/*.js",
274
+ "./api/search/count/*": "./api/search/count/*.js",
275
+ "./api/search/count": "./api/search/count/index.js",
267
276
  "./api/search/grouped/*": "./api/search/grouped/*.js",
268
277
  "./api/search/grouped": "./api/search/grouped/index.js",
269
- "./api/search/sigma/*": "./api/search/sigma/*.js",
270
278
  "./api/search/explain/*": "./api/search/explain/*.js",
271
- "./api/search/count/*": "./api/search/count/*.js",
272
- "./api/search/count": "./api/search/count/index.js",
273
- "./api/hit/comments/*": "./api/hit/comments/*.js",
274
- "./api/hit/comments": "./api/hit/comments/index.js",
275
- "./plugins/clue/*": "./plugins/clue/*.js",
276
- "./plugins/clue": "./plugins/clue/index.js",
277
- "./plugins/clue/components/*": "./plugins/clue/components/*.js",
278
- "./plugins/clue/locales/*": "./plugins/clue/locales/*.js",
279
- "./models/socket/*": "./models/socket/*.js",
280
- "./models/entities/*": "./models/entities/*.js",
281
- "./models/entities/generated/*": "./models/entities/generated/*.js"
279
+ "./api/search/fields/*": "./api/search/fields/*.js",
280
+ "./api/search/fields": "./api/search/fields/index.js",
281
+ "./api/search/sigma/*": "./api/search/sigma/*.js"
282
282
  }
283
283
  }
@@ -30,7 +30,12 @@ export const MOCK_RESPONSES = {
30
30
  total: 1,
31
31
  rows: 1
32
32
  },
33
- '/api/v1/analytic': [createMockAnalytic()]
33
+ '/api/v1/analytic': [createMockAnalytic()],
34
+ '/api/v2/search/hit,observable': {
35
+ items: [],
36
+ total: 0,
37
+ rows: 0
38
+ }
34
39
  };
35
40
  const handlers = [
36
41
  ...Object.entries(MOCK_RESPONSES).map(([path, data]) => http.all(path, async () => HttpResponse.json({ api_response: data }))),
package/tests/utils.d.ts CHANGED
@@ -3,12 +3,14 @@ import type { Analytic } from '@cccsaurora/howler-ui/models/entities/generated/A
3
3
  import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
4
4
  import type { Dossier } from '@cccsaurora/howler-ui/models/entities/generated/Dossier';
5
5
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
6
+ import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
6
7
  import type { Template } from '@cccsaurora/howler-ui/models/entities/generated/Template';
7
8
  import type { View } from '@cccsaurora/howler-ui/models/entities/generated/View';
8
9
  type RecursivePartial<T> = {
9
10
  [P in keyof T]?: T[P] extends (infer U)[] ? RecursivePartial<U>[] : T[P] extends object | undefined ? RecursivePartial<T[P]> : T[P];
10
11
  };
11
12
  export declare const createMockHit: (overrides?: RecursivePartial<Hit>) => Hit;
13
+ export declare const createMockObservable: (overrides?: RecursivePartial<Observable>) => Observable;
12
14
  export declare const createMockAnalytic: (overrides?: Partial<Analytic>) => Analytic;
13
15
  export declare const createMockTemplate: (overrides?: Partial<Template>) => Template;
14
16
  export declare const createMockAction: (overrides?: Partial<Action>) => Action;
package/tests/utils.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Mock data factories
2
2
  export const createMockHit = (overrides) => ({
3
+ ...overrides,
3
4
  __index: 'hit',
4
5
  howler: {
5
6
  id: 'test-hit-1',
@@ -15,6 +16,17 @@ export const createMockHit = (overrides) => ({
15
16
  ...overrides?.event
16
17
  }
17
18
  });
19
+ export const createMockObservable = (overrides) => ({
20
+ ...overrides,
21
+ __index: 'observable',
22
+ howler: {
23
+ id: 'test-observable-1',
24
+ analytic: 'test-analytic',
25
+ detection: 'Test Detection',
26
+ hash: '',
27
+ ...overrides?.howler
28
+ }
29
+ });
18
30
  export const createMockAnalytic = (overrides) => ({
19
31
  analytic_id: 'test-analytic-id',
20
32
  name: 'test-analytic',