@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.
Files changed (48) hide show
  1. package/api/search/facet/hit.d.ts +4 -2
  2. package/api/search/facet/hit.js +5 -5
  3. package/api/search/facet/index.d.ts +1 -0
  4. package/components/app/providers/FavouritesProvider.js +27 -30
  5. package/components/app/providers/HitSearchProvider.js +7 -10
  6. package/components/app/providers/ViewProvider.d.ts +5 -4
  7. package/components/app/providers/ViewProvider.js +58 -36
  8. package/components/elements/hit/HitActions.js +1 -1
  9. package/components/elements/hit/HitSummary.js +5 -5
  10. package/components/elements/hit/aggregate/HitGraph.js +6 -9
  11. package/components/routes/advanced/luceneCompletionProvider.js +4 -2
  12. package/components/routes/analytics/widgets/Assessment.js +3 -2
  13. package/components/routes/analytics/widgets/Escalation.js +4 -3
  14. package/components/routes/analytics/widgets/Stacked.js +4 -3
  15. package/components/routes/hits/search/HitBrowser.js +8 -8
  16. package/components/routes/hits/search/SearchPane.js +1 -1
  17. package/components/routes/hits/search/ViewLink.js +3 -2
  18. package/components/routes/hits/search/grid/HitGrid.js +5 -7
  19. package/components/routes/hits/search/shared/HitFilter.js +2 -2
  20. package/components/routes/hits/search/shared/HitSort.js +9 -8
  21. package/components/routes/hits/search/shared/QuerySettings.js +1 -1
  22. package/components/routes/hits/search/shared/SearchSpan.js +17 -13
  23. package/components/routes/home/AddNewCard.js +14 -1
  24. package/components/routes/home/ViewCard.js +5 -1
  25. package/components/routes/home/index.js +1 -1
  26. package/components/routes/views/ViewComposer.js +17 -16
  27. package/components/routes/views/Views.js +25 -14
  28. package/package.json +5 -1
  29. package/plugins/borealis/Provider.d.ts +3 -0
  30. package/plugins/borealis/Provider.js +14 -0
  31. package/plugins/borealis/components/BorealisChip.d.ts +3 -0
  32. package/plugins/borealis/components/BorealisChip.js +27 -0
  33. package/plugins/borealis/components/BorealisLeadForm.d.ts +4 -0
  34. package/plugins/borealis/components/BorealisLeadForm.js +23 -0
  35. package/plugins/borealis/components/BorealisPivot.d.ts +3 -0
  36. package/plugins/borealis/components/BorealisPivot.js +83 -0
  37. package/plugins/borealis/components/BorealisPivotForm.d.ts +4 -0
  38. package/plugins/borealis/components/BorealisPivotForm.js +44 -0
  39. package/plugins/borealis/components/BorealisTypography.d.ts +3 -0
  40. package/plugins/borealis/components/BorealisTypography.js +53 -0
  41. package/plugins/borealis/helpers.d.ts +6 -0
  42. package/plugins/borealis/helpers.js +137 -0
  43. package/plugins/borealis/index.d.ts +21 -0
  44. package/plugins/borealis/index.js +46 -0
  45. package/plugins/borealis/locales/borealis.en.json +7 -0
  46. package/plugins/borealis/locales/borealis.fr.json +7 -0
  47. package/plugins/borealis/setup.d.ts +2 -0
  48. 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(_category, { query: 'howler.id:*' });
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, useParams } from 'react-router-dom';
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 routeParams = useParams();
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 (viewId) {
55
- const selectedView = views.find(_view => _view.view_id === viewId);
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
- }, [views, viewId]);
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?.find(val => val.view_id === viewId));
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, useMemo } from 'react';
5
+ import { memo, useEffect } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
- import { useLocation, useParams } from 'react-router-dom';
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 viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : null), [location.pathname, routeParams.id]);
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 (!selectedView?.span || location.search.includes('span')) {
25
+ if (location.search.includes('span')) {
28
26
  return;
29
27
  }
30
- if (selectedView.span.includes(':')) {
31
- setSpan(convertLuceneToDate(selectedView.span));
32
- }
33
- else {
34
- setSpan(selectedView.span);
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
- }, [selectedView]);
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?.find(_view => _view.view_id === viewId));
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;
@@ -77,7 +77,7 @@ const Home = () => {
77
77
  api.search.hit
78
78
  .post({
79
79
  query: updateQuery,
80
- rows: 5
80
+ rows: 0
81
81
  })
82
82
  .then(result => setUpdatedHitTotal(result.total));
83
83
  }, [updateQuery]);
@@ -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 views = useContextSelector(ViewContext, ctx => ctx.views);
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
- const viewToEdit = views?.find(_view => _view.view_id === routeParams.id);
135
- if (!viewToEdit && views?.length > 0) {
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
- if (viewToEdit) {
143
- setTitle(viewToEdit.title);
144
- setAdvanceOnTriage(viewToEdit.settings?.advance_on_triage ?? false);
145
- setQuery(viewToEdit.query);
146
- if (viewToEdit.sort) {
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, views]);
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 dispatchApi(removeView(id));
110
+ await removeView(id);
109
111
  onSearch();
110
- }, [dispatchApi, onSearch, removeView]);
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 dispatchApi(removeFavourite(id));
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 dispatchApi(addFavourite(id));
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
- }, [addFavourite, dispatchApi, removeFavourite, user.favourite_views]);
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
- // eslint-disable-next-line react-hooks/exhaustive-deps
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?.length > 0 ? (_jsx(Autocomplete, { options: views, renderOption: (props, o) => (_jsx("li", { ...props, children: _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()) ||
149
- v.query.toLowerCase().includes(inputValue.toLowerCase())), getOptionLabel: (v) => t(v.title), isOptionEqualToValue: (view, value) => view.view_id === value.view_id, value: views?.find(v => v.view_id === 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') ||
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.12.1",
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,3 @@
1
+ import { type PropsWithChildren } from 'react';
2
+ declare const Provider: React.FC<PropsWithChildren<{}>>;
3
+ export default Provider;
@@ -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,3 @@
1
+ import type { PluginChipProps } from '@cccsaurora/howler-ui/components/elements/PluginChip';
2
+ declare const _default: import("react").NamedExoticComponent<PluginChipProps>;
3
+ export default _default;
@@ -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,4 @@
1
+ import type { LeadFormProps } from '@cccsaurora/howler-ui/components/routes/dossiers/LeadEditor';
2
+ import { type FC } from 'react';
3
+ declare const BorealisLeadForm: FC<LeadFormProps>;
4
+ export default BorealisLeadForm;
@@ -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,3 @@
1
+ import type { PivotLinkProps } from '@cccsaurora/howler-ui/components/elements/hit/related/PivotLink';
2
+ declare const _default: import("react").NamedExoticComponent<PivotLinkProps>;
3
+ export default _default;
@@ -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,4 @@
1
+ import type { PivotFormProps } from '@cccsaurora/howler-ui/components/routes/dossiers/PivotForm';
2
+ import { type FC } from 'react';
3
+ declare const BorealisPivotForm: FC<PivotFormProps>;
4
+ export default BorealisPivotForm;
@@ -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;