@cccsaurora/howler-ui 2.13.0-dev.96 → 2.13.1

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 (118) hide show
  1. package/api/hit/index.d.ts +1 -1
  2. package/api/hit/index.js +6 -2
  3. package/api/search/index.d.ts +1 -0
  4. package/api/view/index.d.ts +1 -1
  5. package/api/view/index.js +2 -2
  6. package/commons/components/notification/elements/item/NotificationItemDate.js +2 -2
  7. package/commons/components/utils/hooks/useEnv.d.ts +1 -1
  8. package/components/app/App.js +1 -3
  9. package/components/app/drawers/ApiKeyDrawer.js +4 -4
  10. package/components/app/hooks/useMatchers.d.ts +9 -0
  11. package/components/app/hooks/useMatchers.js +82 -0
  12. package/components/app/hooks/useMatchers.test.d.ts +1 -0
  13. package/components/app/hooks/useMatchers.test.js +237 -0
  14. package/components/app/hooks/useTitle.js +5 -4
  15. package/components/app/providers/AnalyticProvider.d.ts +0 -4
  16. package/components/app/providers/AnalyticProvider.js +1 -44
  17. package/components/app/providers/ApiConfigProvider.js +2 -1
  18. package/components/app/providers/HitProvider.d.ts +2 -1
  19. package/components/app/providers/HitProvider.js +8 -2
  20. package/components/app/providers/HitSearchProvider.d.ts +2 -1
  21. package/components/app/providers/HitSearchProvider.js +2 -1
  22. package/components/app/providers/SocketProvider.js +1 -1
  23. package/components/app/providers/ViewProvider.d.ts +1 -1
  24. package/components/app/providers/ViewProvider.js +3 -3
  25. package/components/app/providers/ViewProvider.test.d.ts +1 -0
  26. package/components/app/providers/ViewProvider.test.js +150 -0
  27. package/components/elements/display/ActionButton.d.ts +8 -0
  28. package/components/elements/display/ActionButton.js +18 -0
  29. package/components/elements/display/handlebars/helpers.js +17 -2
  30. package/components/elements/display/json/JSONViewer.d.ts +2 -0
  31. package/components/elements/display/json/JSONViewer.js +6 -13
  32. package/components/elements/hit/HitActions.js +4 -6
  33. package/components/elements/hit/HitBanner.js +13 -5
  34. package/components/elements/hit/HitCard.js +0 -5
  35. package/components/elements/hit/HitComments.js +5 -4
  36. package/components/elements/hit/HitOutline.d.ts +2 -2
  37. package/components/elements/hit/HitOutline.js +11 -21
  38. package/components/elements/hit/HitOverview.js +7 -4
  39. package/components/elements/hit/HitSummary.d.ts +2 -1
  40. package/components/elements/hit/HitSummary.js +8 -7
  41. package/components/elements/hit/aggregate/HitGraph.d.ts +1 -1
  42. package/components/elements/hit/aggregate/HitGraph.js +7 -7
  43. package/components/elements/hit/elements/HitTimestamp.js +8 -8
  44. package/components/elements/hit/related/PivotLink.js +11 -5
  45. package/components/elements/hit/related/RelatedIcon.d.ts +8 -0
  46. package/components/elements/hit/related/RelatedIcon.js +32 -0
  47. package/components/elements/hit/related/RelatedLink.js +4 -25
  48. package/components/hooks/useMyChart.d.ts +1 -1
  49. package/components/hooks/useMyChart.js +1 -1
  50. package/components/routes/advanced/QueryBuilder.js +47 -11
  51. package/components/routes/advanced/QueryEditor.js +8 -13
  52. package/components/routes/analytics/AnalyticOverview.d.ts +1 -1
  53. package/components/routes/analytics/AnalyticOverview.js +1 -1
  54. package/components/routes/analytics/AnalyticOverviews.d.ts +1 -1
  55. package/components/routes/analytics/AnalyticOverviews.js +1 -1
  56. package/components/routes/analytics/AnalyticSearch.js +14 -2
  57. package/components/routes/analytics/AnalyticTemplates.d.ts +1 -1
  58. package/components/routes/analytics/AnalyticTemplates.js +10 -7
  59. package/components/routes/analytics/RuleView.d.ts +1 -1
  60. package/components/routes/analytics/RuleView.js +1 -1
  61. package/components/routes/analytics/TriageSettings.d.ts +1 -1
  62. package/components/routes/analytics/TriageSettings.js +1 -1
  63. package/components/routes/analytics/widgets/Assessment.d.ts +1 -1
  64. package/components/routes/analytics/widgets/Assessment.js +1 -1
  65. package/components/routes/analytics/widgets/Created.d.ts +1 -1
  66. package/components/routes/analytics/widgets/Created.js +1 -1
  67. package/components/routes/analytics/widgets/Detection.d.ts +1 -1
  68. package/components/routes/analytics/widgets/Detection.js +1 -1
  69. package/components/routes/analytics/widgets/Escalation.d.ts +1 -1
  70. package/components/routes/analytics/widgets/Escalation.js +1 -1
  71. package/components/routes/analytics/widgets/Stacked.d.ts +1 -1
  72. package/components/routes/analytics/widgets/Stacked.js +1 -1
  73. package/components/routes/analytics/widgets/Status.d.ts +1 -1
  74. package/components/routes/analytics/widgets/Status.js +1 -1
  75. package/components/routes/help/SearchDocumentation.js +2 -1
  76. package/components/routes/help/TemplateDocumentation.js +5 -5
  77. package/components/routes/hits/search/HitBrowser.js +2 -2
  78. package/components/routes/hits/search/HitContextMenu.js +6 -7
  79. package/components/routes/hits/search/HitQuery.js +2 -1
  80. package/components/routes/hits/search/InformationPane.js +75 -78
  81. package/components/routes/hits/search/SearchPane.js +3 -9
  82. package/components/routes/hits/search/grid/AddColumnModal.js +10 -5
  83. package/components/routes/hits/search/grid/HitGrid.js +6 -5
  84. package/components/routes/hits/search/shared/CustomSpan.js +6 -6
  85. package/components/routes/hits/view/HitViewer.js +18 -26
  86. package/components/routes/home/index.js +4 -4
  87. package/components/routes/overviews/OverviewViewer.js +33 -31
  88. package/components/routes/settings/SecuritySection.js +2 -2
  89. package/components/routes/templates/TemplateViewer.js +27 -36
  90. package/components/routes/templates/Templates.js +4 -11
  91. package/components/routes/views/ViewComposer.js +8 -1
  92. package/components/routes/views/Views.js +25 -9
  93. package/index.js +7 -0
  94. package/locales/en/help/search.json +17 -0
  95. package/locales/en/translation.json +12 -3
  96. package/locales/fr/help/search.json +17 -0
  97. package/locales/fr/translation.json +12 -4
  98. package/models/WithMetadata.d.ts +10 -0
  99. package/models/WithMetadata.js +1 -0
  100. package/models/entities/generated/ApiType.d.ts +7 -0
  101. package/package.json +112 -111
  102. package/plugins/borealis/components/BorealisTypography.js +4 -2
  103. package/setupTests.d.ts +1 -0
  104. package/setupTests.js +12 -0
  105. package/tests/MockLocalStorage.d.ts +5 -0
  106. package/tests/MockLocalStorage.js +44 -0
  107. package/tests/server-handlers.d.ts +5 -0
  108. package/tests/server-handlers.js +97 -0
  109. package/tests/server.d.ts +3 -0
  110. package/tests/server.js +5 -0
  111. package/utils/constants.js +2 -2
  112. package/utils/stringUtils.d.ts +1 -0
  113. package/utils/stringUtils.js +9 -0
  114. package/utils/utils.js +3 -3
  115. package/components/app/providers/DossierProvider.d.ts +0 -16
  116. package/components/app/providers/DossierProvider.js +0 -82
  117. package/components/app/providers/TemplateProvider.d.ts +0 -14
  118. package/components/app/providers/TemplateProvider.js +0 -103
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Close, ErrorOutline, List, TableChart, Terminal } from '@mui/icons-material';
2
+ import { Close, ErrorOutline, List, SavedSearch, TableChart, Terminal } from '@mui/icons-material';
3
3
  import { Box, IconButton, LinearProgress, Stack, ToggleButton, ToggleButtonGroup, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material';
4
4
  import { grey } from '@mui/material/colors';
5
5
  import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
@@ -7,7 +7,6 @@ import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCente
7
7
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
8
8
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
9
9
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
10
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
11
10
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
12
11
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
13
12
  import FlexPort from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexPort';
@@ -46,7 +45,7 @@ const Item = memo(({ hit, onClick }) => {
46
45
  }
47
46
  }, []);
48
47
  // Search result list item renderer.
49
- return (_jsx(Box, { id: hit.howler.id, onMouseUp: e => checkMiddleClick(e, hit.howler.id), onClick: ev => onClick(ev, hit), sx: [
48
+ return (_jsx(Box, { id: hit.howler.id, onAuxClick: e => checkMiddleClick(e, hit.howler.id), onClick: ev => onClick(ev, hit), sx: [
50
49
  {
51
50
  mb: 2,
52
51
  cursor: 'pointer',
@@ -82,7 +81,6 @@ const SearchPane = () => {
82
81
  const location = useLocation();
83
82
  const navigate = useNavigate();
84
83
  const routeParams = useParams();
85
- const refresh = useContextSelector(TemplateContext, ctx => ctx.refresh);
86
84
  const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
87
85
  const setSelected = useContextSelector(ParameterContext, ctx => ctx.setSelected);
88
86
  const query = useContextSelector(ParameterContext, ctx => ctx.query);
@@ -109,10 +107,6 @@ const SearchPane = () => {
109
107
  }
110
108
  return selectedElement.id;
111
109
  }, []);
112
- // Load the index field for a hit in order to provide autocomplete suggestions.
113
- useEffect(() => {
114
- refresh();
115
- }, [refresh]);
116
110
  useEffect(() => {
117
111
  if (location.pathname.startsWith('/bundles')) {
118
112
  getHit(routeParams.id);
@@ -126,7 +120,7 @@ const SearchPane = () => {
126
120
  ], onClick: () => {
127
121
  clearSelectedHits(bundleHit.howler.id);
128
122
  setSelected(bundleHit.howler.id);
129
- }, children: _jsx(HitBanner, { hit: bundleHit, layout: HitLayout.DENSE, useListener: true }) }) }) }) })), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", children: t('hit.search.prompt') }), error && (_jsx(Tooltip, { title: `${t('route.advanced.error')}: ${error}`, children: _jsx(ErrorOutline, { fontSize: "small", color: "error" }) })), _jsx(FlexOne, {}), bundleHit?.howler.bundles.length > 0 && _jsx(BundleParentMenu, { bundle: bundleHit }), bundleHit && (_jsx(Tooltip, { title: t('hit.bundle.close'), children: _jsx(IconButton, { size: "small", onClick: () => navigate('/search'), children: _jsx(Close, {}) }) })), _jsx(Tooltip, { title: t('route.actions.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/action/execute?query=${query}`, children: _jsx(Terminal, {}) }) }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", children: [_jsx(ToggleButton, { value: "list", children: _jsx(List, {}) }), _jsx(ToggleButton, { value: "grid", children: _jsx(TableChart, {}) })] })] })] }), _jsxs(VSBoxHeader, { ml: -3, mr: -3, px: 2, pb: 1, sx: { zIndex: 999 }, children: [_jsxs(Stack, { sx: { pt: 1 }, children: [_jsxs(Stack, { sx: { position: 'relative', flex: 1 }, children: [_jsx(HitQuery, { disabled: viewId && !selectedView, searching: searching, triggerSearch: triggerSearch }), searching && (_jsx(LinearProgress, { sx: theme => ({
123
+ }, children: _jsx(HitBanner, { hit: bundleHit, layout: HitLayout.DENSE, useListener: true }) }) }) }) })), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", children: t('hit.search.prompt') }), error && (_jsx(Tooltip, { title: `${t('route.advanced.error')}: ${error}`, children: _jsx(ErrorOutline, { fontSize: "small", color: "error" }) })), _jsx(FlexOne, {}), bundleHit?.howler.bundles.length > 0 && _jsx(BundleParentMenu, { bundle: bundleHit }), bundleHit && (_jsx(Tooltip, { title: t('hit.bundle.close'), children: _jsx(IconButton, { size: "small", onClick: () => navigate('/search'), children: _jsx(Close, {}) }) })), _jsx(Tooltip, { title: t('route.views.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/views/create?query=${query}`, children: _jsx(SavedSearch, {}) }) }), _jsx(Tooltip, { title: t('route.actions.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/action/execute?query=${query}`, children: _jsx(Terminal, {}) }) }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", children: [_jsx(ToggleButton, { value: "list", children: _jsx(List, {}) }), _jsx(ToggleButton, { value: "grid", children: _jsx(TableChart, {}) })] })] })] }), _jsxs(VSBoxHeader, { ml: -3, mr: -3, px: 2, pb: 1, sx: { zIndex: 999 }, children: [_jsxs(Stack, { sx: { pt: 1 }, children: [_jsxs(Stack, { sx: { position: 'relative', flex: 1 }, children: [_jsx(HitQuery, { disabled: viewId && !selectedView, searching: searching, triggerSearch: triggerSearch }), searching && (_jsx(LinearProgress, { sx: theme => ({
130
124
  position: 'absolute',
131
125
  left: 0,
132
126
  right: 0,
@@ -1,21 +1,26 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Add, Check } from '@mui/icons-material';
3
3
  import { Autocomplete, Chip, Divider, Grid, IconButton, Popover, Stack, TextField } from '@mui/material';
4
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
4
5
  import { FieldContext } from '@cccsaurora/howler-ui/components/app/providers/FieldProvider';
5
6
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
6
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
7
- import { sortBy, uniq } from 'lodash-es';
8
- import { memo, useContext, useMemo, useState } from 'react';
7
+ import { has, sortBy, uniq } from 'lodash-es';
8
+ import { memo, useContext, useEffect, useMemo, useState } from 'react';
9
9
  import { useTranslation } from 'react-i18next';
10
10
  import { useContextSelector } from 'use-context-selector';
11
11
  const AddColumnModal = ({ open, onClose, anchorEl, addColumn, columns }) => {
12
12
  const { t } = useTranslation();
13
13
  const { hitFields } = useContext(FieldContext);
14
14
  const response = useContextSelector(HitSearchContext, ctx => ctx.response);
15
- const getMatchingTemplate = useContextSelector(TemplateContext, ctx => ctx.getMatchingTemplate);
15
+ const { getMatchingTemplate } = useMatchers();
16
16
  const [columnToAdd, setColumnToAdd] = useState(null);
17
17
  const options = useMemo(() => hitFields.map(field => field.key), [hitFields]);
18
- const suggestions = useMemo(() => uniq((response?.items ?? []).flatMap(_hit => getMatchingTemplate(_hit)?.keys ?? [])), [getMatchingTemplate, response?.items]);
18
+ const [suggestions, setSuggestions] = useState([]);
19
+ useEffect(() => {
20
+ (async () => {
21
+ setSuggestions(uniq((await Promise.all((response?.items ?? []).map(async (_hit) => (has(_hit, '__template') ? _hit.__template?.keys : (await getMatchingTemplate(_hit))?.keys) ?? []))).flat()));
22
+ })();
23
+ }, [getMatchingTemplate, response?.items]);
19
24
  return (_jsx(Popover, { open: open, onClose: onClose, anchorEl: anchorEl, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, children: _jsxs(Stack, { spacing: 1, p: 1, width: "500px", children: [_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Autocomplete, { sx: { flex: 1 }, size: "small", options: options, value: columnToAdd, renderInput: params => _jsx(TextField, { fullWidth: true, placeholder: t('hit.fields'), ...params }), onChange: (_ev, value) => setColumnToAdd(value) }), _jsx(IconButton, { disabled: !columnToAdd, onClick: () => {
20
25
  addColumn(columnToAdd);
21
26
  setColumnToAdd(null);
@@ -3,7 +3,7 @@ import { DndContext, KeyboardSensor, PointerSensor, pointerWithin, useSensor, us
3
3
  import { arrayMove, SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
4
4
  import { Add, FormatIndentDecrease, FormatIndentIncrease, Info, List, Search, TableChart } from '@mui/icons-material';
5
5
  import { IconButton, LinearProgress, Paper, Stack, Table, TableBody, TableCell, TableHead, TableRow, ToggleButton, ToggleButtonGroup, Typography, useTheme } from '@mui/material';
6
- import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
6
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
7
7
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
8
8
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
9
9
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
@@ -14,7 +14,7 @@ import DevelopmentBanner from '@cccsaurora/howler-ui/components/elements/display
14
14
  import DevelopmentIcon from '@cccsaurora/howler-ui/components/elements/display/features/DevelopmentIcon';
15
15
  import useHitSelection from '@cccsaurora/howler-ui/components/hooks/useHitSelection';
16
16
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
17
- import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
17
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
18
18
  import { useTranslation } from 'react-i18next';
19
19
  import { useContextSelector } from 'use-context-selector';
20
20
  import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
@@ -27,10 +27,10 @@ import ColumnHeader from './ColumnHeader';
27
27
  import HitRow from './HitRow';
28
28
  const HitGrid = () => {
29
29
  const { t } = useTranslation();
30
- const { getIdFromName } = useContext(AnalyticContext);
31
30
  const theme = useTheme();
32
31
  const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
33
32
  const { onClick } = useHitSelection();
33
+ const { getMatchingAnalytic } = useMatchers();
34
34
  const search = useContextSelector(HitSearchContext, ctx => ctx.search);
35
35
  const displayType = useContextSelector(HitSearchContext, ctx => ctx.displayType);
36
36
  const setDisplayType = useContextSelector(HitSearchContext, ctx => ctx.setDisplayType);
@@ -65,10 +65,11 @@ const HitGrid = () => {
65
65
  useEffect(() => {
66
66
  response?.items.forEach(hit => {
67
67
  if (!analyticIds[hit.howler.analytic]) {
68
- getIdFromName(hit.howler.analytic).then(_analyticId => setAnalyticIds(_analyticIds => ({ ..._analyticIds, [hit.howler.analytic]: _analyticId })));
68
+ getMatchingAnalytic(hit).then(_analytic => setAnalyticIds(_analyticIds => ({ ..._analyticIds, [hit.howler.analytic]: _analytic.analytic_id })));
69
69
  }
70
70
  });
71
- }, [analyticIds, getIdFromName, response]);
71
+ // eslint-disable-next-line react-hooks/exhaustive-deps
72
+ }, [analyticIds, response]);
72
73
  const onMouseMove = useCallback((event) => {
73
74
  event.stopPropagation();
74
75
  event.preventDefault();
@@ -1,24 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Stack } from '@mui/material';
3
3
  import { LocalizationProvider } from '@mui/x-date-pickers';
4
- import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
4
+ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
5
5
  import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
6
6
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
7
- import moment from 'moment';
7
+ import dayjs from 'dayjs';
8
8
  import { memo, useEffect } from 'react';
9
9
  import { useTranslation } from 'react-i18next';
10
10
  import { useContextSelector } from 'use-context-selector';
11
11
  const CustomSpan = () => {
12
12
  const { t } = useTranslation();
13
13
  const span = useContextSelector(ParameterContext, ctx => ctx.span);
14
- const startDate = useContextSelector(ParameterContext, ctx => (ctx.startDate ? moment(ctx.startDate) : null));
14
+ const startDate = useContextSelector(ParameterContext, ctx => (ctx.startDate ? dayjs(ctx.startDate) : null));
15
15
  const setCustomSpan = useContextSelector(ParameterContext, ctx => ctx.setCustomSpan);
16
- const endDate = useContextSelector(ParameterContext, ctx => (ctx.endDate ? moment(ctx.endDate) : null));
16
+ const endDate = useContextSelector(ParameterContext, ctx => (ctx.endDate ? dayjs(ctx.endDate) : null));
17
17
  useEffect(() => {
18
18
  if (span?.endsWith('custom') && (!startDate || !endDate)) {
19
- setCustomSpan(moment().subtract(3, 'days').toISOString(), moment().toISOString());
19
+ setCustomSpan(dayjs().subtract(3, 'days').toISOString(), dayjs().toISOString());
20
20
  }
21
21
  }, [endDate, setCustomSpan, span, startDate]);
22
- return span?.endsWith('custom') ? (_jsx(LocalizationProvider, { dateAdapter: AdapterMoment, children: _jsxs(Stack, { direction: "row", spacing: 1, useFlexGap: true, flexWrap: "wrap", children: [_jsx(DateTimePicker, { sx: { minWidth: '175px', flexGrow: 1, marginTop: 1 }, slotProps: { textField: { size: 'small' } }, label: t('date.select.start'), value: startDate ? moment(startDate) : moment().subtract(1, 'days'), maxDate: endDate, onChange: (newStartDate) => setCustomSpan(newStartDate.toISOString(), endDate.toISOString()), ampm: false, disableFuture: true }), _jsx(DateTimePicker, { sx: { minWidth: '175px', flexGrow: 1, marginTop: 1 }, slotProps: { textField: { size: 'small' } }, label: t('date.select.end'), value: endDate, minDate: startDate, onChange: (newEndDate) => setCustomSpan(startDate.toISOString(), newEndDate.toISOString()), ampm: false, disableFuture: true })] }) })) : null;
22
+ return span?.endsWith('custom') ? (_jsx(LocalizationProvider, { dateAdapter: AdapterDayjs, children: _jsxs(Stack, { direction: "row", spacing: 1, useFlexGap: true, flexWrap: "wrap", children: [_jsx(DateTimePicker, { sx: { minWidth: '175px', flexGrow: 1, marginTop: 1 }, slotProps: { textField: { size: 'small' } }, label: t('date.select.start'), value: startDate ? dayjs(startDate) : dayjs().subtract(1, 'days'), maxDate: endDate, onChange: (newStartDate) => setCustomSpan(newStartDate.toISOString(), endDate.toISOString()), ampm: false, disableFuture: true }), _jsx(DateTimePicker, { sx: { minWidth: '175px', flexGrow: 1, marginTop: 1 }, slotProps: { textField: { size: 'small' } }, label: t('date.select.end'), value: endDate, minDate: startDate, onChange: (newEndDate) => setCustomSpan(startDate.toISOString(), newEndDate.toISOString()), ampm: false, disableFuture: true })] }) })) : null;
23
23
  };
24
24
  export default memo(CustomSpan);
@@ -3,11 +3,8 @@ import { Icon } from '@iconify/react/dist/iconify.js';
3
3
  import { Code, Comment, DataObject, History, LinkSharp, QueryStats, ViewAgenda } from '@mui/icons-material';
4
4
  import { Badge, Box, CardContent, Collapse, IconButton, Skeleton, Stack, Tab, Tabs, Tooltip, useMediaQuery, useTheme } from '@mui/material';
5
5
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
6
- import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
7
- import { DossierContext } from '@cccsaurora/howler-ui/components/app/providers/DossierProvider';
6
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
8
7
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
9
- import { OverviewContext } from '@cccsaurora/howler-ui/components/app/providers/OverviewProvider';
10
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
11
8
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
12
9
  import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
13
10
  import BundleButton from '@cccsaurora/howler-ui/components/elements/display/icons/BundleButton';
@@ -29,7 +26,7 @@ import RelatedLink from '@cccsaurora/howler-ui/components/elements/hit/related/R
29
26
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
30
27
  import useMyUserList from '@cccsaurora/howler-ui/components/hooks/useMyUserList';
31
28
  import uniqBy from 'lodash-es/uniqBy';
32
- import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
29
+ import { useCallback, useEffect, useMemo, useState } from 'react';
33
30
  import { useTranslation } from 'react-i18next';
34
31
  import { useNavigate, useParams } from 'react-router-dom';
35
32
  import { useContextSelector } from 'use-context-selector';
@@ -49,10 +46,7 @@ const HitViewer = () => {
49
46
  const theme = useTheme();
50
47
  const isUnderLg = useMediaQuery(theme.breakpoints.down('lg'));
51
48
  const [orientation, setOrientation] = useMyLocalStorageItem(StorageKey.VIEWER_ORIENTATION, Orientation.VERTICAL);
52
- const refreshTemplates = useContextSelector(TemplateContext, ctx => ctx.refresh);
53
- const { getAnalyticFromName } = useContext(AnalyticContext);
54
- const { getMatchingOverview, refresh: refreshOverviews } = useContext(OverviewContext);
55
- const getMatchingDossiers = useContextSelector(DossierContext, ctx => ctx.getMatchingDossiers);
49
+ const { getMatchingOverview, getMatchingDossiers, getMatchingAnalytic } = useMatchers();
56
50
  const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
57
51
  const hit = useContextSelector(HitContext, ctx => ctx.hits[params.id]);
58
52
  const [userIds, setUserIds] = useState(new Set());
@@ -60,22 +54,22 @@ const HitViewer = () => {
60
54
  const [tab, setTab] = useState('details');
61
55
  const [analytic, setAnalytic] = useState();
62
56
  const [dossiers, setDossiers] = useState([]);
57
+ const [hasOverview, setHasOverview] = useState(false);
63
58
  const fetchData = useCallback(async () => {
64
59
  try {
65
- let existingHit = hit;
66
- if (!existingHit) {
67
- existingHit = await getHit(params.id, true);
60
+ if (!hit) {
61
+ await getHit(params.id, true);
62
+ return;
68
63
  }
69
- setUserIds(getUserList(existingHit));
70
- setAnalytic(await getAnalyticFromName(existingHit.howler.analytic));
64
+ setUserIds(getUserList(hit));
65
+ setAnalytic(await getMatchingAnalytic(hit));
71
66
  }
72
67
  catch (err) {
73
68
  if (err.cause?.api_status_code === 404) {
74
69
  navigate('/404');
75
70
  }
76
71
  }
77
- // eslint-disable-next-line react-hooks/exhaustive-deps
78
- }, [getAnalyticFromName, getHit, params.id, navigate]);
72
+ }, [hit, getMatchingAnalytic, getHit, params.id, navigate]);
79
73
  useEffect(() => {
80
74
  if (isUnderLg) {
81
75
  setOrientation(Orientation.HORIZONTAL);
@@ -83,22 +77,20 @@ const HitViewer = () => {
83
77
  }, [isUnderLg, setOrientation]);
84
78
  useEffect(() => {
85
79
  fetchData();
86
- }, [params.id, fetchData]);
80
+ }, [params.id, fetchData, hit]);
87
81
  const onOrientationChange = useCallback(() => setOrientation(orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL), [orientation, setOrientation]);
88
- const matchingOverview = useMemo(() => getMatchingOverview(hit), [getMatchingOverview, hit]);
89
82
  useEffect(() => {
90
- refreshTemplates();
91
- refreshOverviews();
92
- }, [refreshOverviews, refreshTemplates]);
83
+ getMatchingOverview(hit).then(_overview => setHasOverview(!!_overview));
84
+ }, [getMatchingOverview, hit]);
93
85
  useEffect(() => {
94
- if (matchingOverview && tab === 'details') {
86
+ if (hasOverview && tab === 'details') {
95
87
  setTab('overview');
96
88
  }
97
- else if (!matchingOverview && tab === 'overview') {
89
+ else if (!hasOverview && tab === 'overview') {
98
90
  setTab('details');
99
91
  }
100
92
  // eslint-disable-next-line react-hooks/exhaustive-deps
101
- }, [matchingOverview]);
93
+ }, [hasOverview]);
102
94
  const tabContent = useMemo(() => {
103
95
  if (!tab || !hit) {
104
96
  return;
@@ -122,7 +114,7 @@ const HitViewer = () => {
122
114
  if (!hit) {
123
115
  return;
124
116
  }
125
- getMatchingDossiers(hit.howler.id).then(setDossiers);
117
+ getMatchingDossiers(hit).then(setDossiers);
126
118
  }, [getMatchingDossiers, hit]);
127
119
  if (!hit) {
128
120
  return (_jsx(PageCenter, { children: _jsx(Skeleton, { variant: "rounded", height: "520px" }) }));
@@ -150,7 +142,7 @@ const HitViewer = () => {
150
142
  position: 'absolute',
151
143
  top: theme.spacing(2),
152
144
  right: theme.spacing(-6)
153
- }, children: [_jsx(Tooltip, { title: t('page.hits.view.layout'), children: _jsx(IconButton, { onClick: onOrientationChange, children: _jsx(ViewAgenda, { sx: { transition: 'rotate 250ms', rotate: orientation === 'vertical' ? '90deg' : '0deg' } }) }) }), _jsx(SocketBadge, { size: "medium" }), analytic && (_jsx(Tooltip, { title: t('hit.panel.analytic.open'), children: _jsx(IconButton, { onClick: () => navigate(`/analytics/${analytic.analytic_id}`), children: _jsx(QueryStats, {}) }) })), hit?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles })] }))] }), _jsx(HowlerCard, { sx: [orientation === 'horizontal' && { height: '0px' }], children: _jsx(CardContent, { sx: { padding: 1, position: 'relative' }, children: _jsx(HitActions, { hit: hit, orientation: "vertical" }) }) }), _jsx(Box, { sx: { gridColumn: '1 / span 2', mb: 1 }, children: _jsxs(Tabs, { value: tab === 'overview' && !matchingOverview ? 'details' : tab, sx: { display: 'flex', flexDirection: 'row', pr: 2, alignItems: 'center' }, children: [hit?.howler?.is_bundle && (_jsx(Tab, { label: t('hit.viewer.aggregate'), value: "hit_aggregate", onClick: () => setTab('hit_aggregate') })), matchingOverview && (_jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') })), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
145
+ }, children: [_jsx(Tooltip, { title: t('page.hits.view.layout'), children: _jsx(IconButton, { onClick: onOrientationChange, children: _jsx(ViewAgenda, { sx: { transition: 'rotate 250ms', rotate: orientation === 'vertical' ? '90deg' : '0deg' } }) }) }), _jsx(SocketBadge, { size: "medium" }), analytic && (_jsx(Tooltip, { title: t('hit.panel.analytic.open'), children: _jsx(IconButton, { onClick: () => navigate(`/analytics/${analytic.analytic_id}`), children: _jsx(QueryStats, {}) }) })), hit?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles })] }))] }), _jsx(HowlerCard, { sx: [orientation === 'horizontal' && { height: '0px' }], children: _jsx(CardContent, { sx: { padding: 1, position: 'relative' }, children: _jsx(HitActions, { hit: hit, orientation: "vertical" }) }) }), _jsx(Box, { sx: { gridColumn: '1 / span 2', mb: 1 }, children: _jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: { display: 'flex', flexDirection: 'row', pr: 2, alignItems: 'center' }, children: [hit?.howler?.is_bundle && (_jsx(Tab, { label: t('hit.viewer.aggregate'), value: "hit_aggregate", onClick: () => setTab('hit_aggregate') })), hasOverview && (_jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') })), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
154
146
  // eslint-disable-next-line react/no-array-index-key
155
147
  , { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [lead.icon && _jsx(Icon, { icon: lead.icon }), _jsx("span", { children: i18n.language === 'en' ? lead.label.en : lead.label.fr })] }), value: 'lead:' + index, onClick: () => setTab('lead:' + index) }, 'lead:' + index))), dossiers.flatMap((_dossier, dossierIndex) => _dossier.leads?.map((_lead, leadIndex) => (_jsx(Tab
156
148
  // eslint-disable-next-line react/no-array-index-key
@@ -10,8 +10,8 @@ import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCente
10
10
  import CustomButton from '@cccsaurora/howler-ui/components/elements/addons/buttons/CustomButton';
11
11
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
12
12
  import useMyUserFunctions from '@cccsaurora/howler-ui/components/hooks/useMyUserFunctions';
13
+ import dayjs from 'dayjs';
13
14
  import isEqual from 'lodash-es/isEqual';
14
- import moment from 'moment';
15
15
  import { useCallback, useEffect, useMemo, useState } from 'react';
16
16
  import { useTranslation } from 'react-i18next';
17
17
  import { Link } from 'react-router-dom';
@@ -26,7 +26,7 @@ const Home = () => {
26
26
  const { user, setUser } = useAppUser();
27
27
  const { setDashboard } = useMyUserFunctions();
28
28
  const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
29
- const [lastViewed, setLastViewed] = useMyLocalStorageItem(StorageKey.LAST_VIEW, moment().utc().format(LUCENE_DATE_FMT));
29
+ const [lastViewed, setLastViewed] = useMyLocalStorageItem(StorageKey.LAST_VIEW, dayjs().utc().format(LUCENE_DATE_FMT));
30
30
  const [loading, setLoading] = useState(false);
31
31
  const [isEditing, setIsEditing] = useState(false);
32
32
  const [updatedHitTotal, setUpdatedHitTotal] = useState(0);
@@ -81,8 +81,8 @@ const Home = () => {
81
81
  })
82
82
  .then(result => setUpdatedHitTotal(result.total));
83
83
  }, [updateQuery]);
84
- return (_jsx(PageCenter, { maxWidth: "1800px", textAlign: "left", height: "100%", children: _jsxs(Stack, { direction: "column", spacing: 1, sx: { height: '100%' }, children: [_jsxs(Stack, { direction: "row", justifyContent: "end", spacing: 1, children: [isEditing && (_jsx(CustomButton, { variant: "outlined", size: "small", color: "error", startIcon: _jsx(Cancel, {}), onClick: discardChanges, children: t('cancel') })), _jsx(CustomButton, { variant: "outlined", size: "small", disabled: isEditing && isEqual(dashboard, user.dashboard), color: isEditing ? 'success' : 'primary', startIcon: isEditing ? loading ? _jsx(CircularProgress, { size: 20 }) : _jsx(Check, {}) : _jsx(Edit, {}), onClick: () => (!isEditing ? setIsEditing(true) : saveChanges()), children: t(isEditing ? 'save' : 'edit') })] }), updatedHitTotal > 0 && (_jsxs(Alert, { severity: "info", variant: "outlined", action: _jsxs(Stack, { spacing: 1, direction: "row", children: [_jsx(IconButton, { color: "info", component: Link, to: `/hits?query=${encodeURIComponent(updateQuery)}`, onClick: () => setLastViewed(moment().utc().format(LUCENE_DATE_FMT)), children: _jsx(OpenInNew, {}) }), _jsx(IconButton, { color: "info", onClick: () => {
85
- setLastViewed(moment().utc().format(LUCENE_DATE_FMT));
84
+ return (_jsx(PageCenter, { maxWidth: "1800px", textAlign: "left", height: "100%", children: _jsxs(Stack, { direction: "column", spacing: 1, sx: { height: '100%' }, children: [_jsxs(Stack, { direction: "row", justifyContent: "end", spacing: 1, children: [isEditing && (_jsx(CustomButton, { variant: "outlined", size: "small", color: "error", startIcon: _jsx(Cancel, {}), onClick: discardChanges, children: t('cancel') })), _jsx(CustomButton, { variant: "outlined", size: "small", disabled: isEditing && isEqual(dashboard, user.dashboard), color: isEditing ? 'success' : 'primary', startIcon: isEditing ? loading ? _jsx(CircularProgress, { size: 20 }) : _jsx(Check, {}) : _jsx(Edit, {}), onClick: () => (!isEditing ? setIsEditing(true) : saveChanges()), children: t(isEditing ? 'save' : 'edit') })] }), updatedHitTotal > 0 && (_jsxs(Alert, { severity: "info", variant: "outlined", action: _jsxs(Stack, { spacing: 1, direction: "row", children: [_jsx(IconButton, { color: "info", component: Link, to: `/hits?query=${encodeURIComponent(updateQuery)}`, onClick: () => setLastViewed(dayjs().utc().format(LUCENE_DATE_FMT)), children: _jsx(OpenInNew, {}) }), _jsx(IconButton, { color: "info", onClick: () => {
85
+ setLastViewed(dayjs().utc().format(LUCENE_DATE_FMT));
86
86
  setUpdatedHitTotal(0);
87
87
  }, children: _jsx(Close, {}) })] }), children: [_jsx(AlertTitle, { children: t('route.home.alert.updated.title') }), t('route.home.alert.updated.description', { count: updatedHitTotal })] })), _jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: _jsx(SortableContext, { items: (dashboard ?? []).map(entry => getIdFromEntry(entry)), children: _jsxs(Grid, { container: true, spacing: 1, alignItems: "stretch", sx: [
88
88
  theme => ({
@@ -1,15 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Autocomplete, Box, Button, CircularProgress, Divider, FormControl, LinearProgress, Stack, TextField, Tooltip, useTheme } from '@mui/material';
2
+ import { Autocomplete, Box, Button, CircularProgress, Divider, FormControl, LinearProgress, Stack, TextField, ThemeProvider, ToggleButton, ToggleButtonGroup, Tooltip, useTheme } from '@mui/material';
3
3
  import api from '@cccsaurora/howler-ui/api';
4
4
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
5
5
  import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
- import { Check, Delete, SsidChart } from '@mui/icons-material';
7
+ import { Check, DarkMode, Delete, SsidChart, WbSunny } from '@mui/icons-material';
8
+ import { useApp } from '@cccsaurora/howler-ui/commons/components/app/hooks';
8
9
  import AppInfoPanel from '@cccsaurora/howler-ui/commons/components/display/AppInfoPanel';
9
- import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
10
+ import useThemeBuilder from '@cccsaurora/howler-ui/commons/components/utils/hooks/useThemeBuilder';
10
11
  import { OverviewContext } from '@cccsaurora/howler-ui/components/app/providers/OverviewProvider';
11
12
  import HitOverview from '@cccsaurora/howler-ui/components/elements/hit/HitOverview';
12
13
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
14
+ import useMyTheme from '@cccsaurora/howler-ui/components/hooks/useMyTheme';
13
15
  import { useSearchParams } from 'react-router-dom';
14
16
  import hitsData from '@cccsaurora/howler-ui/utils/hit.json';
15
17
  import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
@@ -17,6 +19,8 @@ import OverviewEditor from './OverviewEditor';
17
19
  import { useStartingTemplate } from './startingTemplate';
18
20
  const OverviewViewer = () => {
19
21
  const theme = useTheme();
22
+ const app = useApp();
23
+ const { lightTheme, darkTheme } = useThemeBuilder(useMyTheme());
20
24
  const { t } = useTranslation();
21
25
  const [params, setParams] = useSearchParams();
22
26
  const { getOverviews } = useContext(OverviewContext);
@@ -24,6 +28,7 @@ const OverviewViewer = () => {
24
28
  const [overviewList, setOverviewList] = useState([]);
25
29
  const [selectedOverview, setSelectedOverview] = useState(null);
26
30
  const [content, setContent] = useState('');
31
+ const [chosenTheme, setChosenTheme] = useState(app.theme);
27
32
  const [analytics, setAnalytics] = useState([]);
28
33
  const [detections, setDetections] = useState([]);
29
34
  const [analytic, setAnalytic] = useState(params.get('analytic') ?? '');
@@ -32,7 +37,6 @@ const OverviewViewer = () => {
32
37
  const [overviewLoading, setOverviewLoading] = useState(false);
33
38
  const [exampleHit, setExampleHit] = useState(null);
34
39
  const [x, setX] = useState(0);
35
- const analyticContext = useContext(AnalyticContext);
36
40
  const wrapper = useRef();
37
41
  const startingTemplate = useStartingTemplate();
38
42
  useEffect(() => {
@@ -58,19 +62,14 @@ const OverviewViewer = () => {
58
62
  // eslint-disable-next-line react-hooks/exhaustive-deps
59
63
  }, [analytic, dispatchApi]);
60
64
  useEffect(() => {
61
- if (analytic) {
62
- setLoading(true);
63
- analyticContext
64
- .getAnalyticFromName(analytic)
65
- .then(foundAnalytic => {
66
- setDetections(foundAnalytic.detections);
67
- })
68
- .catch(() => {
65
+ if (analytic && analytics) {
66
+ const _detections = analytics.find(_analytic => _analytic.name.toLowerCase() === analytic.toLowerCase())?.detections ?? [];
67
+ setDetections(_detections);
68
+ if (detection && !_detections.map(_detection => _detection.toLowerCase()).includes(detection.toLowerCase())) {
69
69
  setDetection('ANY');
70
- })
71
- .finally(() => setLoading(false));
70
+ }
72
71
  }
73
- }, [analytic, analyticContext, detection, dispatchApi, params, setParams]);
72
+ }, [analytic, analytics, detection]);
74
73
  useEffect(() => {
75
74
  (async () => {
76
75
  const result = await dispatchApi(api.search.hit.post({
@@ -173,7 +172,8 @@ const OverviewViewer = () => {
173
172
  }, [onMouseMove, onMouseUp]);
174
173
  const analyticOrDetectionMissing = useMemo(() => !analytic || !detection, [analytic, detection]);
175
174
  const noChange = useMemo(() => selectedOverview?.content === content, [content, selectedOverview?.content]);
176
- return (_jsxs(PageCenter, { maxWidth: "100%", width: "100%", textAlign: "left", height: "100%", children: [_jsx(LinearProgress, { sx: { mb: 1, opacity: +loading } }), _jsxs(Stack, { direction: "column", spacing: 2, divider: _jsx(Divider, { orientation: "horizontal", flexItem: true }), height: "100%", children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(FormControl, { sx: { maxWidth: { sm: '300px', lg: '450px' }, width: '100%' }, children: _jsx(Autocomplete, { id: "analytic", options: analytics.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())), getOptionLabel: option => option.name, value: analytics.find(a => a.name === analytic) || null, onChange: (_event, newValue) => setAnalytic(newValue ? newValue.name : ''), renderInput: autocompleteAnalyticParams => (_jsx(TextField, { ...autocompleteAnalyticParams, label: t('route.overviews.analytic'), size: "small" })) }) }), !analytics.find(_analytic => _analytic.name === analytic)?.rule ? (_jsx(FormControl, { sx: { minWidth: { sm: '200px' } }, disabled: !analytic, children: _jsx(Autocomplete, { id: "detection", options: ['ANY', ...detections.sort()], getOptionLabel: option => option, value: detection ?? '', onChange: (_event, newValue) => setDetection(newValue), renderInput: autocompleteDetectionParams => (_jsx(TextField, { ...autocompleteDetectionParams, label: t('route.overviews.detection'), size: "small" })) }) })) : (_jsx(Tooltip, { title: t('route.overviews.rule.explanation'), children: _jsx(SsidChart, { color: "info", sx: { alignSelf: 'center' } }) })), selectedOverview && (_jsx(Button, { variant: "outlined", startIcon: _jsx(Delete, {}), onClick: onDelete, children: t('button.delete') })), _jsx(Button, { variant: "outlined", disabled: analyticOrDetectionMissing || noChange, startIcon: overviewLoading ? _jsx(CircularProgress, { size: 16 }) : _jsx(Check, {}), onClick: onSave, children: t(!analyticOrDetectionMissing && !noChange ? 'button.save' : 'button.saved') })] }), analyticOrDetectionMissing ? (_jsx(AppInfoPanel, { i18nKey: "route.overviews.select", sx: { width: '100%', alignSelf: 'start' } })) : (_jsxs(Stack, { ref: wrapper, direction: "row", spacing: 1, height: "100%", onKeyDown: e => {
175
+ const activeTheme = chosenTheme === 'light' ? lightTheme : darkTheme;
176
+ return (_jsxs(PageCenter, { maxWidth: "100%", width: "100%", textAlign: "left", height: "100%", children: [_jsx(LinearProgress, { sx: { mb: 1, opacity: +loading } }), _jsxs(Stack, { direction: "column", spacing: 2, divider: _jsx(Divider, { orientation: "horizontal", flexItem: true }), height: "100%", children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(FormControl, { sx: { maxWidth: { sm: '300px', lg: '450px' }, width: '100%' }, children: _jsx(Autocomplete, { id: "analytic", options: analytics.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())), getOptionLabel: option => option.name, value: analytics.find(a => a.name === analytic) || null, onChange: (_event, newValue) => setAnalytic(newValue ? newValue.name : ''), renderInput: autocompleteAnalyticParams => (_jsx(TextField, { ...autocompleteAnalyticParams, label: t('route.overviews.analytic'), size: "small" })) }) }), !analytics.find(_analytic => _analytic.name === analytic)?.rule ? (_jsx(FormControl, { sx: { minWidth: { sm: '200px' } }, disabled: !analytic, children: _jsx(Autocomplete, { id: "detection", options: ['ANY', ...detections.sort()], getOptionLabel: option => option, value: detection ?? '', onChange: (_event, newValue) => setDetection(newValue), renderInput: autocompleteDetectionParams => (_jsx(TextField, { ...autocompleteDetectionParams, label: t('route.overviews.detection'), size: "small" })) }) })) : (_jsx(Tooltip, { title: t('route.overviews.rule.explanation'), children: _jsx(SsidChart, { color: "info", sx: { alignSelf: 'center' } }) })), selectedOverview && (_jsx(Button, { variant: "outlined", startIcon: _jsx(Delete, {}), onClick: onDelete, children: t('button.delete') })), _jsx(Button, { variant: "outlined", disabled: analyticOrDetectionMissing || noChange, startIcon: overviewLoading ? _jsx(CircularProgress, { size: 16 }) : _jsx(Check, {}), onClick: onSave, children: t(!analyticOrDetectionMissing && !noChange ? 'button.save' : 'button.saved') }), _jsx("div", { style: { flex: 1 } }), _jsxs(ToggleButtonGroup, { exclusive: true, value: chosenTheme, onChange: (_event, value) => setChosenTheme(value), sx: { maxHeight: '40px' }, children: [_jsx(Tooltip, { title: t('route.overviews.theme.light'), children: _jsx(ToggleButton, { value: "light", children: _jsx(WbSunny, {}) }) }), _jsx(Tooltip, { title: t('route.overviews.theme.dark'), children: _jsx(ToggleButton, { value: "dark", children: _jsx(DarkMode, {}) }) })] })] }), analyticOrDetectionMissing ? (_jsx(AppInfoPanel, { i18nKey: "route.overviews.select", sx: { width: '100%', alignSelf: 'start' } })) : (_jsxs(Stack, { ref: wrapper, direction: "row", spacing: 1, height: "100%", onKeyDown: e => {
177
177
  if (e.ctrlKey && e.key === 's') {
178
178
  if (!noChange) {
179
179
  onSave();
@@ -199,20 +199,22 @@ const OverviewViewer = () => {
199
199
  transform: `translateX(${x}px)`,
200
200
  zIndex: 1000,
201
201
  borderRadius: theme.shape.borderRadius
202
- }, onMouseDown: onMouseDown }), _jsx(Box, { flex: 1, px: 2, sx: {
203
- position: 'absolute',
204
- top: 0,
205
- left: `calc(50% + 7px + ${x}px)`,
206
- bottom: 0,
207
- right: 0,
208
- display: 'flex',
209
- alignItems: 'stretch',
210
- justifyContent: 'stretch',
211
- px: 1,
212
- pt: 1,
213
- mt: -1,
214
- '& > *': { width: '100%' },
215
- '& > div > :first-child': { mt: 0 }
216
- }, children: _jsx(HitOverview, { content: content || startingTemplate, hit: exampleHit }) })] }))] })] }));
202
+ }, onMouseDown: onMouseDown }), _jsx(ThemeProvider, { theme: activeTheme, children: _jsx(Box, { flex: 1, px: 2, sx: {
203
+ position: 'absolute',
204
+ top: 0,
205
+ left: `calc(50% + 7px + ${x}px)`,
206
+ bottom: 0,
207
+ right: 0,
208
+ display: 'flex',
209
+ alignItems: 'stretch',
210
+ justifyContent: 'stretch',
211
+ px: 1,
212
+ pt: 1,
213
+ mt: -1,
214
+ '& > *': { width: '100%' },
215
+ '& > div > :first-child': { mt: 0 },
216
+ backgroundColor: activeTheme.palette.background.default,
217
+ color: activeTheme.palette.text.primary
218
+ }, children: _jsx(HitOverview, { content: content || startingTemplate, hit: exampleHit }) }) })] }))] })] }));
217
219
  };
218
220
  export default OverviewViewer;
@@ -3,7 +3,7 @@ import { Add } from '@mui/icons-material';
3
3
  import { Chip, Grid, IconButton, TableCell, TableRow } from '@mui/material';
4
4
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
5
5
  import useMyLocalStorage from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
6
- import moment from 'moment';
6
+ import dayjs from 'dayjs';
7
7
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
8
8
  import { useContext, useMemo } from 'react';
9
9
  import { Trans, useTranslation } from 'react-i18next';
@@ -26,6 +26,6 @@ const SecuritySection = ({ user, editPassword, addApiKey, removeApiKey, editQuot
26
26
  return (_jsxs(SettingsSection, { title: t('page.settings.security.title'), colSpan: 3, children: [!isOAuth && editPassword && (_jsx(EditRow, { titleKey: "page.settings.security.table.password", value: "\u25CF\u25CF\u25CF\u25CF\u25CF\u25CF\u25CF\u25CF\u25CF\u25CF\u25CF", onEdit: editPassword, type: "password" })), config?.configuration.auth.allow_apikeys && (_jsxs(TableRow, { sx: { cursor: 'pointer' }, children: [_jsx(TableCell, { style: { whiteSpace: 'nowrap' }, children: t('page.settings.security.table.apikeys') }), _jsx(TableCell, { width: "100%", children: _jsxs(Grid, { container: true, spacing: 1, children: [user?.apikeys?.map(apiKey => (_jsx(Grid, { item: true, children: _jsx(Chip, { label: apiKey[0].toLocaleLowerCase() +
27
27
  (apiKey[1].length > 0
28
28
  ? ` (${apiKey[1].map(permission => t(APIKEY_LABELS[permission])).join(', ')})`
29
- : ''), style: { backgroundColor: moment.utc(apiKey[2]).isBefore(moment().utc()) ? 'orange' : 'default' }, onDelete: removeApiKey ? () => removeApiKey(apiKey) : null }) }, apiKey[0]))), user?.apikeys?.length < 1 && (_jsx(Grid, { item: true, children: _jsx(Trans, { i18nKey: "none" }) }))] }) }), _jsx(TableCell, { align: "right", children: addApiKey && (_jsx(IconButton, { onClick: addApiKey, children: _jsx(Add, { fontSize: "small" }) })) })] })), _jsx(EditRow, { titleKey: "page.settings.security.table.apiquota", descriptionKey: "page.settings.security.table.apiquota.description", value: user?.api_quota, validate: value => value && /^[0-9]*$/m.test(value.toString()), type: "number", onEdit: editQuota }), howlerPluginStore.plugins.map(plugin => pluginStore.executeFunction(`${plugin}.settings`, 'security'))] }));
29
+ : ''), style: { backgroundColor: dayjs.utc(apiKey[2]).isBefore(dayjs().utc()) ? 'orange' : 'default' }, onDelete: removeApiKey ? () => removeApiKey(apiKey) : null }) }, apiKey[0]))), user?.apikeys?.length < 1 && (_jsx(Grid, { item: true, children: _jsx(Trans, { i18nKey: "none" }) }))] }) }), _jsx(TableCell, { align: "right", children: addApiKey && (_jsx(IconButton, { onClick: addApiKey, children: _jsx(Add, { fontSize: "small" }) })) })] })), _jsx(EditRow, { titleKey: "page.settings.security.table.apiquota", descriptionKey: "page.settings.security.table.apiquota.description", value: user?.api_quota, validate: value => value && /^[0-9]*$/m.test(value.toString()), type: "number", onEdit: editQuota }), howlerPluginStore.plugins.map(plugin => pluginStore.executeFunction(`${plugin}.settings`, 'security'))] }));
30
30
  };
31
31
  export default SecuritySection;
@@ -5,22 +5,17 @@ import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCente
5
5
  import TemplateEditor from '@cccsaurora/howler-ui/components/routes/templates/TemplateEditor';
6
6
  import { useCallback, useEffect, useMemo, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
- import { Check, Delete, Remove, SsidChart } from '@mui/icons-material';
8
+ import { Check, Delete, SsidChart } from '@mui/icons-material';
9
9
  import AppInfoPanel from '@cccsaurora/howler-ui/commons/components/display/AppInfoPanel';
10
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
11
- import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
12
- import HitOutline, { DEFAULT_FIELDS } from '@cccsaurora/howler-ui/components/elements/hit/HitOutline';
10
+ import { DEFAULT_FIELDS } from '@cccsaurora/howler-ui/components/elements/hit/HitOutline';
13
11
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
14
12
  import isEqual from 'lodash-es/isEqual';
15
13
  import { useSearchParams } from 'react-router-dom';
16
- import { useContextSelector } from 'use-context-selector';
17
14
  import hitsData from '@cccsaurora/howler-ui/utils/hit.json';
18
15
  import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
19
- const CUSTOM_OUTLINES = ['cmt.aws.sigma.rules', 'assemblyline', '6tailphish'];
20
16
  const TemplateViewer = () => {
21
17
  const { t } = useTranslation();
22
18
  const [params, setParams] = useSearchParams();
23
- const getTemplates = useContextSelector(TemplateContext, ctx => ctx.getTemplates);
24
19
  const { dispatchApi } = useMyApi();
25
20
  const [templateList, setTemplateList] = useState([]);
26
21
  const [selectedTemplate, setSelectedTemplate] = useState(null);
@@ -47,32 +42,33 @@ const TemplateViewer = () => {
47
42
  }
48
43
  setAnalytics(_analytics);
49
44
  });
50
- getTemplates(true).then(setTemplateList);
45
+ dispatchApi(api.template.get()).then(setTemplateList);
51
46
  // eslint-disable-next-line react-hooks/exhaustive-deps
52
47
  }, [analytic, dispatchApi]);
53
48
  useEffect(() => {
54
- if (analytic) {
55
- setLoading(true);
56
- dispatchApi(api.search.grouped.hit.post('howler.detection', {
57
- limit: 0,
58
- query: `howler.analytic:"${sanitizeLuceneQuery(analytic)}"`
59
- }), {
60
- logError: false,
61
- showError: true,
62
- throwError: true
63
- })
64
- .finally(() => setLoading(false))
65
- .then(result => result.items.map(i => i.value))
66
- .then(_detections => {
67
- if (_detections.length < 1 || (type === 'global' && CUSTOM_OUTLINES.includes(analytic.toLowerCase()))) {
68
- setDetection('ANY');
69
- }
70
- if (detection && !_detections.includes(detection)) {
71
- setDetection('ANY');
72
- }
73
- setDetections(_detections);
74
- });
49
+ if (!analytic) {
50
+ return;
75
51
  }
52
+ setLoading(true);
53
+ dispatchApi(api.search.grouped.hit.post('howler.detection', {
54
+ limit: 0,
55
+ query: `howler.analytic:"${sanitizeLuceneQuery(analytic)}"`
56
+ }), {
57
+ logError: false,
58
+ showError: true,
59
+ throwError: true
60
+ })
61
+ .finally(() => setLoading(false))
62
+ .then(result => result.items.map(i => i.value))
63
+ .then(_detections => {
64
+ if (_detections.length < 1) {
65
+ setDetection('ANY');
66
+ }
67
+ if (detection && !_detections.includes(detection)) {
68
+ setDetection('ANY');
69
+ }
70
+ setDetections(_detections);
71
+ });
76
72
  }, [analytic, detection, dispatchApi, params, setParams, type]);
77
73
  useEffect(() => {
78
74
  if (analytic && detection) {
@@ -149,17 +145,12 @@ const TemplateViewer = () => {
149
145
  }
150
146
  }
151
147
  }, [analytic, detection, dispatchApi, displayFields, selectedTemplate, templateList, type]);
152
- const isCustomOutline = useMemo(() => CUSTOM_OUTLINES.includes(analytic.toLowerCase()), [analytic]);
153
148
  const analyticOrDetectionMissing = useMemo(() => !analytic || !detection, [analytic, detection]);
154
149
  const noFieldChange = useMemo(() => displayFields.length < 1 || isEqual(selectedTemplate?.keys ?? DEFAULT_FIELDS, displayFields), [displayFields, selectedTemplate?.keys]);
155
- return (_jsxs(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: [_jsx(LinearProgress, { sx: { mb: 1, opacity: +loading } }), _jsxs(Stack, { direction: "column", spacing: 2, divider: _jsx(Divider, { orientation: "horizontal", flexItem: true }), height: "100%", children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(FormControl, { sx: { flex: 1, maxWidth: '450px' }, children: _jsx(Autocomplete, { id: "analytic", options: analytics.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())), getOptionLabel: option => option.name, value: analytics.find(a => a.name === analytic) || null, onChange: (__, newValue) => setAnalytic(newValue ? newValue.name : ''), renderInput: autocompleteAnalyticParams => (_jsx(TextField, { ...autocompleteAnalyticParams, label: t('route.templates.analytic'), size: "small" })) }) }), !(detections?.length < 2 && detections[0]?.toLowerCase() === 'rule') ? (_jsx(FormControl, { sx: { flex: 1, maxWidth: '300px' }, disabled: !analytic || (isCustomOutline && type === 'global'), children: _jsx(Autocomplete, { id: "detection", options: ['ANY', ...detections.sort()], getOptionLabel: option => option, value: isCustomOutline && type === 'global' ? 'any' : (detection ?? ''), onChange: (__, newValue) => setDetection(newValue), renderInput: autocompleteDetectionParams => (_jsx(TextField, { ...autocompleteDetectionParams, label: t('route.templates.detection'), size: "small" })) }) })) : (_jsx(Tooltip, { title: t('route.templates.rule.explanation'), children: _jsx(SsidChart, { color: "info", sx: { alignSelf: 'center' } }) })), _jsxs(ToggleButtonGroup, { sx: { display: 'grid', gridTemplateColumns: '1fr 1fr' }, size: "small", exclusive: true, value: type, disabled: analyticOrDetectionMissing, onChange: (__, _type) => {
150
+ return (_jsxs(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: [_jsx(LinearProgress, { sx: { mb: 1, opacity: +loading } }), _jsxs(Stack, { direction: "column", spacing: 2, divider: _jsx(Divider, { orientation: "horizontal", flexItem: true }), height: "100%", children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(FormControl, { sx: { flex: 1, maxWidth: '450px' }, children: _jsx(Autocomplete, { id: "analytic", options: analytics.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())), getOptionLabel: option => option.name, value: analytics.find(a => a.name === analytic) || null, onChange: (__, newValue) => setAnalytic(newValue ? newValue.name : ''), renderInput: autocompleteAnalyticParams => (_jsx(TextField, { ...autocompleteAnalyticParams, label: t('route.templates.analytic'), size: "small" })) }) }), !(detections?.length < 2 && detections[0]?.toLowerCase() === 'rule') ? (_jsx(FormControl, { sx: { flex: 1, maxWidth: '300px' }, disabled: !analytic, children: _jsx(Autocomplete, { id: "detection", options: ['ANY', ...detections.sort()], getOptionLabel: option => option, value: detection ?? '', onChange: (__, newValue) => setDetection(newValue), renderInput: autocompleteDetectionParams => (_jsx(TextField, { ...autocompleteDetectionParams, label: t('route.templates.detection'), size: "small" })) }) })) : (_jsx(Tooltip, { title: t('route.templates.rule.explanation'), children: _jsx(SsidChart, { color: "info", sx: { alignSelf: 'center' } }) })), _jsxs(ToggleButtonGroup, { sx: { display: 'grid', gridTemplateColumns: '1fr 1fr' }, size: "small", exclusive: true, value: type, disabled: analyticOrDetectionMissing, onChange: (__, _type) => {
156
151
  if (_type) {
157
152
  setType(_type);
158
153
  }
159
- }, children: [_jsx(ToggleButton, { sx: { flex: 1 }, value: "personal", "aria-label": "personal", children: t('route.templates.personal') }), _jsx(ToggleButton, { sx: { flex: 1 }, value: "global", "aria-label": "global", children: t('route.templates.global') })] }), selectedTemplate && (_jsx(Button, { variant: "outlined", startIcon: _jsx(Delete, {}), onClick: onDelete, children: t('button.delete') })), _jsx(Button, { variant: "outlined", disabled: analyticOrDetectionMissing || (isCustomOutline && type === 'global') || noFieldChange, startIcon: templateLoading ? (_jsx(CircularProgress, { size: 16 })) : isCustomOutline && type === 'global' ? (_jsx(Remove, {})) : (_jsx(Check, {})), onClick: onSave, children: t(isCustomOutline && type === 'global'
160
- ? 'button.readonly'
161
- : !analyticOrDetectionMissing && !noFieldChange
162
- ? 'button.save'
163
- : 'button.saved') })] }), isCustomOutline && type === 'global' ? (_jsx(HitOutline, { hit: exampleHit, layout: HitLayout.COMFY, type: "global" })) : analyticOrDetectionMissing ? (_jsx(AppInfoPanel, { i18nKey: "route.templates.select", sx: { width: '100%', alignSelf: 'start' } })) : (_jsx(TemplateEditor, { hit: exampleHit, fields: displayFields, setFields: setDisplayFields, onAdd: field => setDisplayFields([...displayFields, field]), onRemove: field => setDisplayFields(displayFields.filter(f => f !== field)) }))] })] }));
154
+ }, children: [_jsx(ToggleButton, { sx: { flex: 1 }, value: "personal", "aria-label": "personal", children: t('route.templates.personal') }), _jsx(ToggleButton, { sx: { flex: 1 }, value: "global", "aria-label": "global", children: t('route.templates.global') })] }), selectedTemplate && (_jsx(Button, { variant: "outlined", startIcon: _jsx(Delete, {}), onClick: onDelete, children: t('button.delete') })), _jsx(Button, { variant: "outlined", disabled: analyticOrDetectionMissing || noFieldChange, startIcon: templateLoading ? _jsx(CircularProgress, { size: 16 }) : _jsx(Check, {}), onClick: onSave, children: t(!analyticOrDetectionMissing && !noFieldChange ? 'button.save' : 'button.saved') })] }), analyticOrDetectionMissing ? (_jsx(AppInfoPanel, { i18nKey: "route.templates.select", sx: { width: '100%', alignSelf: 'start' } })) : (_jsx(TemplateEditor, { hit: exampleHit, fields: displayFields, setFields: setDisplayFields, onAdd: field => setDisplayFields([...displayFields, field]), onRemove: field => setDisplayFields(displayFields.filter(f => f !== field)) }))] })] }));
164
155
  };
165
156
  export default TemplateViewer;