@cccsaurora/howler-ui 2.17.0-dev.420 → 2.17.0-dev.470

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 (57) hide show
  1. package/commons/components/app/hooks/useAppConfigs.d.ts +1 -1
  2. package/components/app/providers/HitSearchProvider.d.ts +0 -1
  3. package/components/app/providers/HitSearchProvider.js +4 -6
  4. package/components/app/providers/HitSearchProvider.test.js +1 -1
  5. package/components/app/providers/ParameterProvider.js +3 -3
  6. package/components/app/providers/ViewProvider.d.ts +1 -1
  7. package/components/app/providers/ViewProvider.js +3 -6
  8. package/components/app/providers/ViewProvider.test.js +1 -1
  9. package/components/elements/PluginChip.d.ts +2 -0
  10. package/components/elements/PluginChip.js +2 -1
  11. package/components/elements/PluginTypography.d.ts +4 -3
  12. package/components/elements/PluginTypography.js +4 -3
  13. package/components/elements/display/modals/RationaleModal.js +1 -1
  14. package/components/elements/display/modals/RationaleModal.test.js +1 -1
  15. package/components/elements/hit/HitBanner.js +2 -2
  16. package/components/elements/hit/HitDetails.js +9 -9
  17. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  18. package/components/routes/hits/search/ViewLink.js +1 -1
  19. package/components/routes/hits/search/ViewLink.test.js +3 -3
  20. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -0
  21. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  22. package/components/routes/hits/search/grid/HitRow.js +1 -1
  23. package/components/routes/views/ViewComposer.js +2 -2
  24. package/index.js +5 -0
  25. package/locales/en/translation.json +1 -0
  26. package/locales/fr/translation.json +1 -0
  27. package/models/entities/generated/Analytic.d.ts +2 -2
  28. package/models/entities/generated/ApiType.d.ts +2 -1
  29. package/models/entities/generated/Clue.d.ts +8 -0
  30. package/models/entities/generated/Hit.d.ts +2 -20
  31. package/models/entities/generated/Labels.d.ts +1 -0
  32. package/models/entities/generated/Type.d.ts +7 -0
  33. package/package.json +24 -15
  34. package/plugins/HowlerPlugin.js +1 -0
  35. package/plugins/clue/Provider.d.ts +3 -0
  36. package/plugins/clue/Provider.js +13 -0
  37. package/plugins/clue/components/ClueChip.d.ts +3 -0
  38. package/plugins/clue/components/ClueChip.js +29 -0
  39. package/plugins/clue/components/ClueLeadForm.d.ts +4 -0
  40. package/plugins/clue/components/ClueLeadForm.js +24 -0
  41. package/plugins/clue/components/CluePivot.d.ts +3 -0
  42. package/plugins/clue/components/CluePivot.js +145 -0
  43. package/plugins/clue/components/CluePivotForm.d.ts +21 -0
  44. package/plugins/clue/components/CluePivotForm.js +270 -0
  45. package/plugins/clue/components/ClueTypography.d.ts +3 -0
  46. package/plugins/clue/components/ClueTypography.js +53 -0
  47. package/plugins/clue/helpers.d.ts +3 -0
  48. package/plugins/clue/helpers.js +196 -0
  49. package/plugins/clue/index.d.ts +21 -0
  50. package/plugins/clue/index.js +66 -0
  51. package/plugins/clue/locales/clue.en.json +8 -0
  52. package/plugins/clue/locales/clue.fr.json +8 -0
  53. package/plugins/clue/setup.d.ts +2 -0
  54. package/plugins/clue/setup.js +46 -0
  55. package/plugins/clue/utils.d.ts +2 -0
  56. package/plugins/clue/utils.js +19 -0
  57. package/plugins/store.js +3 -0
@@ -24,7 +24,7 @@ export declare function useAppConfigs(): {
24
24
  hideNestedIcons?: boolean;
25
25
  };
26
26
  appName: string;
27
- appLink?: import("react-router-dom").To;
27
+ appLink?: import("react-router").To;
28
28
  appIconDark: import("react").ReactElement<any>;
29
29
  appIconLight: import("react").ReactElement<any>;
30
30
  bannerDark?: import("react").ReactElement<any>;
@@ -22,5 +22,4 @@ export interface HitSearchContextType {
22
22
  }
23
23
  export declare const HitSearchContext: import("use-context-selector").Context<HitSearchContextType>;
24
24
  declare const HitSearchProvider: FC<PropsWithChildren>;
25
- export declare const useHitSearchContextSelector: <Selected>(selector: (value: HitSearchContextType) => Selected) => Selected;
26
25
  export default HitSearchProvider;
@@ -77,7 +77,7 @@ const HitSearchProvider = ({ children }) => {
77
77
  }
78
78
  // Fetch all view queries
79
79
  if (views.length > 0) {
80
- const viewObjects = await getCurrentViews();
80
+ const viewObjects = await getCurrentViews({ views });
81
81
  // Filter out null/undefined views and extract queries
82
82
  viewObjects
83
83
  .filter(view => view?.query)
@@ -157,7 +157,8 @@ const HitSearchProvider = ({ children }) => {
157
157
  trackTotalHits,
158
158
  loadHits,
159
159
  getCurrentViews,
160
- setOffset
160
+ setOffset,
161
+ getFilters
161
162
  ]);
162
163
  // We only run this when ancillary properties (i.e. filters, sorting) change
163
164
  useEffect(() => {
@@ -171,7 +172,7 @@ const HitSearchProvider = ({ children }) => {
171
172
  setResponse(null);
172
173
  }
173
174
  // eslint-disable-next-line react-hooks/exhaustive-deps
174
- }, [offset, pageCount, sort, span, bundleId, location.pathname, startDate, endDate, filters, query]);
175
+ }, [offset, pageCount, sort, span, bundleId, location.pathname, startDate, endDate, filters, query, views]);
175
176
  return (_jsx(HitSearchContext.Provider, { value: {
176
177
  displayType,
177
178
  setDisplayType,
@@ -187,7 +188,4 @@ const HitSearchProvider = ({ children }) => {
187
188
  setFzfSearch
188
189
  }, children: children }));
189
190
  };
190
- export const useHitSearchContextSelector = (selector) => {
191
- return useContextSelector(HitSearchContext, selector);
192
- };
193
191
  export default HitSearchProvider;
@@ -17,7 +17,7 @@ const mockSetParams = vi.fn();
17
17
  const mockParams = vi.mocked(useParams);
18
18
  const mockLocation = vi.mocked(useLocation());
19
19
  const mockViewContext = {
20
- getCurrentViews: ({ viewId } = {}) => Promise.resolve([{ view_id: viewId || 'test_view_id', query: 'howler.id:*' }])
20
+ getCurrentViews: ({ views } = {}) => Promise.resolve([{ view_id: views?.[0] || 'test_view_id', query: 'howler.id:*' }])
21
21
  };
22
22
  let mockParameterContext = {
23
23
  filters: [],
@@ -81,8 +81,8 @@ const ParameterProvider = ({ children }) => {
81
81
  if (value === values[key]) {
82
82
  return;
83
83
  }
84
- if (key === 'selected' && !value) {
85
- pendingChanges.current.selected = getSelectedValue(params, location.pathname, routeParams.id);
84
+ if (key === 'selected') {
85
+ pendingChanges.current.selected = value;
86
86
  }
87
87
  else {
88
88
  pendingChanges.current[key] = value ?? DEFAULT_VALUES[key] ?? null;
@@ -95,7 +95,7 @@ const ParameterProvider = ({ children }) => {
95
95
  _setValues(_current => ({ ..._current, ...pendingChanges.current }));
96
96
  pendingChanges.current = {};
97
97
  });
98
- }, [location.pathname, routeParams.id, values, params]);
98
+ }, [values]);
99
99
  const setOffset = useCallback(_offset => _setValues(_current => ({ ..._current, offset: parseOffset(_offset) })), []);
100
100
  const setCustomSpan = useCallback((startDate, endDate) => {
101
101
  _setValues(_values => ({
@@ -13,7 +13,7 @@ export interface ViewContextType {
13
13
  editView: (id: string, newView: Partial<Omit<View, 'view_id' | 'owner'>>) => Promise<View>;
14
14
  removeView: (id: string) => Promise<void>;
15
15
  getCurrentViews: (config?: {
16
- viewId?: string;
16
+ views?: string[];
17
17
  lazy?: boolean;
18
18
  ignoreParams?: boolean;
19
19
  }) => Promise<View[]>;
@@ -3,7 +3,7 @@ import api from '@cccsaurora/howler-ui/api';
3
3
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
4
4
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
5
5
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
6
- import { has, omit } from 'lodash-es';
6
+ import { has, omit, uniq } from 'lodash-es';
7
7
  import { useCallback, useEffect, useState } from 'react';
8
8
  import { useSearchParams } from 'react-router-dom';
9
9
  import { createContext, useContextSelector } from 'use-context-selector';
@@ -59,11 +59,8 @@ const ViewProvider = ({ children }) => {
59
59
  }
60
60
  })();
61
61
  }, [defaultView, fetchViews, setDefaultView, views]);
62
- const getCurrentViews = useCallback(async ({ viewId, lazy = false, ignoreParams = false } = {}) => {
63
- const currentViews = ignoreParams ? [] : searchParams.getAll('view');
64
- if (viewId && !currentViews.includes(viewId)) {
65
- currentViews.push(viewId);
66
- }
62
+ const getCurrentViews = useCallback(async ({ views: _views, lazy = false, ignoreParams = false } = {}) => {
63
+ const currentViews = uniq([...(_views ?? []), ...(ignoreParams ? [] : searchParams.getAll('view'))]);
67
64
  if (currentViews.length < 1) {
68
65
  return [];
69
66
  }
@@ -135,7 +135,7 @@ describe('ViewContext', () => {
135
135
  it('should allow the user to fetch their current view based on the view ID', async () => {
136
136
  // lazy load should return nothing
137
137
  await expect(hook.result.current({ lazy: true })).resolves.toEqual([]);
138
- const result = await act(async () => hook.result.current({ viewId: 'searched_view_id' }));
138
+ const result = await act(async () => hook.result.current({ views: ['searched_view_id'] }));
139
139
  expect(result).toEqual([MOCK_RESPONSES['/api/v1/search/view'].items[0]]);
140
140
  });
141
141
  });
@@ -1,9 +1,11 @@
1
1
  import { type ChipProps } from '@mui/material';
2
+ import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
3
  import { type FC } from 'react';
3
4
  export type PluginChipProps = ChipProps & {
4
5
  value: string;
5
6
  context: string;
6
7
  field?: string;
8
+ hit?: Hit;
7
9
  };
8
10
  declare const PluginChip: FC<PluginChipProps>;
9
11
  export default PluginChip;
@@ -3,7 +3,7 @@ import { Chip } from '@mui/material';
3
3
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
4
4
  import {} from 'react';
5
5
  import { usePluginStore } from 'react-pluggable';
6
- const PluginChip = ({ children, value, context, field, ...props }) => {
6
+ const PluginChip = ({ children, value, context, field, hit, ...props }) => {
7
7
  const pluginStore = usePluginStore();
8
8
  for (const plugin of howlerPluginStore.plugins) {
9
9
  const component = pluginStore.executeFunction(`${plugin}.chip`, {
@@ -11,6 +11,7 @@ const PluginChip = ({ children, value, context, field, ...props }) => {
11
11
  value,
12
12
  context,
13
13
  field,
14
+ hit,
14
15
  ...props
15
16
  });
16
17
  if (component) {
@@ -1,9 +1,10 @@
1
1
  import { type TypographyProps } from '@mui/material';
2
- import { type FC } from 'react';
2
+ import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
3
3
  export type PluginTypographyProps = TypographyProps & {
4
4
  value: string;
5
5
  context: string;
6
6
  field?: string;
7
+ hit?: Hit;
7
8
  };
8
- declare const PluginTypography: FC<PluginTypographyProps>;
9
- export default PluginTypography;
9
+ declare const _default: import("react").NamedExoticComponent<PluginTypographyProps>;
10
+ export default _default;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Typography } from '@mui/material';
3
3
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
4
- import {} from 'react';
4
+ import { memo } from 'react';
5
5
  import { usePluginStore } from 'react-pluggable';
6
- const PluginTypography = ({ children, value, context, field, ...props }) => {
6
+ const PluginTypography = ({ children, value, context, field, hit, ...props }) => {
7
7
  const pluginStore = usePluginStore();
8
8
  for (const plugin of howlerPluginStore.plugins) {
9
9
  const component = pluginStore.executeFunction(`${plugin}.typography`, {
@@ -11,6 +11,7 @@ const PluginTypography = ({ children, value, context, field, ...props }) => {
11
11
  value,
12
12
  context,
13
13
  field,
14
+ hit,
14
15
  ...props
15
16
  });
16
17
  if (component) {
@@ -19,4 +20,4 @@ const PluginTypography = ({ children, value, context, field, ...props }) => {
19
20
  }
20
21
  return _jsx(Typography, { ...props, children: children ?? value });
21
22
  };
22
- export default PluginTypography;
23
+ export default memo(PluginTypography);
@@ -80,7 +80,7 @@ const RationaleModal = ({ hits, onSubmit }) => {
80
80
  query: 'howler.rationale:*',
81
81
  rows: 10,
82
82
  fields: ['howler.rationale'],
83
- filters: hits.map(hit => `howler.analytic:"${sanitizeLuceneQuery(hit.howler.analytic)}")`)
83
+ filters: hits.map(hit => `howler.analytic:"${sanitizeLuceneQuery(hit.howler.analytic)}"`)
84
84
  }, 'analytic'),
85
85
  // Rationales provided by this user
86
86
  runFacet({
@@ -119,7 +119,7 @@ describe('RationaleModal', () => {
119
119
  });
120
120
  expect(mockHpost).toHaveBeenCalledWith('/api/v1/search/facet/hit', {
121
121
  fields: ['howler.rationale'],
122
- filters: ['howler.analytic:"test\\-analytic\\-1")'],
122
+ filters: ['howler.analytic:"test\\-analytic\\-1"'],
123
123
  query: 'howler.rationale:*',
124
124
  rows: 10
125
125
  });
@@ -88,9 +88,9 @@ const HitBanner = ({ hit, layout = HitLayout.NORMAL, showAssigned = true }) => {
88
88
  const _children = (_jsxs(Stack, { direction: "row", spacing: 1, flex: 1, children: [_jsxs(Typography, { variant: textVariant, noWrap: compressed, textOverflow: compressed ? 'ellipsis' : 'wrap', ...typographyProps, sx: [
89
89
  { display: 'flex', flexDirection: 'row' },
90
90
  ...(Array.isArray(typographyProps?.sx) ? typographyProps?.sx : [typographyProps?.sx])
91
- ], children: [t(i18nKey), ":"] }), (Array.isArray(value) ? value : [value]).map(val => (_jsx(PluginTypography, { component: "span", context: "banner", variant: textVariant, noWrap: compressed, textOverflow: compressed ? 'ellipsis' : 'wrap', ...typographyProps, value: val, field: field }, val)))] }));
91
+ ], children: [t(i18nKey), ":"] }), (Array.isArray(value) ? value : [value]).map(val => (_jsx(PluginTypography, { component: "span", context: "banner", variant: textVariant, noWrap: compressed, textOverflow: compressed ? 'ellipsis' : 'wrap', ...typographyProps, value: val, field: field, hit: hit }, val)))] }));
92
92
  return compressed ? (_jsx(Tooltip, { title: Array.isArray(value) ? (_jsx("div", { children: value.map(_indicator => (_jsx("p", { style: { margin: 0, padding: 0 }, children: _indicator }, _indicator))) })) : (value), children: _children })) : (_children);
93
- }, [compressed, t, textVariant]);
93
+ }, [compressed, hit, t, textVariant]);
94
94
  return (_jsxs(Box, { display: "grid", gridTemplateColumns: "minmax(0, auto) minmax(0, 1fr) minmax(0, auto)", alignItems: "stretch", sx: { width: '100%', ml: 0, overflow: 'hidden' }, children: [leftBox, _jsxs(Stack, { sx: {
95
95
  height: '100%',
96
96
  padding: theme.spacing(1),
@@ -10,7 +10,7 @@ import { memo, useEffect, useMemo, useState } from 'react';
10
10
  import { useTranslation } from 'react-i18next';
11
11
  import Throttler from '@cccsaurora/howler-ui/utils/Throttler';
12
12
  import PluginTypography from '../PluginTypography';
13
- const ListRenderer = memo(({ objKey: key, entries, maxKeyLength }) => {
13
+ const ListRenderer = memo(({ hit, objKey: key, entries, maxKeyLength }) => {
14
14
  const theme = useTheme();
15
15
  const { t } = useTranslation();
16
16
  const allPrimitives = useMemo(() => entries.every(entry => !isObject(entry)), [entries]);
@@ -36,15 +36,15 @@ const ListRenderer = memo(({ objKey: key, entries, maxKeyLength }) => {
36
36
  marginBottom: allPrimitives ? 0 : theme.spacing(1)
37
37
  }, children: allPrimitives ? key.padStart(maxKeyLength ?? key.length) : key }) }), _jsxs(Grid, { container: true, spacing: allPrimitives ? 1 : 4, ml: allPrimitives ? -1 : -4, overflow: "hidden", maxWidth: "100%", children: [uniqueEntries.map((entry, index) => {
38
38
  if (Array.isArray(entry)) {
39
- return (_jsx(Grid, { item: true, xs: "auto", maxWidth: "100%", children: _jsx(ListRenderer, { objKey: `${key}.${index}`, entries: entry }) }, index));
39
+ return (_jsx(Grid, { item: true, xs: "auto", maxWidth: "100%", children: _jsx(ListRenderer, { hit: hit, objKey: `${key}.${index}`, entries: entry }) }, index));
40
40
  }
41
41
  if (isPlainObject(entry)) {
42
42
  return (_jsx(Grid, { item: true, xs: 'auto', maxWidth: "100%", minWidth: "350px", children: _jsx(ObjectRenderer, { parentKey: `${key}.${index}`, indent: true, data: entry }) }, index));
43
43
  }
44
- return (_jsxs(Grid, { item: true, maxWidth: "100%", className: `${key}_${index}`.replace(/\./g, '_'), component: "code", display: "flex", flexDirection: "row", children: [_jsx(PluginTypography, { context: "details", component: "code", style: { maxWidth: '100%', font: 'inherit' }, value: entry, field: key.replace(/\.[0-9]+/g, ''), children: entry }), allPrimitives && index < uniqueEntries.length - 1 && _jsx("span", { children: "," })] }, entry));
44
+ return (_jsxs(Grid, { item: true, maxWidth: "100%", className: `${key}_${index}`.replace(/\./g, '_'), component: "code", display: "flex", flexDirection: "row", children: [_jsx(PluginTypography, { context: "details", component: "code", style: { maxWidth: '100%', font: 'inherit' }, value: entry, field: key.replace(/\.[0-9]+/g, ''), hit: hit, children: entry }), allPrimitives && index < uniqueEntries.length - 1 && _jsx("span", { children: "," })] }, entry));
45
45
  }), omittedDuplicates && (_jsx(Grid, { item: true, display: "flex", alignItems: "center", children: _jsx(Tooltip, { title: t('duplicates.omitted'), children: _jsx(InfoOutlined, { sx: { fontSize: '20px', ml: 1 }, color: "disabled" }) }) }))] })] }));
46
46
  });
47
- const ObjectRenderer = memo(({ data, parentKey, indent = false }) => {
47
+ const ObjectRenderer = memo(({ hit, data, parentKey, indent = false }) => {
48
48
  const theme = useTheme();
49
49
  const entries = useMemo(() => {
50
50
  const unsorted = Object.entries(flatten(data, { safe: true })).map(([key, val]) => [key, val]);
@@ -59,7 +59,7 @@ const ObjectRenderer = memo(({ data, parentKey, indent = false }) => {
59
59
  .filter(([__, val]) => !isNull(val) && !isUndefined(val) && !isEmpty(val))
60
60
  .map(([key, val]) => {
61
61
  if (Array.isArray(val)) {
62
- return _jsx(ListRenderer, { maxKeyLength: longestKey, objKey: key, entries: val }, key);
62
+ return _jsx(ListRenderer, { hit: hit, maxKeyLength: longestKey, objKey: key, entries: val }, key);
63
63
  }
64
64
  return (_jsxs("code", { className: (parentKey ? `${parentKey}.${key}` : key).replace(/\./g, '_'), style: {
65
65
  display: 'grid',
@@ -75,10 +75,10 @@ const ObjectRenderer = memo(({ data, parentKey, indent = false }) => {
75
75
  paddingRight: theme.spacing(1),
76
76
  height: '100%',
77
77
  wordWrap: 'break-word'
78
- }, children: _jsx("code", { style: { maxWidth: '100%' }, children: key }) }), _jsx(Box, { display: "flex", alignItems: "start", children: _jsx(PluginTypography, { context: "details", component: "code", style: { maxWidth: '100%', font: 'inherit' }, value: val, field: (parentKey ? parentKey.concat('.', key) : key).replace(/\.[0-9]+/g, ''), children: val }) })] }, key));
78
+ }, children: _jsx("code", { style: { maxWidth: '100%' }, children: key }) }), _jsx(Box, { display: "flex", alignItems: "start", children: _jsx(PluginTypography, { context: "details", component: "code", style: { maxWidth: '100%', font: 'inherit' }, value: val, field: (parentKey ? parentKey.concat('.', key) : key).replace(/\.[0-9]+/g, ''), hit: hit, children: val }) })] }, key));
79
79
  }) })] }));
80
80
  });
81
- const Collapsible = memo(({ title, data, query }) => {
81
+ const Collapsible = memo(({ hit, title, data, query }) => {
82
82
  const throttler = useMemo(() => new Throttler(400), []);
83
83
  const [scores, setScores] = useState([]);
84
84
  const [results, setResults] = useState({});
@@ -109,7 +109,7 @@ const Collapsible = memo(({ title, data, query }) => {
109
109
  if (isEmpty(results)) {
110
110
  return null;
111
111
  }
112
- return (_jsxs(Accordion, { defaultExpanded: true, children: [_jsx(AccordionSummary, { expandIcon: _jsx(ArrowDropDown, {}), children: _jsx(Typography, { children: title }) }), _jsx(AccordionDetails, { children: _jsx(Stack, { spacing: 1, justifyContent: "stretch", sx: styles, children: _jsx(ObjectRenderer, { showParentKey: true, data: results }) }) })] }));
112
+ return (_jsxs(Accordion, { defaultExpanded: true, children: [_jsx(AccordionSummary, { expandIcon: _jsx(ArrowDropDown, {}), children: _jsx(Typography, { children: title }) }), _jsx(AccordionDetails, { children: _jsx(Stack, { spacing: 1, justifyContent: "stretch", sx: styles, children: _jsx(ObjectRenderer, { hit: hit, showParentKey: true, data: results }) }) })] }));
113
113
  });
114
114
  const HitDetails = ({ hit }) => {
115
115
  const { t } = useTranslation();
@@ -119,7 +119,7 @@ const HitDetails = ({ hit }) => {
119
119
  ['howler', 'labels'].every(prefix => !key.startsWith(prefix)) &&
120
120
  !isEmpty(value)), ([key]) => key.split('.')[0]), [hit]);
121
121
  return (_jsxs(Stack, { spacing: 1, children: [_jsx(TextField, { value: query, onChange: event => setQuery(event.target.value), label: t('overview.search') }), Object.entries(groups).map(([section, entries]) => {
122
- return (_jsx(Collapsible, { query: query, title: section
122
+ return (_jsx(Collapsible, { hit: hit, query: query, title: section
123
123
  .split('_')
124
124
  .map(word => capitalize(word))
125
125
  .join(' '), data: Object.fromEntries(entries) }, section));
@@ -41,7 +41,7 @@ const DefaultOutline = ({ hit, fields, template, layout = HitLayout.NORMAL, read
41
41
  if (!displayedData) {
42
42
  return null;
43
43
  }
44
- return (_jsxs(React.Fragment, { children: [_jsx(Tooltip, { title: (config.indexes.hit[field]?.description ?? t('none')).split('\n')[0], children: _jsxs(Typography, { variant: layout !== HitLayout.COMFY ? 'caption' : 'body1', fontWeight: "bold", children: [field, ":"] }) }), _jsx(PluginTypography, { context: "outline", variant: layout !== HitLayout.COMFY ? 'caption' : 'body1', whiteSpace: "normal", sx: { width: '100%', wordBreak: 'break-all' }, value: displayedData, field: field, children: displayedData })] }, field));
44
+ return (_jsxs(React.Fragment, { children: [_jsx(Tooltip, { title: (config.indexes.hit[field]?.description ?? t('none')).split('\n')[0], children: _jsxs(Typography, { variant: layout !== HitLayout.COMFY ? 'caption' : 'body1', fontWeight: "bold", children: [field, ":"] }) }), _jsx(PluginTypography, { context: "outline", variant: layout !== HitLayout.COMFY ? 'caption' : 'body1', whiteSpace: "normal", sx: { width: '100%', wordBreak: 'break-all' }, value: displayedData, field: field, hit: hit, children: displayedData })] }, field));
45
45
  })] }));
46
46
  };
47
47
  export default memo(DefaultOutline);
@@ -25,7 +25,7 @@ const ViewLink = ({ id, viewId }) => {
25
25
  const [view, setView] = useState(null);
26
26
  useEffect(() => {
27
27
  setLoading(true);
28
- getCurrentViews({ viewId, ignoreParams: true })
28
+ getCurrentViews({ views: [viewId], ignoreParams: true })
29
29
  .then(result => setView(result[0]))
30
30
  .finally(() => setLoading(false));
31
31
  }, [getCurrentViews, viewId]);
@@ -341,7 +341,7 @@ describe('ViewLink', () => {
341
341
  it('should call getCurrentViews when viewId changes', async () => {
342
342
  const { rerender } = render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
343
343
  await screen.findByText('Test View');
344
- expect(mockViewContext.getCurrentViews).toHaveBeenCalledWith({ viewId: 'test-view-id', ignoreParams: true });
344
+ expect(mockViewContext.getCurrentViews).toHaveBeenCalledWith({ views: ['test-view-id'], ignoreParams: true });
345
345
  mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([
346
346
  createMockView({
347
347
  view_id: 'another-view-id',
@@ -350,7 +350,7 @@ describe('ViewLink', () => {
350
350
  ]);
351
351
  rerender(_jsx(ViewLink, { id: 0, viewId: "another-view-id" }));
352
352
  await screen.findByText('Another View');
353
- expect(mockViewContext.getCurrentViews).toHaveBeenCalledWith({ viewId: 'another-view-id', ignoreParams: true });
353
+ expect(mockViewContext.getCurrentViews).toHaveBeenCalledWith({ views: ['another-view-id'], ignoreParams: true });
354
354
  });
355
355
  });
356
356
  describe('Accessibility', () => {
@@ -385,7 +385,7 @@ describe('ViewLink', () => {
385
385
  mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
386
386
  render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
387
387
  await screen.findByText('Test View');
388
- expect(mockViewContext.getCurrentViews).toHaveBeenCalledWith({ viewId: 'test-view-id', ignoreParams: true });
388
+ expect(mockViewContext.getCurrentViews).toHaveBeenCalledWith({ views: ['test-view-id'], ignoreParams: true });
389
389
  });
390
390
  it('should use removeView from ParameterContext', async () => {
391
391
  mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
@@ -1,5 +1,7 @@
1
1
  import { type SxProps } from '@mui/material';
2
+ import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
3
  declare const _default: import("react").NamedExoticComponent<{
4
+ hit: Hit;
3
5
  value: string;
4
6
  sx?: SxProps;
5
7
  className: string;
@@ -4,7 +4,7 @@ import { Stack, TableCell } from '@mui/material';
4
4
  import PluginTypography from '@cccsaurora/howler-ui/components/elements/PluginTypography';
5
5
  import { memo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
- const EnhancedCell = ({ value: rawValue, sx = {}, className, field }) => {
7
+ const EnhancedCell = ({ hit, value: rawValue, sx = {}, className, field }) => {
8
8
  const { t } = useTranslation();
9
9
  if (!rawValue) {
10
10
  return _jsx(TableCell, { style: { borderBottom: 'none' }, children: t('none') });
@@ -13,6 +13,6 @@ const EnhancedCell = ({ value: rawValue, sx = {}, className, field }) => {
13
13
  return (_jsx(TableCell, { sx: { borderBottom: 'none', borderRight: 'thin solid', borderRightColor: 'divider', fontSize: '0.8rem' }, children: _jsx(Stack, { direction: "row", className: className, spacing: 0.5, sx: [
14
14
  { display: 'flex', justifyContent: 'start', width: '100%', overflow: 'hidden' },
15
15
  ...(Array.isArray(sx) ? sx : [sx])
16
- ], children: values.map((value, index) => (_jsx(PluginTypography, { context: "table", sx: { fontSize: 'inherit', textOverflow: 'ellipsis' }, value: value, field: field, children: value }, value + index))) }) }));
16
+ ], children: values.map((value, index) => (_jsx(PluginTypography, { context: "table", sx: { fontSize: 'inherit', textOverflow: 'ellipsis' }, value: value, field: field, hit: hit, children: value }, value + index))) }) }));
17
17
  };
18
18
  export default memo(EnhancedCell);
@@ -45,6 +45,6 @@ const HitRow = ({ hit, analyticIds, columns, columnWidths, collapseMainColumn, o
45
45
  e.preventDefault();
46
46
  e.stopPropagation();
47
47
  setExpandRow(_expanded => !_expanded);
48
- }, children: _jsx(KeyboardArrowUp, {}) }), _jsx(Collapse, { in: !collapseMainColumn, orientation: "horizontal", unmountOnExit: true, children: _jsxs(Stack, { direction: "row", spacing: 1, flexWrap: "nowrap", children: [_jsx(EscalationChip, { hit: hit, layout: HitLayout.DENSE, hideLabel: true }), _jsxs(Typography, { sx: { textWrap: 'nowrap', whiteSpace: 'nowrap', fontSize: 'inherit' }, children: [analyticIds[hit.howler.analytic] ? (_jsx(Link, { to: `/analytics/${analyticIds[hit.howler.analytic]}`, onClick: e => e.stopPropagation(), children: hit.howler.analytic })) : (hit.howler.analytic), hit.howler.detection && ': ', hit.howler.detection] }), hit.howler.assignment !== 'unassigned' && _jsx(Assigned, { hit: hit, layout: HitLayout.DENSE, hideLabel: true })] }) })] }) }), columns.map(col => (_jsx(EnhancedCell, { className: `col-${col.replaceAll('.', '-')}`, value: get(hit, col) ?? t('none'), sx: columnWidths[col] ? { width: columnWidths[col] } : { width: '220px', maxWidth: '300px' }, field: col }, col)))] }, hit.howler.id), _jsx(TableRow, { onClick: ev => onClick(ev, hit), children: _jsx(TableCell, { colSpan: columns.length + 2, style: { paddingBottom: 0, paddingTop: 0 }, children: _jsx(Collapse, { in: expandRow, unmountOnExit: true, children: _jsx(Box, { width: "100%", maxWidth: "1200px", margin: 1, children: _jsx(HitCard, { id: hit.howler.id, layout: HitLayout.NORMAL }) }) }) }) })] }));
48
+ }, children: _jsx(KeyboardArrowUp, {}) }), _jsx(Collapse, { in: !collapseMainColumn, orientation: "horizontal", unmountOnExit: true, children: _jsxs(Stack, { direction: "row", spacing: 1, flexWrap: "nowrap", children: [_jsx(EscalationChip, { hit: hit, layout: HitLayout.DENSE, hideLabel: true }), _jsxs(Typography, { sx: { textWrap: 'nowrap', whiteSpace: 'nowrap', fontSize: 'inherit' }, children: [analyticIds[hit.howler.analytic] ? (_jsx(Link, { to: `/analytics/${analyticIds[hit.howler.analytic]}`, onClick: e => e.stopPropagation(), children: hit.howler.analytic })) : (hit.howler.analytic), hit.howler.detection && ': ', hit.howler.detection] }), hit.howler.assignment !== 'unassigned' && _jsx(Assigned, { hit: hit, layout: HitLayout.DENSE, hideLabel: true })] }) })] }) }), columns.map(col => (_jsx(EnhancedCell, { hit: hit, className: `col-${col.replaceAll('.', '-')}`, value: get(hit, col) ?? t('none'), sx: columnWidths[col] ? { width: columnWidths[col] } : { width: '220px', maxWidth: '300px' }, field: col }, col)))] }, hit.howler.id), _jsx(TableRow, { onClick: ev => onClick(ev, hit), children: _jsx(TableCell, { colSpan: columns.length + 2, style: { paddingBottom: 0, paddingTop: 0 }, children: _jsx(Collapse, { in: expandRow, unmountOnExit: true, children: _jsx(Box, { width: "100%", maxWidth: "1200px", margin: 1, children: _jsx(HitCard, { id: hit.howler.id, layout: HitLayout.NORMAL }) }) }) }) })] }));
49
49
  };
50
50
  export default memo(HitRow);
@@ -143,7 +143,7 @@ const ViewComposer = () => {
143
143
  return;
144
144
  }
145
145
  (async () => {
146
- const viewToEdit = (await getCurrentViews({ viewId: routeParams.id }))[0];
146
+ const viewToEdit = (await getCurrentViews({ views: [routeParams.id] }))[0];
147
147
  if (!viewToEdit) {
148
148
  setError('route.views.missing');
149
149
  return;
@@ -162,7 +162,7 @@ const ViewComposer = () => {
162
162
  }
163
163
  })();
164
164
  // eslint-disable-next-line react-hooks/exhaustive-deps
165
- }, [routeParams.id, getCurrentViews]);
165
+ }, [routeParams.id]);
166
166
  return (_jsx(FlexPort, { children: _jsx(ErrorBoundary, { children: _jsx(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: _jsxs(VSBox, { top: 0, children: [_jsx(VSBoxHeader, { pb: 1, children: _jsxs(Stack, { spacing: 1, children: [error && (_jsx(Alert, { variant: "outlined", severity: "error", children: t(error) })), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(TextField, { label: t('route.views.name'), size: "small", value: title, onChange: e => setTitle(e.target.value), fullWidth: true }), _jsxs(ToggleButtonGroup, { sx: { display: 'grid', gridTemplateColumns: '1fr 1fr' }, size: "small", exclusive: true, value: type, onChange: (__, _type) => {
167
167
  if (_type) {
168
168
  setType(_type);
package/index.js CHANGED
@@ -3,8 +3,13 @@ import '@fontsource/roboto';
3
3
  import App from '@cccsaurora/howler-ui/components/app/App';
4
4
  import '@cccsaurora/howler-ui/i18n';
5
5
  import 'index.css';
6
+ import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
6
7
  // import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
7
8
  import * as ReactDOM from 'react-dom/client';
8
9
  // This is where you can inject UI plugins to modify Howler's interface.
9
10
  // howlerPluginStore.install(new ExamplePlugin());
11
+ if (import.meta.env.VITE_ENABLE_CLUE === 'true') {
12
+ const cluePlugin = await import('plugins/clue');
13
+ howlerPluginStore.install(new cluePlugin.default());
14
+ }
10
15
  ReactDOM.createRoot(document.getElementById('root')).render(_jsx(App, {}));
@@ -315,6 +315,7 @@
315
315
  "modal.rationale.label": "Rationale",
316
316
  "modal.rationale.type.analytic": "This rationale was used when assessing alerts with the same analytic name.",
317
317
  "modal.rationale.type.assignment": "This is a rationale you have recently used when assessing an alert.",
318
+ "modal.rationale.type.preset": "This is a preset rationale configured for the associated analytic.",
318
319
  "none": "None",
319
320
  "no.data": "No Data",
320
321
  "on": "on",
@@ -317,6 +317,7 @@
317
317
  "modal.rationale.label": "Justification",
318
318
  "modal.rationale.type.analytic": "Cette justification a été utilisée lors de l'évaluation des alertes avec le même nom d'analyse.",
319
319
  "modal.rationale.type.assignment": "Il s'agit d'une justification que vous avez récemment utilisée lors de l'évaluation d'une alerte.",
320
+ "modal.rationale.type.preset": "Il s'agit d'un raisonnement prédéfini configuré pour l'analyse associée.",
320
321
  "none": "Rien",
321
322
  "no.data": "Aucune donnée",
322
323
  "on": "sur",
@@ -1,18 +1,18 @@
1
- import type { Notebook } from './Notebook';
2
1
  import type { Comment } from './Comment';
2
+ import type { Notebook } from './Notebook';
3
3
  import type { TriageSettings } from './TriageSettings';
4
4
 
5
5
  /**
6
6
  * NOTE: This is an auto-generated file. Don't edit this manually.
7
7
  */
8
8
  export interface Analytic {
9
- notebooks?: Notebook[];
10
9
  analytic_id?: string;
11
10
  comment?: Comment[];
12
11
  contributors?: string[];
13
12
  description?: string;
14
13
  detections?: string[];
15
14
  name?: string;
15
+ notebooks?: Notebook[];
16
16
  owner?: string;
17
17
  rule?: string;
18
18
  rule_crontab?: string;
@@ -48,8 +48,8 @@ export interface APILookups {
48
48
  'mitigated'
49
49
  ];
50
50
  transitions: { [index: string]: string[] };
51
- tactics: { [index: string]: { key: string; name: string; url: string } };
52
51
  techniques: { [index: string]: { key: string; name: string; url: string } };
52
+ tactics: { [index: string]: { key: string; name: string; url: string } };
53
53
  icons: string[];
54
54
  roles: ['admin', 'automation_advanced', 'automation_basic', 'user'];
55
55
  }
@@ -81,6 +81,7 @@ export interface APIConfiguration {
81
81
  };
82
82
  mapping: APIMappings;
83
83
  features: {
84
+ clue: boolean;
84
85
  notebook: boolean;
85
86
  [feature: string]: boolean;
86
87
  };
@@ -0,0 +1,8 @@
1
+ import type { Type } from './Type';
2
+
3
+ /**
4
+ * NOTE: This is an auto-generated file. Don't edit this manually.
5
+ */
6
+ export interface Clue {
7
+ types?: Type[];
8
+ }
@@ -4,6 +4,7 @@ import type { Aws } from './Aws';
4
4
  import type { Azure } from './Azure';
5
5
  import type { Cbs } from './Cbs';
6
6
  import type { Cloud } from './Cloud';
7
+ import type { Clue } from './Clue';
7
8
  import type { Container } from './Container';
8
9
  import type { Destination } from './Destination';
9
10
  import type { Dns } from './Dns';
@@ -41,30 +42,16 @@ import type { Vulnerability } from './Vulnerability';
41
42
  export interface Hit {
42
43
  agent?: Agent;
43
44
  assemblyline?: Assemblyline;
44
- autoruns_category?: string;
45
- autoruns_display_name?: string;
46
- autoruns_enabled?: number;
47
- autoruns_flags?: string;
48
- autoruns_last_runtime?: string;
49
- autoruns_last_task_result?: string;
50
- autoruns_location?: string;
51
- autoruns_mod_time?: string;
52
- autoruns_name?: string;
53
- autoruns_scheduled_time?: string;
54
45
  aws?: Aws;
55
46
  azure?: Azure;
56
47
  cbs?: Cbs;
57
- client?: string;
58
- client_id?: string;
59
48
  cloud?: Cloud;
60
- cluster_size?: number;
49
+ clue?: Clue;
61
50
  container?: Container;
62
51
  destination?: Destination;
63
52
  dns?: Dns;
64
- dns_servers?: string;
65
53
  ecs?: Ecs;
66
54
  email?: Email;
67
- eml_paths?: string;
68
55
  error?: Error;
69
56
  event?: Event;
70
57
  faas?: Faas;
@@ -74,8 +61,6 @@ export interface Hit {
74
61
  host?: Host;
75
62
  howler: Howler;
76
63
  http?: Http;
77
- incident_urls?: string;
78
- indicator_summaries?: string;
79
64
  interface?: Interface;
80
65
  labels?: { [index: string]: string };
81
66
  message?: string;
@@ -85,10 +70,7 @@ export interface Hit {
85
70
  process?: Process;
86
71
  registry?: Registry;
87
72
  related?: Related;
88
- retained_by?: string;
89
- retention_url?: string;
90
73
  rule?: Rule;
91
- senders?: string;
92
74
  server?: Server;
93
75
  source?: Source;
94
76
  tags?: string[];
@@ -9,5 +9,6 @@ export interface Labels {
9
9
  mitigation?: string[];
10
10
  operation?: string[];
11
11
  threat?: string[];
12
+ tuning?: string[];
12
13
  victim?: string[];
13
14
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * NOTE: This is an auto-generated file. Don't edit this manually.
3
+ */
4
+ export interface Type {
5
+ field?: string;
6
+ type?: string;
7
+ }