@cccsaurora/howler-ui 2.12.1 → 2.13.0-dev.77
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 +5 -5
- 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/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 +5 -1
- 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
|
@@ -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
|
@@ -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.77",
|
|
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;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Icon } from '@iconify/react/dist/iconify.js';
|
|
3
|
+
import { Settings } from '@mui/icons-material';
|
|
4
|
+
import { Divider, IconButton, Stack, Typography } from '@mui/material';
|
|
5
|
+
import { useBorealisActionsSelector, useBorealisEnrichSelector } from 'borealis-ui';
|
|
6
|
+
import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
|
|
7
|
+
import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
|
|
8
|
+
import { get } from 'lodash-es';
|
|
9
|
+
import { memo, useCallback, useState } from 'react';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
const BorealisPivot = ({ pivot, hit, compact }) => {
|
|
12
|
+
const guessType = useBorealisEnrichSelector(ctx => ctx?.guessType);
|
|
13
|
+
const { showErrorMessage } = useMySnackbar();
|
|
14
|
+
const { i18n, t } = useTranslation();
|
|
15
|
+
const actions = useBorealisActionsSelector(ctx => ctx?.availableActions ?? {});
|
|
16
|
+
const executeAction = useBorealisActionsSelector(ctx => ctx?.executeAction);
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
const onBorealisClick = useCallback(async (event, forceMenu = false) => {
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
event.stopPropagation();
|
|
21
|
+
if (loading) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (!actions[pivot.value]) {
|
|
25
|
+
showErrorMessage(t('pivot.borealis.missing'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
setLoading(true);
|
|
29
|
+
const data = Object.fromEntries(pivot.mappings.map(_mapping => {
|
|
30
|
+
const value = _mapping.field !== 'custom' ? get(hit, _mapping.field) : _mapping.custom_value;
|
|
31
|
+
if (['selector', 'selectors'].includes(_mapping.key)) {
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return [
|
|
34
|
+
_mapping.key,
|
|
35
|
+
value.map(val => ({
|
|
36
|
+
// TODO: Use the mapped borealis values here eventually
|
|
37
|
+
type: guessType(val),
|
|
38
|
+
value: val
|
|
39
|
+
}))
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
return [
|
|
43
|
+
_mapping.key,
|
|
44
|
+
{
|
|
45
|
+
// TODO: Use the mapped borealis values here eventually
|
|
46
|
+
type: guessType(value),
|
|
47
|
+
value
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
return [_mapping.key, value];
|
|
52
|
+
}));
|
|
53
|
+
const selectors = (actions[pivot.value].accept_multiple ? [data.selectors] : [data.selector]).flat();
|
|
54
|
+
delete data.selector;
|
|
55
|
+
delete data.selectors;
|
|
56
|
+
try {
|
|
57
|
+
await executeAction(pivot.value, selectors, data, { forceMenu });
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
setLoading(false);
|
|
61
|
+
}
|
|
62
|
+
}, [actions, executeAction, guessType, hit, loading, pivot.mappings, pivot.value, showErrorMessage, t]);
|
|
63
|
+
if (!actions[pivot.value]) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
return (_jsx(HowlerCard, { variant: compact ? 'outlined' : 'elevation', onClick: e => onBorealisClick(e), sx: [
|
|
67
|
+
theme => ({
|
|
68
|
+
backgroundColor: 'transparent',
|
|
69
|
+
transition: theme.transitions.create(['border-color']),
|
|
70
|
+
'&:hover': { borderColor: 'primary.main' },
|
|
71
|
+
'& > div': {
|
|
72
|
+
height: '100%'
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
loading
|
|
76
|
+
? { opacity: 0.5, pointerEvents: 'none' }
|
|
77
|
+
: {
|
|
78
|
+
cursor: 'pointer'
|
|
79
|
+
},
|
|
80
|
+
!compact && { border: 'thin solid', borderColor: 'transparent' }
|
|
81
|
+
], children: _jsxs(Stack, { direction: "row", p: compact ? 0.5 : 1, spacing: 1, alignItems: "center", children: [_jsx(Icon, { fontSize: "1.5rem", icon: pivot.icon }), _jsx(Typography, { children: pivot.label[i18n.language] }), _jsx(Divider, { orientation: "vertical", flexItem: true }), _jsx(IconButton, { size: "small", onClick: e => onBorealisClick(e, true), children: _jsx(Settings, { fontSize: "small" }) })] }) }));
|
|
82
|
+
};
|
|
83
|
+
export default memo(BorealisPivot);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Autocomplete, Divider, Stack, TextField, Typography, useTheme } from '@mui/material';
|
|
3
|
+
import { useBorealisActionsSelector } from 'borealis-ui';
|
|
4
|
+
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
5
|
+
import { Fragment, useContext } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
const BorealisPivotForm = ({ pivot, update }) => {
|
|
8
|
+
const theme = useTheme();
|
|
9
|
+
const { t } = useTranslation();
|
|
10
|
+
const { config } = useContext(ApiConfigContext);
|
|
11
|
+
const actions = useBorealisActionsSelector(ctx => ctx?.availableActions);
|
|
12
|
+
return (_jsxs(_Fragment, { children: [_jsx(Autocomplete, { fullWidth: true, disabled: !pivot, options: Object.entries(actions)
|
|
13
|
+
.filter(([_key, definition]) => !!definition && definition.format == 'pivot')
|
|
14
|
+
.map(([key]) => key), renderOption: ({ key, ...optionProps }, actionId) => {
|
|
15
|
+
const definition = actions[actionId];
|
|
16
|
+
return (_jsxs(Stack, { component: "li", ...optionProps, spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignSelf: "start", alignItems: "center", children: [_jsx(Typography, { children: definition.name }), _jsx("pre", { style: {
|
|
17
|
+
fontSize: '0.85rem',
|
|
18
|
+
border: `thin solid ${theme.palette.divider}`,
|
|
19
|
+
padding: theme.spacing(0.5),
|
|
20
|
+
borderRadius: theme.shape.borderRadius
|
|
21
|
+
}, children: actionId })] }), _jsx(Typography, { variant: "body2", color: "text.secondary", alignSelf: "start", children: definition.summary })] }, key));
|
|
22
|
+
}, getOptionLabel: opt => actions[opt]?.name ?? '', renderInput: params => (_jsx(TextField, { ...params, size: "small", fullWidth: true, label: t('route.dossiers.manager.pivot.value') })), value: pivot?.value ?? '', onChange: (_ev, value) => update({
|
|
23
|
+
value,
|
|
24
|
+
mappings: [
|
|
25
|
+
{ key: actions[value].accept_multiple ? 'selectors' : 'selector', field: 'howler.id' },
|
|
26
|
+
...Object.entries(actions[value].params?.properties ?? [])
|
|
27
|
+
.filter(([property]) => !['selector', 'selectors'].includes(property))
|
|
28
|
+
.map(([prop, schema]) => ({
|
|
29
|
+
key: prop,
|
|
30
|
+
field: typeof schema === 'boolean' || !schema.default ? null : 'custom',
|
|
31
|
+
custom_value: typeof schema !== 'boolean' && !!schema.default ? schema.default.toString() : null
|
|
32
|
+
}))
|
|
33
|
+
]
|
|
34
|
+
}) }), _jsx(Divider, { flexItem: true }), _jsx(Typography, { children: t('route.dossiers.manager.pivot.mappings') }), pivot?.mappings?.map((_mapping, index) => (
|
|
35
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
36
|
+
_jsxs(Fragment, { children: [_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(TextField, { size: "small", label: t('route.dossiers.manager.pivot.mapping.key'), disabled: !pivot, value: _mapping?.key ?? '', onChange: ev => update({
|
|
37
|
+
mappings: pivot.mappings.map((_m, _index) => index === _index ? { ..._m, key: ev.target.value } : _m)
|
|
38
|
+
}) }), _jsx(Autocomplete, { fullWidth: true, disabled: !pivot, options: ['custom', 'unset', ...Object.keys(config.indexes.hit)], renderInput: params => (_jsx(TextField, { ...params, size: "small", fullWidth: true, label: t('route.dossiers.manager.pivot.mapping.field'), sx: { minWidth: '150px' } })), getOptionLabel: opt => t(opt), value: _mapping.field ?? '', onChange: (_ev, field) => update({
|
|
39
|
+
mappings: pivot.mappings.map((_m, _index) => (index === _index ? { ..._m, field } : _m))
|
|
40
|
+
}) })] }), _mapping.field === 'custom' && (_jsx(TextField, { size: "small", label: t('route.dossiers.manager.pivot.mapping.custom'), disabled: !pivot, value: _mapping?.custom_value ?? '', onChange: ev => update({
|
|
41
|
+
mappings: pivot.mappings.map((_m, _index) => index === _index ? { ..._m, custom_value: ev.target.value } : _m)
|
|
42
|
+
}) }))] }, index)))] }));
|
|
43
|
+
};
|
|
44
|
+
export default BorealisPivotForm;
|