@cccsaurora/howler-ui 2.12.1 → 2.13.0-dev.102

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 (49) 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/HitQuery.js +1 -1
  17. package/components/routes/hits/search/SearchPane.js +1 -1
  18. package/components/routes/hits/search/ViewLink.js +3 -2
  19. package/components/routes/hits/search/grid/HitGrid.js +5 -7
  20. package/components/routes/hits/search/shared/HitFilter.js +2 -2
  21. package/components/routes/hits/search/shared/HitSort.js +9 -8
  22. package/components/routes/hits/search/shared/QuerySettings.js +1 -1
  23. package/components/routes/hits/search/shared/SearchSpan.js +17 -13
  24. package/components/routes/home/AddNewCard.js +14 -1
  25. package/components/routes/home/ViewCard.js +5 -1
  26. package/components/routes/home/index.js +1 -1
  27. package/components/routes/views/ViewComposer.js +17 -16
  28. package/components/routes/views/Views.js +25 -14
  29. package/package.json +7 -3
  30. package/plugins/borealis/Provider.d.ts +3 -0
  31. package/plugins/borealis/Provider.js +14 -0
  32. package/plugins/borealis/components/BorealisChip.d.ts +3 -0
  33. package/plugins/borealis/components/BorealisChip.js +27 -0
  34. package/plugins/borealis/components/BorealisLeadForm.d.ts +4 -0
  35. package/plugins/borealis/components/BorealisLeadForm.js +23 -0
  36. package/plugins/borealis/components/BorealisPivot.d.ts +3 -0
  37. package/plugins/borealis/components/BorealisPivot.js +83 -0
  38. package/plugins/borealis/components/BorealisPivotForm.d.ts +4 -0
  39. package/plugins/borealis/components/BorealisPivotForm.js +44 -0
  40. package/plugins/borealis/components/BorealisTypography.d.ts +3 -0
  41. package/plugins/borealis/components/BorealisTypography.js +53 -0
  42. package/plugins/borealis/helpers.d.ts +6 -0
  43. package/plugins/borealis/helpers.js +137 -0
  44. package/plugins/borealis/index.d.ts +21 -0
  45. package/plugins/borealis/index.js +46 -0
  46. package/plugins/borealis/locales/borealis.en.json +7 -0
  47. package/plugins/borealis/locales/borealis.fr.json +7 -0
  48. package/plugins/borealis/setup.d.ts +2 -0
  49. 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 viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : null), [location.pathname, routeParams.id]);
45
- const selectedView = useContextSelector(ViewContext, ctx => ctx.views?.find(val => val.view_id === viewId));
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 !== routeParams.id) {
60
+ if (selectedHits.length === 1 && selected && selectedHits[0]?.howler.id !== selected) {
63
61
  return true;
64
62
  }
65
63
  return false;
66
- }, [routeParams.id, selectedHits]);
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(_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
@@ -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.10.0",
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.8.1",
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.12.1",
99
+ "version": "2.13.0-dev.102",
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;