@cccsaurora/howler-ui 2.12.2 → 2.13.0-dev.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/search/facet/hit.d.ts +4 -2
- package/api/search/facet/hit.js +5 -5
- package/api/search/facet/index.d.ts +1 -0
- package/components/app/providers/FavouritesProvider.js +27 -30
- package/components/app/providers/HitSearchProvider.js +7 -10
- package/components/app/providers/ViewProvider.d.ts +5 -4
- package/components/app/providers/ViewProvider.js +58 -36
- package/components/elements/hit/HitActions.js +1 -1
- package/components/elements/hit/HitSummary.js +6 -6
- package/components/elements/hit/aggregate/HitGraph.js +6 -9
- package/components/routes/advanced/luceneCompletionProvider.js +4 -2
- package/components/routes/analytics/widgets/Assessment.js +3 -2
- package/components/routes/analytics/widgets/Escalation.js +4 -3
- package/components/routes/analytics/widgets/Stacked.js +4 -3
- package/components/routes/hits/search/HitBrowser.js +8 -8
- package/components/routes/hits/search/HitQuery.js +1 -1
- package/components/routes/hits/search/SearchPane.js +1 -1
- package/components/routes/hits/search/ViewLink.js +3 -2
- package/components/routes/hits/search/grid/HitGrid.js +5 -7
- package/components/routes/hits/search/shared/HitFilter.js +2 -2
- package/components/routes/hits/search/shared/HitSort.js +9 -8
- package/components/routes/hits/search/shared/QuerySettings.js +1 -1
- package/components/routes/hits/search/shared/SearchSpan.js +17 -13
- package/components/routes/home/AddNewCard.js +14 -1
- package/components/routes/home/ViewCard.js +5 -1
- package/components/routes/home/index.js +1 -1
- package/components/routes/views/ViewComposer.js +17 -16
- package/components/routes/views/Views.js +25 -14
- package/package.json +7 -3
- package/plugins/borealis/Provider.d.ts +3 -0
- package/plugins/borealis/Provider.js +14 -0
- package/plugins/borealis/components/BorealisChip.d.ts +3 -0
- package/plugins/borealis/components/BorealisChip.js +27 -0
- package/plugins/borealis/components/BorealisLeadForm.d.ts +4 -0
- package/plugins/borealis/components/BorealisLeadForm.js +23 -0
- package/plugins/borealis/components/BorealisPivot.d.ts +3 -0
- package/plugins/borealis/components/BorealisPivot.js +83 -0
- package/plugins/borealis/components/BorealisPivotForm.d.ts +4 -0
- package/plugins/borealis/components/BorealisPivotForm.js +44 -0
- package/plugins/borealis/components/BorealisTypography.d.ts +3 -0
- package/plugins/borealis/components/BorealisTypography.js +53 -0
- package/plugins/borealis/helpers.d.ts +6 -0
- package/plugins/borealis/helpers.js +137 -0
- package/plugins/borealis/index.d.ts +21 -0
- package/plugins/borealis/index.js +46 -0
- package/plugins/borealis/locales/borealis.en.json +7 -0
- package/plugins/borealis/locales/borealis.fr.json +7 -0
- package/plugins/borealis/setup.d.ts +2 -0
- package/plugins/borealis/setup.js +44 -0
|
@@ -16,7 +16,6 @@ import useHitSelection from '@cccsaurora/howler-ui/components/hooks/useHitSelect
|
|
|
16
16
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
17
17
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
18
18
|
import { useTranslation } from 'react-i18next';
|
|
19
|
-
import { useLocation, useParams } from 'react-router-dom';
|
|
20
19
|
import { useContextSelector } from 'use-context-selector';
|
|
21
20
|
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
22
21
|
import HitContextMenu from '../HitContextMenu';
|
|
@@ -29,8 +28,6 @@ import HitRow from './HitRow';
|
|
|
29
28
|
const HitGrid = () => {
|
|
30
29
|
const { t } = useTranslation();
|
|
31
30
|
const { getIdFromName } = useContext(AnalyticContext);
|
|
32
|
-
const routeParams = useParams();
|
|
33
|
-
const location = useLocation();
|
|
34
31
|
const theme = useTheme();
|
|
35
32
|
const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
36
33
|
const { onClick } = useHitSelection();
|
|
@@ -39,10 +36,11 @@ const HitGrid = () => {
|
|
|
39
36
|
const setDisplayType = useContextSelector(HitSearchContext, ctx => ctx.setDisplayType);
|
|
40
37
|
const response = useContextSelector(HitSearchContext, ctx => ctx.response);
|
|
41
38
|
const searching = useContextSelector(HitSearchContext, ctx => ctx.searching);
|
|
39
|
+
const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
|
|
42
40
|
const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
|
|
43
41
|
const query = useContextSelector(ParameterContext, ctx => ctx.query);
|
|
44
|
-
const
|
|
45
|
-
const selectedView = useContextSelector(ViewContext, ctx => ctx.views
|
|
42
|
+
const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
|
|
43
|
+
const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
|
|
46
44
|
const [collapseMainColumn, setCollapseMainColumn] = useMyLocalStorageItem(StorageKey.GRID_COLLAPSE_COLUMN, false);
|
|
47
45
|
const [analyticIds, setAnalyticIds] = useState({});
|
|
48
46
|
const columnModalRef = useRef();
|
|
@@ -59,11 +57,11 @@ const HitGrid = () => {
|
|
|
59
57
|
if (selectedHits.length > 1) {
|
|
60
58
|
return true;
|
|
61
59
|
}
|
|
62
|
-
if (selectedHits.length === 1 && selectedHits[0]?.howler.id !==
|
|
60
|
+
if (selectedHits.length === 1 && selected && selectedHits[0]?.howler.id !== selected) {
|
|
63
61
|
return true;
|
|
64
62
|
}
|
|
65
63
|
return false;
|
|
66
|
-
}, [
|
|
64
|
+
}, [selected, selectedHits]);
|
|
67
65
|
useEffect(() => {
|
|
68
66
|
response?.items.forEach(hit => {
|
|
69
67
|
if (!analyticIds[hit.howler.analytic]) {
|
|
@@ -42,8 +42,8 @@ const HitFilter = ({ size }) => {
|
|
|
42
42
|
setFilter('');
|
|
43
43
|
setSavedFilter(null);
|
|
44
44
|
if (!config.lookups[_category]) {
|
|
45
|
-
const facets = await api.search.facet.hit.post(
|
|
46
|
-
setCustomLookups(Object.keys(facets));
|
|
45
|
+
const facets = await api.search.facet.hit.post({ query: 'howler.id:*', fields: [_category] });
|
|
46
|
+
setCustomLookups(Object.keys(facets[_category]));
|
|
47
47
|
}
|
|
48
48
|
else {
|
|
49
49
|
setCustomLookups([]);
|
|
@@ -4,7 +4,7 @@ import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers
|
|
|
4
4
|
import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
|
|
5
5
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { useLocation
|
|
7
|
+
import { useLocation } from 'react-router-dom';
|
|
8
8
|
import { useContextSelector } from 'use-context-selector';
|
|
9
9
|
import CustomSort from '../CustomSort';
|
|
10
10
|
const CUSTOM = '__custom__';
|
|
@@ -21,8 +21,7 @@ const ACCEPTED_SORTS = [
|
|
|
21
21
|
const HitSort = ({ size = 'small' }) => {
|
|
22
22
|
const { t } = useTranslation();
|
|
23
23
|
const location = useLocation();
|
|
24
|
-
const
|
|
25
|
-
const views = useContextSelector(ViewContext, ctx => ctx.views);
|
|
24
|
+
const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
|
|
26
25
|
const savedSort = useContextSelector(ParameterContext, ctx => ctx.sort);
|
|
27
26
|
const setSavedSort = useContextSelector(ParameterContext, ctx => ctx.setSort);
|
|
28
27
|
const sortEntries = useMemo(() => savedSort.split(',').filter(part => !!part), [savedSort]);
|
|
@@ -38,7 +37,6 @@ const HitSort = ({ size = 'small' }) => {
|
|
|
38
37
|
* Should the custom sorter be shown? Defaults to true if there's more than one sort field, or we're sorting on a field not supported by the default dropdown
|
|
39
38
|
*/
|
|
40
39
|
const [showCustomSort, setShowCustomSort] = useState(sortEntries.length > 1 || (sortEntries.length > 0 && !ACCEPTED_SORTS.includes(sortEntries[0]?.split(' ')[0])));
|
|
41
|
-
const viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : null), [location.pathname, routeParams.id]);
|
|
42
40
|
/**
|
|
43
41
|
* This handles changing the sort if the basic sorter is used, OR enables the custom sorting.
|
|
44
42
|
*/
|
|
@@ -51,14 +49,17 @@ const HitSort = ({ size = 'small' }) => {
|
|
|
51
49
|
}
|
|
52
50
|
}, [setSavedSort, sort]);
|
|
53
51
|
useEffect(() => {
|
|
54
|
-
if (
|
|
55
|
-
|
|
52
|
+
if (location.search.includes('sort')) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
(async () => {
|
|
56
|
+
const selectedView = await getCurrentView(true);
|
|
56
57
|
if (selectedView?.sort && !location.search.includes('sort')) {
|
|
57
58
|
setSavedSort(selectedView.sort);
|
|
58
59
|
}
|
|
59
|
-
}
|
|
60
|
+
})();
|
|
60
61
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
61
|
-
}, [
|
|
62
|
+
}, [getCurrentView]);
|
|
62
63
|
return !showCustomSort ? (_jsxs(Stack, { direction: "row", spacing: 1, sx: { flex: 1.5 }, children: [_jsx(Autocomplete, { fullWidth: true, sx: { minWidth: '175px' }, size: size, value: field, options: ACCEPTED_SORTS, getOptionLabel: option => (option === CUSTOM ? t('hit.search.custom') : option), isOptionEqualToValue: (option, value) => option === value || (!value && option === ACCEPTED_SORTS[0]), renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.sort.fields') }), onChange: (_, value) => handleChange(value) }), _jsxs(Select, { size: size, sx: { minWidth: '150px' }, value: sort, onChange: e => setSavedSort(`${field} ${e.target.value}`), children: [_jsx(MenuItem, { value: "asc", children: t('asc') }), _jsx(MenuItem, { value: "desc", children: t('desc') })] })] })) : (_jsx(CustomSort, {}));
|
|
63
64
|
};
|
|
64
65
|
export default memo(HitSort);
|
|
@@ -10,7 +10,7 @@ import HitSort from './HitSort';
|
|
|
10
10
|
import SearchSpan from './SearchSpan';
|
|
11
11
|
const QuerySettings = ({ verticalSorters = false, boxSx }) => {
|
|
12
12
|
const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
|
|
13
|
-
const selectedView = useContextSelector(ViewContext, ctx => ctx.views
|
|
13
|
+
const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
|
|
14
14
|
return (_jsxs(Box, { sx: boxSx ?? { position: 'relative', maxWidth: '1200px' }, children: [_jsxs(Stack, { direction: verticalSorters ? 'column' : 'row', justifyContent: "space-between", spacing: 1, divider: !verticalSorters && _jsx(Divider, { flexItem: true, orientation: "vertical" }), sx: [
|
|
15
15
|
viewId &&
|
|
16
16
|
!selectedView && {
|
|
@@ -2,9 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Autocomplete, TextField } from '@mui/material';
|
|
3
3
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
4
4
|
import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
|
|
5
|
-
import { memo, useEffect
|
|
5
|
+
import { memo, useEffect } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { useLocation
|
|
7
|
+
import { useLocation } from 'react-router-dom';
|
|
8
8
|
import { useContextSelector } from 'use-context-selector';
|
|
9
9
|
import { convertLuceneToDate } from '@cccsaurora/howler-ui/utils/utils';
|
|
10
10
|
const DATE_RANGES = [
|
|
@@ -18,23 +18,27 @@ const DATE_RANGES = [
|
|
|
18
18
|
const SearchSpan = ({ omitCustom = false, size }) => {
|
|
19
19
|
const { t } = useTranslation();
|
|
20
20
|
const location = useLocation();
|
|
21
|
-
const routeParams = useParams();
|
|
22
21
|
const span = useContextSelector(ParameterContext, ctx => ctx.span);
|
|
23
22
|
const setSpan = useContextSelector(ParameterContext, ctx => ctx.setSpan);
|
|
24
|
-
const
|
|
25
|
-
const selectedView = useContextSelector(ViewContext, ctx => ctx.views?.find(_view => _view.view_id === viewId));
|
|
23
|
+
const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
|
|
26
24
|
useEffect(() => {
|
|
27
|
-
if (
|
|
25
|
+
if (location.search.includes('span')) {
|
|
28
26
|
return;
|
|
29
27
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
(async () => {
|
|
29
|
+
const viewSpan = (await getCurrentView(true))?.span;
|
|
30
|
+
if (!viewSpan) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (viewSpan.includes(':')) {
|
|
34
|
+
setSpan(convertLuceneToDate(viewSpan));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
setSpan(viewSpan);
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
36
40
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
37
|
-
}, [
|
|
41
|
+
}, [getCurrentView]);
|
|
38
42
|
return (_jsx(Autocomplete, { fullWidth: true, sx: { minWidth: '200px', flex: 1 }, size: size ?? 'small', value: span, options: omitCustom ? DATE_RANGES.slice(0, DATE_RANGES.length - 1) : DATE_RANGES, renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.span') }), getOptionLabel: option => t(option), onChange: (_, value) => setSpan(value), disableClearable: true }));
|
|
39
43
|
};
|
|
40
44
|
export default memo(SearchSpan);
|
|
@@ -16,9 +16,12 @@ const VISUALIZATIONS = ['assessment', 'created', 'escalation', 'status', 'detect
|
|
|
16
16
|
const AddNewCard = ({ dashboard, addCard }) => {
|
|
17
17
|
const { t } = useTranslation();
|
|
18
18
|
const views = useContextSelector(ViewContext, ctx => ctx.views ?? []);
|
|
19
|
+
const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
|
|
19
20
|
const [selectedType, setSelectedType] = useState('');
|
|
20
21
|
const [analytics, setAnalytics] = useState([]);
|
|
21
22
|
const [config, _setConfig] = useState({});
|
|
23
|
+
const [viewOpen, setViewOpen] = useState(false);
|
|
24
|
+
const [viewLoading, setViewLoading] = useState(false);
|
|
22
25
|
const setConfig = useCallback((key, value) => _setConfig(_config => ({ ..._config, [key]: value })), []);
|
|
23
26
|
const _addCard = useCallback(() => {
|
|
24
27
|
if (!selectedType) {
|
|
@@ -43,9 +46,19 @@ const AddNewCard = ({ dashboard, addCard }) => {
|
|
|
43
46
|
_setConfig({});
|
|
44
47
|
}
|
|
45
48
|
}, [selectedType]);
|
|
49
|
+
const onViewOpen = useCallback(async () => {
|
|
50
|
+
setViewOpen(true);
|
|
51
|
+
setViewLoading(true);
|
|
52
|
+
try {
|
|
53
|
+
await fetchViews();
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
setViewLoading(false);
|
|
57
|
+
}
|
|
58
|
+
}, [fetchViews]);
|
|
46
59
|
return (_jsx(Grid, { item: true, xs: 12, md: 6, children: _jsxs(Card, { variant: "outlined", sx: { height: '100%' }, children: [_jsx(CardHeader, { title: t('route.home.add'), subheader: _jsx(Typography, { variant: "body2", color: "text.secondary", children: t('route.home.add.description') }) }), _jsx(CardContent, { children: _jsxs(Stack, { spacing: 1, children: [_jsxs(FormControl, { sx: theme => ({ mt: `${theme.spacing(2)} !important` }), children: [_jsx(InputLabel, { children: t('route.home.add.type') }), _jsx(Select, { value: selectedType, onChange: event => setSelectedType(event.target.value), label: t('route.home.add.type'), children: Object.keys(TYPES).map(type => (_jsx(MenuItem, { value: type, children: _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(`route.home.add.type.${type}`) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t(`route.home.add.type.${type}.description`) })] }) }, type))) })] }), selectedType && _jsx(Divider, { flexItem: true }), selectedType === 'analytic' && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body1", children: t('route.home.add.analytic.title') }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('route.home.add.analytic.description') }), _jsx(Autocomplete, { sx: { pt: 1 }, onChange: (__, opt) => setConfig('analyticId', opt.analytic_id), options: analytics, filterOptions: (options, state) => options.filter(opt => opt.name.toLowerCase().includes(state.inputValue.toLowerCase()) ||
|
|
47
60
|
opt.description?.split('\n')[0]?.toLowerCase().includes(state.inputValue.toLowerCase())), renderOption: (props, option) => (_createElement("li", { ...props, key: option.analytic_id },
|
|
48
|
-
_jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: option.name }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: option.description?.split('\n')[0] })] }))), getOptionLabel: option => option.name, renderInput: params => _jsx(TextField, { ...params, label: t('route.home.add.analytic') }) }), _jsxs(FormControl, { sx: theme => ({ mt: `${theme.spacing(2)} !important` }), children: [_jsx(InputLabel, { children: t('route.home.add.visualization') }), _jsx(Select, { value: config.type ?? '', onChange: event => setConfig('type', event.target.value), label: t('route.home.add.visualization'), children: VISUALIZATIONS.map(viz => (_jsx(MenuItem, { value: viz, children: _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(`route.home.add.visualization.${viz}`) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t(`route.home.add.visualization.${viz}.description`) })] }) }, viz))) })] })] })), selectedType === 'view' && (_jsxs(_Fragment, { children: [_jsx(Autocomplete, { sx: { pt: 1 }, onChange: (__, opt) => setConfig('viewId', opt.view_id), options: views, filterOptions: (options, state) => options.filter(opt => !dashboard?.find(entry => entry.type === 'view' && JSON.parse(entry.config).viewId === opt.view_id) &&
|
|
61
|
+
_jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: option.name }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: option.description?.split('\n')[0] })] }))), getOptionLabel: option => option.name, renderInput: params => _jsx(TextField, { ...params, label: t('route.home.add.analytic') }) }), _jsxs(FormControl, { sx: theme => ({ mt: `${theme.spacing(2)} !important` }), children: [_jsx(InputLabel, { children: t('route.home.add.visualization') }), _jsx(Select, { value: config.type ?? '', onChange: event => setConfig('type', event.target.value), label: t('route.home.add.visualization'), children: VISUALIZATIONS.map(viz => (_jsx(MenuItem, { value: viz, children: _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(`route.home.add.visualization.${viz}`) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t(`route.home.add.visualization.${viz}.description`) })] }) }, viz))) })] })] })), selectedType === 'view' && (_jsxs(_Fragment, { children: [_jsx(Autocomplete, { sx: { pt: 1 }, onChange: (__, opt) => setConfig('viewId', opt.view_id), onOpen: onViewOpen, onClose: () => setViewOpen(false), open: viewOpen, loading: viewLoading, options: Object.values(views), filterOptions: (options, state) => options.filter(opt => !dashboard?.find(entry => entry.type === 'view' && JSON.parse(entry.config).viewId === opt.view_id) &&
|
|
49
62
|
(opt.title.toLowerCase().includes(state.inputValue.toLowerCase()) ||
|
|
50
63
|
opt.query.toLowerCase().includes(state.inputValue.toLowerCase()))), renderOption: (props, option) => (_createElement("li", { ...props, key: option.view_id },
|
|
51
64
|
_jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(option.title) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: option.query })] }))), getOptionLabel: option => t(option.title), renderInput: params => _jsx(TextField, { ...params, label: t('route.home.add.view') }) }), _jsx(Typography, { variant: "body1", sx: { pt: 1 }, children: t('route.home.add.limit') }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('route.home.add.limit.description') }), _jsx(Box, { sx: { px: 0.5 }, children: _jsx(Slider, { value: config.limit ?? 3, valueLabelDisplay: "auto", onChange: (_, value) => setConfig('limit', value), min: 1, max: 10, step: 1, marks: true }) })] })), _jsx(Stack, { direction: "row", justifyContent: "end", children: _jsx(CustomButton, { variant: "outlined", size: "small", color: "primary", startIcon: _jsx(Check, {}), disabled: !selectedType || TYPES[selectedType]?.filter(field => !config[field])?.length > 0, onClick: _addCard, children: t('create') }) })] }) })] }) }));
|
|
@@ -17,7 +17,11 @@ const ViewCard = ({ viewId, limit }) => {
|
|
|
17
17
|
const { dispatchApi } = useMyApi();
|
|
18
18
|
const [hits, setHits] = useState([]);
|
|
19
19
|
const [loading, setLoading] = useState(false);
|
|
20
|
-
const view = useContextSelector(ViewContext, ctx => ctx.views
|
|
20
|
+
const view = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
|
|
21
|
+
const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
fetchViews([viewId]);
|
|
24
|
+
}, [fetchViews, viewId]);
|
|
21
25
|
useEffect(() => {
|
|
22
26
|
if (!view?.query) {
|
|
23
27
|
return;
|
|
@@ -36,7 +36,7 @@ const ViewComposer = () => {
|
|
|
36
36
|
const navigate = useNavigate();
|
|
37
37
|
const addView = useContextSelector(ViewContext, ctx => ctx.addView);
|
|
38
38
|
const editView = useContextSelector(ViewContext, ctx => ctx.editView);
|
|
39
|
-
const
|
|
39
|
+
const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
|
|
40
40
|
const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
|
|
41
41
|
const loadHits = useContextSelector(HitContext, ctx => ctx.loadHits);
|
|
42
42
|
// view state
|
|
@@ -130,29 +130,30 @@ const ViewComposer = () => {
|
|
|
130
130
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
131
131
|
}, [sort, span]);
|
|
132
132
|
useEffect(() => {
|
|
133
|
-
if (routeParams.id) {
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
if (!routeParams.id) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
(async () => {
|
|
137
|
+
const viewToEdit = await getCurrentView();
|
|
138
|
+
if (!viewToEdit) {
|
|
136
139
|
setError('route.views.missing');
|
|
137
140
|
return;
|
|
138
141
|
}
|
|
139
142
|
else {
|
|
140
143
|
setError(null);
|
|
141
144
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
setSort(viewToEdit.sort);
|
|
148
|
-
}
|
|
149
|
-
if (viewToEdit.span) {
|
|
150
|
-
setSpan(viewToEdit.span);
|
|
151
|
-
}
|
|
145
|
+
setTitle(viewToEdit.title);
|
|
146
|
+
setAdvanceOnTriage(viewToEdit.settings?.advance_on_triage ?? false);
|
|
147
|
+
setQuery(viewToEdit.query);
|
|
148
|
+
if (viewToEdit.sort) {
|
|
149
|
+
setSort(viewToEdit.sort);
|
|
152
150
|
}
|
|
153
|
-
|
|
151
|
+
if (viewToEdit.span) {
|
|
152
|
+
setSpan(viewToEdit.span);
|
|
153
|
+
}
|
|
154
|
+
})();
|
|
154
155
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
155
|
-
}, [routeParams.id,
|
|
156
|
+
}, [routeParams.id, getCurrentView]);
|
|
156
157
|
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) => {
|
|
157
158
|
if (_type) {
|
|
158
159
|
setType(_type);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createElement as _createElement } from "react";
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { Clear, Edit, SavedSearch, Star, StarBorder } from '@mui/icons-material';
|
|
3
4
|
import { Autocomplete, Card, Checkbox, IconButton, Skeleton, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from '@mui/material';
|
|
@@ -12,6 +13,7 @@ import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemM
|
|
|
12
13
|
import { ViewTitle } from '@cccsaurora/howler-ui/components/elements/view/ViewTitle';
|
|
13
14
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
14
15
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
16
|
+
import { isNull, omitBy, size } from 'lodash-es';
|
|
15
17
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
|
16
18
|
import { useTranslation } from 'react-i18next';
|
|
17
19
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
|
@@ -24,8 +26,8 @@ const ViewsBase = () => {
|
|
|
24
26
|
const { user } = useAppUser();
|
|
25
27
|
const navigate = useNavigate();
|
|
26
28
|
const { dispatchApi } = useMyApi();
|
|
27
|
-
const addFavourite = useContextSelector(ViewContext, ctx => ctx.addFavourite);
|
|
28
29
|
const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
|
|
30
|
+
const addFavourite = useContextSelector(ViewContext, ctx => ctx.addFavourite);
|
|
29
31
|
const removeFavourite = useContextSelector(ViewContext, ctx => ctx.removeFavourite);
|
|
30
32
|
const removeView = useContextSelector(ViewContext, ctx => ctx.removeView);
|
|
31
33
|
const views = useContextSelector(ViewContext, ctx => ctx.views);
|
|
@@ -41,6 +43,8 @@ const ViewsBase = () => {
|
|
|
41
43
|
const [hasError, setHasError] = useState(false);
|
|
42
44
|
const [searching, setSearching] = useState(false);
|
|
43
45
|
const [favouritesOnly, setFavouritesOnly] = useState(false);
|
|
46
|
+
const [defaultViewOpen, setDefaultViewOpen] = useState(false);
|
|
47
|
+
const [defaultViewLoading, setDefaultViewLoading] = useState(false);
|
|
44
48
|
const onSearch = useCallback(async () => {
|
|
45
49
|
try {
|
|
46
50
|
setSearching(true);
|
|
@@ -52,7 +56,6 @@ const ViewsBase = () => {
|
|
|
52
56
|
searchParams.delete('phrase');
|
|
53
57
|
}
|
|
54
58
|
setSearchParams(searchParams, { replace: true });
|
|
55
|
-
fetchViews(true);
|
|
56
59
|
const searchTerm = phrase ? `*${sanitizeLuceneQuery(phrase)}*` : '*';
|
|
57
60
|
const phraseQuery = FIELDS_TO_SEARCH.map(_field => `${_field}:${searchTerm}`).join(' OR ');
|
|
58
61
|
const typeQuery = `(type:global OR owner:(${user.username} OR none)) AND type:(${types.join(' OR ') || '*'}${types.includes('personal') ? ' OR readonly' : ''})`;
|
|
@@ -73,7 +76,6 @@ const ViewsBase = () => {
|
|
|
73
76
|
phrase,
|
|
74
77
|
setSearchParams,
|
|
75
78
|
searchParams,
|
|
76
|
-
fetchViews,
|
|
77
79
|
user.username,
|
|
78
80
|
user.favourite_views,
|
|
79
81
|
types,
|
|
@@ -105,29 +107,37 @@ const ViewsBase = () => {
|
|
|
105
107
|
const onDelete = useCallback(async (event, id) => {
|
|
106
108
|
event.preventDefault();
|
|
107
109
|
event.stopPropagation();
|
|
108
|
-
await
|
|
110
|
+
await removeView(id);
|
|
109
111
|
onSearch();
|
|
110
|
-
}, [
|
|
112
|
+
}, [onSearch, removeView]);
|
|
111
113
|
const onFavourite = useCallback(async (event, id) => {
|
|
112
114
|
event.preventDefault();
|
|
113
115
|
if (user.favourite_views?.includes(id)) {
|
|
114
|
-
await
|
|
116
|
+
await removeFavourite(id);
|
|
115
117
|
if (user.favourite_views?.length < 2) {
|
|
116
118
|
setFavouritesOnly(false);
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
else {
|
|
120
|
-
await
|
|
122
|
+
await addFavourite(id);
|
|
123
|
+
}
|
|
124
|
+
}, [addFavourite, removeFavourite, user.favourite_views]);
|
|
125
|
+
const onDefaultViewOpen = useCallback(async () => {
|
|
126
|
+
setDefaultViewOpen(true);
|
|
127
|
+
setDefaultViewLoading(true);
|
|
128
|
+
try {
|
|
129
|
+
await fetchViews();
|
|
121
130
|
}
|
|
122
|
-
|
|
131
|
+
finally {
|
|
132
|
+
setDefaultViewLoading(false);
|
|
133
|
+
}
|
|
134
|
+
}, [fetchViews]);
|
|
123
135
|
useEffect(() => {
|
|
124
|
-
onSearch();
|
|
125
136
|
if (!searchParams.has('offset')) {
|
|
126
137
|
searchParams.set('offset', '0');
|
|
127
138
|
setSearchParams(searchParams, { replace: true });
|
|
128
139
|
}
|
|
129
|
-
|
|
130
|
-
}, [dispatchApi, types]);
|
|
140
|
+
}, [searchParams, setSearchParams]);
|
|
131
141
|
useEffect(() => {
|
|
132
142
|
if (response?.total <= offset) {
|
|
133
143
|
setOffset(0);
|
|
@@ -140,13 +150,14 @@ const ViewsBase = () => {
|
|
|
140
150
|
onSearch();
|
|
141
151
|
}
|
|
142
152
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
|
-
}, [offset, favouritesOnly]);
|
|
153
|
+
}, [offset, favouritesOnly, types]);
|
|
144
154
|
return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, searchFilters: _jsx(Stack, { direction: "row", spacing: 1, alignItems: "center", children: _jsxs(ToggleButtonGroup, { sx: { display: 'grid', gridTemplateColumns: '1fr 1fr' }, size: "small", value: types, onChange: (__, _types) => {
|
|
145
155
|
if (_types) {
|
|
146
156
|
setTypes(_types.length < 2 ? _types : []);
|
|
147
157
|
}
|
|
148
|
-
}, children: [_jsx(ToggleButton, { value: "personal", "aria-label": "personal", children: t('route.views.manager.personal') }), _jsx(ToggleButton, { value: "global", "aria-label": "global", children: t('route.views.manager.global') })] }) }), aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.views.search.prompt') }), afterSearch: views
|
|
149
|
-
|
|
158
|
+
}, children: [_jsx(ToggleButton, { value: "personal", "aria-label": "personal", children: t('route.views.manager.personal') }), _jsx(ToggleButton, { value: "global", "aria-label": "global", children: t('route.views.manager.global') })] }) }), aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.views.search.prompt') }), afterSearch: size(views) > 0 ? (_jsx(Autocomplete, { open: defaultViewOpen, loading: defaultViewLoading, onOpen: onDefaultViewOpen, onClose: () => setDefaultViewOpen(false), options: Object.values(omitBy(views, isNull)), renderOption: ({ key, ...props }, o) => (_createElement("li", { ...props, key: key },
|
|
159
|
+
_jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(o.title) }), _jsx(Typography, { variant: "caption", children: _jsx("code", { children: o.query }) })] }))), renderInput: params => (_jsx(TextField, { ...params, label: t('route.views.manager.default'), sx: { minWidth: '300px' } })), filterOptions: (_views, { inputValue }) => _views.filter(v => t(v.title).toLowerCase().includes(inputValue.toLowerCase()) ||
|
|
160
|
+
v.query.toLowerCase().includes(inputValue.toLowerCase())), getOptionLabel: (v) => t(v.title), isOptionEqualToValue: (view, value) => view.view_id === value.view_id, value: views[defaultView] ?? null, onChange: (_, option) => setDefaultView(option?.view_id) })) : (_jsx(Skeleton, { variant: "rounded", width: "300px", height: "initial" })), belowSearch: _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Checkbox, { size: "small", disabled: user.favourite_views?.length < 1, checked: favouritesOnly, onChange: (_, checked) => setFavouritesOnly(checked) }), _jsx(Typography, { variant: "body1", sx: theme => ({ color: theme.palette.text.disabled }), children: t('route.views.manager.favourites') })] }), renderer: ({ item }, classRenderer) => (_jsx(Card, { variant: "outlined", sx: { p: 1, mb: 1, transitionProperty: 'border-color', '&:hover': { borderColor: 'primary.main' } }, className: classRenderer(), children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { color: 'inherit', textDecoration: 'none' }, component: Link, to: `/views/${item.item.view_id}`, children: [_jsx(ViewTitle, { ...item.item }), _jsx(FlexOne, {}), ((item.item.owner === user.username && item.item.type !== 'readonly') ||
|
|
150
161
|
(item.item.type === 'global' && user.is_admin)) && (_jsx(Tooltip, { title: t('button.edit'), children: _jsx(IconButton, { component: Link, to: `/views/${item.item.view_id}/edit?query=${item.item.query}`, children: _jsx(Edit, {}) }) })), item.item.owner === user.username && item.item.type !== 'readonly' && (_jsx(Tooltip, { title: t('button.delete'), children: _jsx(IconButton, { onClick: event => onDelete(event, item.item.view_id), children: _jsx(Clear, {}) }) })), item.item.type === 'global' && item.item.owner !== user.username && (_jsx(Tooltip, { title: item.item.owner, children: _jsx("div", { children: _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important', marginLeft: '8px !important' }, userId: item.item.owner }) }) })), _jsx(Tooltip, { title: t('button.pin'), children: _jsx(IconButton, { onClick: e => onFavourite(e, item.item.view_id), children: user.favourite_views?.includes(item.item.view_id) ? _jsx(Star, {}) : _jsx(StarBorder, {}) }) })] }) }, item.item.view_id)), response: response, searchPrompt: "route.views.manager.search", onCreate: () => navigate('/views/create'), createPrompt: "route.views.create", createIcon: _jsx(SavedSearch, { sx: { mr: 1 } }) }));
|
|
151
162
|
};
|
|
152
163
|
const Views = () => {
|
package/package.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@mui/x-date-pickers": "^7.29.4",
|
|
28
28
|
"ajv": "^8.17.1",
|
|
29
29
|
"ajv-i18n": "^4.2.0",
|
|
30
|
-
"axios": "^1.
|
|
30
|
+
"axios": "^1.11.0",
|
|
31
31
|
"axios-retry": "^3.9.1",
|
|
32
32
|
"borealis-ui": "0.11.0",
|
|
33
33
|
"chart.js": "^4.5.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"json2mq": "^0.2.0",
|
|
47
47
|
"lodash-es": "^4.17.21",
|
|
48
48
|
"md5": "^2.3.0",
|
|
49
|
-
"mermaid": "^11.
|
|
49
|
+
"mermaid": "^11.9.0",
|
|
50
50
|
"moment": "^2.30.1",
|
|
51
51
|
"monaco-editor": "^0.49.0",
|
|
52
52
|
"notistack": "^3.0.2",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"internal-slot": "1.0.7"
|
|
97
97
|
},
|
|
98
98
|
"type": "module",
|
|
99
|
-
"version": "2.
|
|
99
|
+
"version": "2.13.0-dev.103",
|
|
100
100
|
"exports": {
|
|
101
101
|
"./i18n": "./i18n.js",
|
|
102
102
|
"./index.css": "./index.css",
|
|
@@ -201,6 +201,10 @@
|
|
|
201
201
|
"./components/routes/analytics/widgets/*": "./components/routes/analytics/widgets/*.js",
|
|
202
202
|
"./components/logins/hooks/*": "./components/logins/hooks/*.js",
|
|
203
203
|
"./components/logins/auth/*": "./components/logins/auth/*.js",
|
|
204
|
+
"./plugins/borealis/*": "./plugins/borealis/*.js",
|
|
205
|
+
"./plugins/borealis": "./plugins/borealis/index.js",
|
|
206
|
+
"./plugins/borealis/components/*": "./plugins/borealis/components/*.js",
|
|
207
|
+
"./plugins/borealis/locales/*": "./plugins/borealis/locales/*.js",
|
|
204
208
|
"./api/search/*": "./api/search/*.js",
|
|
205
209
|
"./api/search": "./api/search/index.js",
|
|
206
210
|
"./api/analytic/*": "./api/analytic/*.js",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { BorealisProvider } from 'borealis-ui/dist/hooks/BorealisProvider';
|
|
3
|
+
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
4
|
+
import { useContext } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
7
|
+
import { getStored } from '@cccsaurora/howler-ui/utils/localStorage';
|
|
8
|
+
const Provider = ({ children }) => {
|
|
9
|
+
const apiConfig = useContext(ApiConfigContext);
|
|
10
|
+
return (_jsx(BorealisProvider, { baseURL: location.origin + '/api/v1/borealis', getToken: () => getStored(StorageKey.APP_TOKEN), enabled: apiConfig.config?.configuration?.features?.borealis, publicIconify: false, customIconify: location.origin.includes('localhost')
|
|
11
|
+
? 'https://icons.dev.analysis.cyber.gc.ca'
|
|
12
|
+
: location.origin.replace(/howler(-stg)?/, 'icons'), defaultTimeout: 5, i18next: useTranslation('borealis'), chunkSize: 50, children: children }));
|
|
13
|
+
};
|
|
14
|
+
export default Provider;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Chip } from '@mui/material';
|
|
3
|
+
import { EnrichedChip, useBorealisEnrichSelector } from 'borealis-ui';
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
const BorealisChip = ({ children, value, context, ...props }) => {
|
|
6
|
+
const guessType = useBorealisEnrichSelector(ctx => ctx.guessType);
|
|
7
|
+
const type = guessType(value);
|
|
8
|
+
if (!type) {
|
|
9
|
+
return _jsx(Chip, { ...props, children: children });
|
|
10
|
+
}
|
|
11
|
+
let enrichedProps = {
|
|
12
|
+
...props,
|
|
13
|
+
value
|
|
14
|
+
};
|
|
15
|
+
delete enrichedProps.label;
|
|
16
|
+
if (context === 'summary') {
|
|
17
|
+
enrichedProps = {
|
|
18
|
+
...enrichedProps,
|
|
19
|
+
sx: [
|
|
20
|
+
...(Array.isArray(enrichedProps.sx) ? enrichedProps.sx : [enrichedProps.sx]),
|
|
21
|
+
[{ height: '24px', '& .iconify': { fontSize: '1em' } }]
|
|
22
|
+
]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return _jsx(EnrichedChip, { ...enrichedProps, type: type });
|
|
26
|
+
};
|
|
27
|
+
export default memo(BorealisChip);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Autocomplete, Divider, ListItemText, TextField, Typography } from '@mui/material';
|
|
3
|
+
import { useBorealisEnrichSelector, useBorealisFetcherSelector } from 'borealis-ui';
|
|
4
|
+
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
5
|
+
import { useContext, useState } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
const BorealisLeadForm = ({ lead, metadata, update, updateMetadata }) => {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
const { config } = useContext(ApiConfigContext);
|
|
10
|
+
const fetchers = useBorealisFetcherSelector(ctx => ctx.fetchers);
|
|
11
|
+
const types = useBorealisEnrichSelector(ctx => ctx.typesDetection);
|
|
12
|
+
const [showCustom, setShowCustom] = useState(false);
|
|
13
|
+
return (_jsxs(_Fragment, { children: [_jsx(Divider, { orientation: "horizontal" }), _jsx(Autocomplete, { disabled: !lead, options: Object.keys(fetchers), renderInput: params => _jsx(TextField, { ...params, size: "small", label: t('route.dossiers.manager.borealis') }), value: Object.keys(fetchers).includes(lead?.content) ? lead.content : '', onChange: (_ev, content) => update({ content, metadata: '{}' }), renderOption: (props, option) => (_jsx(ListItemText, { ...props, sx: { flexDirection: 'column', alignItems: 'start !important' }, primary: _jsx("code", { children: option }), secondary: fetchers[option].description })) }), _jsx(Autocomplete, { options: Object.keys(types), renderInput: params => _jsx(TextField, { ...params, size: "small", label: t('route.dossiers.manager.borealis.type') }), value: metadata?.type ?? '', onChange: (_ev, type) => updateMetadata({ type }) }), _jsx(Autocomplete, { options: ['custom', ...Object.keys(config.indexes.hit)], disabled: !metadata?.type || !types[metadata.type], renderInput: params => (_jsx(TextField, { ...params, size: "small", label: t('route.dossiers.manager.borealis.value') })), getOptionLabel: opt => t(opt), value: metadata?.value ?? '', onChange: (_ev, value) => {
|
|
14
|
+
if (value === 'custom') {
|
|
15
|
+
setShowCustom(true);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
setShowCustom(false);
|
|
19
|
+
updateMetadata({ value });
|
|
20
|
+
}
|
|
21
|
+
} }), showCustom && (_jsxs(_Fragment, { children: [_jsx(TextField, { size: "small", label: t('route.dossiers.manager.borealis.value.custom'), value: metadata?.value ?? '', disabled: !metadata?.type || !types[metadata.type], fullWidth: true, onChange: ev => updateMetadata({ value: ev.target.value }) }), _jsx(Typography, { variant: "caption", color: "text.secondary", sx: { mt: '0 !important' }, children: t('route.dossiers.manager.borealis.value.description') })] }))] }));
|
|
22
|
+
};
|
|
23
|
+
export default BorealisLeadForm;
|