@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.
- package/commons/components/app/hooks/useAppConfigs.d.ts +1 -1
- package/components/app/providers/HitSearchProvider.d.ts +0 -1
- package/components/app/providers/HitSearchProvider.js +4 -6
- package/components/app/providers/HitSearchProvider.test.js +1 -1
- package/components/app/providers/ParameterProvider.js +3 -3
- package/components/app/providers/ViewProvider.d.ts +1 -1
- package/components/app/providers/ViewProvider.js +3 -6
- package/components/app/providers/ViewProvider.test.js +1 -1
- package/components/elements/PluginChip.d.ts +2 -0
- package/components/elements/PluginChip.js +2 -1
- package/components/elements/PluginTypography.d.ts +4 -3
- package/components/elements/PluginTypography.js +4 -3
- package/components/elements/display/modals/RationaleModal.js +1 -1
- package/components/elements/display/modals/RationaleModal.test.js +1 -1
- package/components/elements/hit/HitBanner.js +2 -2
- package/components/elements/hit/HitDetails.js +9 -9
- package/components/elements/hit/outlines/DefaultOutline.js +1 -1
- package/components/routes/hits/search/ViewLink.js +1 -1
- package/components/routes/hits/search/ViewLink.test.js +3 -3
- package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -0
- package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
- package/components/routes/hits/search/grid/HitRow.js +1 -1
- package/components/routes/views/ViewComposer.js +2 -2
- package/index.js +5 -0
- package/locales/en/translation.json +1 -0
- package/locales/fr/translation.json +1 -0
- package/models/entities/generated/Analytic.d.ts +2 -2
- package/models/entities/generated/ApiType.d.ts +2 -1
- package/models/entities/generated/Clue.d.ts +8 -0
- package/models/entities/generated/Hit.d.ts +2 -20
- package/models/entities/generated/Labels.d.ts +1 -0
- package/models/entities/generated/Type.d.ts +7 -0
- package/package.json +24 -15
- package/plugins/HowlerPlugin.js +1 -0
- package/plugins/clue/Provider.d.ts +3 -0
- package/plugins/clue/Provider.js +13 -0
- package/plugins/clue/components/ClueChip.d.ts +3 -0
- package/plugins/clue/components/ClueChip.js +29 -0
- package/plugins/clue/components/ClueLeadForm.d.ts +4 -0
- package/plugins/clue/components/ClueLeadForm.js +24 -0
- package/plugins/clue/components/CluePivot.d.ts +3 -0
- package/plugins/clue/components/CluePivot.js +145 -0
- package/plugins/clue/components/CluePivotForm.d.ts +21 -0
- package/plugins/clue/components/CluePivotForm.js +270 -0
- package/plugins/clue/components/ClueTypography.d.ts +3 -0
- package/plugins/clue/components/ClueTypography.js +53 -0
- package/plugins/clue/helpers.d.ts +3 -0
- package/plugins/clue/helpers.js +196 -0
- package/plugins/clue/index.d.ts +21 -0
- package/plugins/clue/index.js +66 -0
- package/plugins/clue/locales/clue.en.json +8 -0
- package/plugins/clue/locales/clue.fr.json +8 -0
- package/plugins/clue/setup.d.ts +2 -0
- package/plugins/clue/setup.js +46 -0
- package/plugins/clue/utils.d.ts +2 -0
- package/plugins/clue/utils.js +19 -0
- 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
|
|
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: ({
|
|
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'
|
|
85
|
-
pendingChanges.current.selected =
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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 ({
|
|
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({
|
|
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 {
|
|
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
|
|
9
|
-
export default
|
|
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({
|
|
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({
|
|
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({
|
|
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()]);
|
|
@@ -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({
|
|
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
|
|
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
|
};
|
|
@@ -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
|
-
|
|
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[];
|