@cccsaurora/howler-ui 2.13.0-dev.107 → 2.13.0-dev.125

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 (41) 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/commons/components/utils/hooks/useEnv.d.ts +1 -1
  5. package/components/app/App.js +1 -3
  6. package/components/app/hooks/useMatchers.d.ts +8 -0
  7. package/components/app/hooks/useMatchers.js +46 -0
  8. package/components/app/hooks/useMatchers.test.d.ts +1 -0
  9. package/components/app/hooks/useMatchers.test.js +237 -0
  10. package/components/app/hooks/useTitle.js +5 -4
  11. package/components/app/providers/HitProvider.d.ts +2 -1
  12. package/components/app/providers/HitProvider.js +8 -2
  13. package/components/app/providers/HitSearchProvider.d.ts +2 -1
  14. package/components/app/providers/HitSearchProvider.js +2 -1
  15. package/components/app/providers/SocketProvider.js +1 -1
  16. package/components/elements/display/handlebars/helpers.js +5 -2
  17. package/components/elements/hit/HitCard.js +0 -5
  18. package/components/elements/hit/HitOutline.d.ts +2 -2
  19. package/components/elements/hit/HitOutline.js +11 -21
  20. package/components/elements/hit/HitOverview.js +7 -4
  21. package/components/elements/hit/HitSummary.d.ts +2 -1
  22. package/components/elements/hit/HitSummary.js +7 -6
  23. package/components/elements/hit/related/PivotLink.js +10 -3
  24. package/components/routes/analytics/AnalyticTemplates.js +9 -6
  25. package/components/routes/hits/search/InformationPane.js +25 -40
  26. package/components/routes/hits/search/SearchPane.js +2 -8
  27. package/components/routes/hits/search/grid/AddColumnModal.js +10 -5
  28. package/components/routes/hits/view/HitViewer.js +17 -23
  29. package/components/routes/templates/TemplateViewer.js +27 -36
  30. package/components/routes/templates/Templates.js +4 -11
  31. package/locales/en/translation.json +1 -0
  32. package/locales/fr/translation.json +1 -0
  33. package/models/WithMetadata.d.ts +8 -0
  34. package/models/WithMetadata.js +1 -0
  35. package/package.json +104 -104
  36. package/setupTests.d.ts +1 -0
  37. package/setupTests.js +8 -0
  38. package/components/app/providers/DossierProvider.d.ts +0 -16
  39. package/components/app/providers/DossierProvider.js +0 -82
  40. package/components/app/providers/TemplateProvider.d.ts +0 -14
  41. package/components/app/providers/TemplateProvider.js +0 -103
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Analytics, InfoOutlined } from '@mui/icons-material';
3
3
  import { Alert, AlertTitle, Autocomplete, Box, Button, Chip, CircularProgress, Divider, Fade, Grid, LinearProgress, Stack, TextField, Tooltip, Typography } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
5
6
  import { FieldContext } from '@cccsaurora/howler-ui/components/app/providers/FieldProvider';
6
7
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
7
8
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
8
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
9
9
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
10
10
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
11
11
  import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
@@ -19,11 +19,11 @@ import PluginChip from '../PluginChip';
19
19
  import HitGraph from './aggregate/HitGraph';
20
20
  const HitSummary = ({ query, response, onStart, onComplete }) => {
21
21
  const { t } = useTranslation();
22
- const getMatchingTemplate = useContextSelector(TemplateContext, ctx => ctx.getMatchingTemplate);
23
22
  const { dispatchApi } = useMyApi();
24
23
  const { hitFields } = useContext(FieldContext);
25
24
  const { showErrorMessage } = useMySnackbar();
26
25
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
26
+ const { getMatchingTemplate } = useMatchers();
27
27
  const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
28
28
  const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
29
29
  const searching = useContextSelector(HitSearchContext, ctx => ctx.searching);
@@ -48,16 +48,17 @@ const HitSummary = ({ query, response, onStart, onComplete }) => {
48
48
  }
49
49
  try {
50
50
  // Get a list of every key in every template of the hits we're searching
51
- const _keyCounts = (response?.items ?? [])
52
- .flatMap(h => {
53
- const matchingTemplate = getMatchingTemplate(h);
51
+ const rawCounts = await Promise.all((response?.items ?? []).map(async (h) => {
52
+ const matchingTemplate = await getMatchingTemplate(h);
54
53
  return (matchingTemplate?.keys ?? [])
55
54
  .filter(key => !['howler.id', 'howler.hash'].includes(key))
56
55
  .map(key => ({
57
56
  key,
58
57
  source: `${matchingTemplate.analytic}: ${matchingTemplate.detection ?? t('any')}`
59
58
  }));
60
- })
59
+ }));
60
+ const _keyCounts = rawCounts
61
+ .flat()
61
62
  .concat(customKeys.map(key => ({ key, source: 'custom' })))
62
63
  // Take that array and reduce it to unique keys and the number of times we see it,
63
64
  // as well as the templates we sourced this key from
@@ -34,9 +34,16 @@ const PivotLink = ({ pivot, hit, compact = false }) => {
34
34
  if (href) {
35
35
  return (_jsx(RelatedLink, { title: pivot.label[i18n.language], href: href, compact: compact, children: _jsx(Icon, { fontSize: "1.5rem", icon: pivot.icon }) }));
36
36
  }
37
- const pluginPivot = pluginStore.executeFunction(`pivot.${pivot.format}`, { pivot, hit, compact });
38
- if (pluginPivot) {
39
- return pluginPivot;
37
+ try {
38
+ const pluginPivot = pluginStore.executeFunction(`pivot.${pivot.format}`, { pivot, hit, compact });
39
+ if (pluginPivot) {
40
+ return pluginPivot;
41
+ }
42
+ }
43
+ catch (e) {
44
+ // eslint-disable-next-line no-console
45
+ console.warn(`Pivot plugin for format ${pivot.format} does not exist, not rendering`);
46
+ return null;
40
47
  }
41
48
  return _jsx(Skeleton, { variant: "rounded" });
42
49
  };
@@ -1,24 +1,27 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Article } from '@mui/icons-material';
3
3
  import { Box, Fab, Skeleton, Stack, Typography, useMediaQuery } from '@mui/material';
4
+ import api from '@cccsaurora/howler-ui/api';
4
5
  import 'chartjs-adapter-moment';
5
6
  import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
6
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
7
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
7
8
  import { useEffect, useState } from 'react';
8
9
  import { useTranslation } from 'react-i18next';
9
10
  import { Link } from 'react-router-dom';
10
- import { useContextSelector } from 'use-context-selector';
11
11
  import TemplateCard from '../templates/TemplateCard';
12
12
  const AnalyticTemplates = ({ analytic }) => {
13
13
  const { t } = useTranslation();
14
14
  const isNarrow = useMediaQuery('(max-width: 1800px)');
15
- const getTemplates = useContextSelector(TemplateContext, ctx => ctx.getTemplates);
16
- const templates = useContextSelector(TemplateContext, ctx => ctx.templates.filter(_template => _template.analytic === analytic?.name));
15
+ const { dispatchApi } = useMyApi();
16
+ const [templates, setTemplates] = useState([]);
17
17
  const [loading, setLoading] = useState(false);
18
18
  useEffect(() => {
19
19
  setLoading(true);
20
- getTemplates().finally(() => setLoading(false));
21
- }, [getTemplates]);
20
+ dispatchApi(api.template.get())
21
+ .then(_templates => _templates.filter(_template => _template.analytic === analytic?.name))
22
+ .then(setTemplates)
23
+ .finally(() => setLoading(false));
24
+ }, [analytic?.name, dispatchApi]);
22
25
  if (!analytic) {
23
26
  return _jsx(Skeleton, { variant: "rounded", width: "100%", sx: { minHeight: '300px', mt: 2 } });
24
27
  }
@@ -3,10 +3,9 @@ import { Clear, Code, Comment, DataObject, History, LinkSharp, OpenInNew, QueryS
3
3
  import { Badge, Box, Divider, Skeleton, Stack, Tab, Tabs, Tooltip, useTheme } from '@mui/material';
4
4
  import TuiIconButton from '@cccsaurora/howler-ui/components/elements/addons/buttons/CustomIconButton';
5
5
  import { Icon } from '@iconify/react/dist/iconify.js';
6
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
6
7
  import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
7
- import { DossierContext } from '@cccsaurora/howler-ui/components/app/providers/DossierProvider';
8
8
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
9
- import { OverviewContext } from '@cccsaurora/howler-ui/components/app/providers/OverviewProvider';
10
9
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
11
10
  import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
12
11
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
@@ -48,13 +47,13 @@ const InformationPane = ({ onClose }) => {
48
47
  const location = useLocation();
49
48
  const { emit, isOpen } = useContext(SocketContext);
50
49
  const { getAnalyticFromName } = useContext(AnalyticContext);
51
- const { getMatchingOverview, refresh } = useContext(OverviewContext);
50
+ const { getMatchingOverview, getMatchingDossiers } = useMatchers();
52
51
  const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
53
52
  const pluginStore = usePluginStore();
54
- const getMatchingDossiers = useContextSelector(DossierContext, ctx => ctx.getMatchingDossiers);
55
53
  const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
56
54
  const [userIds, setUserIds] = useState(new Set());
57
55
  const [analytic, setAnalytic] = useState();
56
+ const [hasOverview, setHasOverview] = useState(false);
58
57
  const [tab, setTab] = useState('overview');
59
58
  const [loading, setLoading] = useState(false);
60
59
  const [dossiers, setDossiers] = useState([]);
@@ -63,33 +62,28 @@ const InformationPane = ({ onClose }) => {
63
62
  howlerPluginStore.plugins.forEach(plugin => {
64
63
  pluginStore.executeFunction(`${plugin}.on`, 'viewing');
65
64
  });
65
+ useEffect(() => {
66
+ getMatchingOverview(hit).then(_overview => setHasOverview(!!_overview));
67
+ }, [getMatchingOverview, hit]);
66
68
  useEffect(() => {
67
69
  if (!selected) {
68
70
  return;
69
71
  }
70
- (async () => {
71
- if (selected && !hit) {
72
- setLoading(true);
73
- try {
74
- await getHit(selected, true);
75
- }
76
- finally {
77
- setLoading(false);
78
- return;
79
- }
80
- }
81
- else if (!hit?.howler.data) {
82
- getHit(selected, true);
83
- }
84
- setUserIds(getUserList(hit));
85
- setAnalytic(await getAnalyticFromName(hit.howler.analytic));
86
- if (tab === 'hit_aggregate' && !hit.howler.is_bundle) {
87
- setTab('overview');
88
- }
89
- })();
72
+ if (!hit?.howler.data) {
73
+ setLoading(true);
74
+ getHit(selected, true).finally(() => setLoading(false));
75
+ return;
76
+ }
77
+ setUserIds(getUserList(hit));
78
+ getAnalyticFromName(hit.howler.analytic).then(setAnalytic);
79
+ getMatchingDossiers(hit).then(setDossiers);
90
80
  // eslint-disable-next-line react-hooks/exhaustive-deps
91
- }, [getAnalyticFromName, getHit, selected, tab]);
92
- const matchingOverview = useMemo(() => getMatchingOverview(hit), [getMatchingOverview, hit]);
81
+ }, [getAnalyticFromName, getHit, selected]);
82
+ useEffect(() => {
83
+ if (tab === 'hit_aggregate' && !hit?.howler.is_bundle) {
84
+ setTab('overview');
85
+ }
86
+ }, [hit?.howler.is_bundle, tab]);
93
87
  useEffect(() => {
94
88
  if (selected && isOpen()) {
95
89
  emit({
@@ -105,17 +99,14 @@ const InformationPane = ({ onClose }) => {
105
99
  }
106
100
  }, [emit, selected, isOpen]);
107
101
  useEffect(() => {
108
- refresh();
109
- }, [refresh]);
110
- useEffect(() => {
111
- if (matchingOverview && tab === 'details') {
102
+ if (hasOverview && tab === 'details') {
112
103
  setTab('overview');
113
104
  }
114
- else if (!matchingOverview && tab === 'overview') {
105
+ else if (!hasOverview && tab === 'overview') {
115
106
  setTab('details');
116
107
  }
117
108
  // eslint-disable-next-line react-hooks/exhaustive-deps
118
- }, [matchingOverview]);
109
+ }, [hasOverview]);
119
110
  /**
120
111
  * What to show as the header? If loading a skeleton, then it depends on bundle or not. Bundles don't
121
112
  * show anything while normal hits do
@@ -151,12 +142,6 @@ const InformationPane = ({ onClose }) => {
151
142
  ])))
152
143
  }[tab]?.();
153
144
  }, [dossiers, hit, loading, tab, users]);
154
- useEffect(() => {
155
- if (!hit) {
156
- return;
157
- }
158
- getMatchingDossiers(hit.howler.id).then(setDossiers);
159
- }, [getMatchingDossiers, hit]);
160
145
  return (_jsxs(VSBox, { top: 10, sx: { height: '100%', flex: 1 }, children: [_jsxs(Stack, { direction: "column", flex: 1, sx: { overflowY: 'auto', flexGrow: 1 }, position: "relative", spacing: 1, ml: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, flexShrink: 0, pr: 2, sx: [hit?.howler?.is_bundle && { position: 'absolute', top: 1, right: 0, zIndex: 1100 }], children: [_jsx(FlexOne, {}), onClose && !location.pathname.startsWith('/bundles') && (_jsx(TuiIconButton, { size: "small", onClick: onClose, tooltip: t('hit.panel.details.exit'), children: _jsx(Clear, {}) })), _jsx(SocketBadge, { size: "small" }), analytic && (_jsx(TuiIconButton, { size: "small", tooltip: t('hit.panel.analytic.open'), disabled: !analytic || loading, route: `/analytics/${analytic.analytic_id}`, children: _jsx(QueryStats, {}) })), hit?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles, disabled: loading }), !!hit && !hit.howler.is_bundle && (_jsx(TuiIconButton, { tooltip: t('hit.panel.open'), href: `/hits/${selected}`, disabled: !hit || loading, size: "small", target: "_blank", children: _jsx(OpenInNew, {}) }))] }), _jsx(Box, { pr: 2, children: header }), !!hit &&
161
146
  !hit.howler.is_bundle &&
162
147
  (!loading ? (_jsxs(_Fragment, { children: [_jsx(HitOutline, { hit: hit, layout: HitLayout.DENSE }), _jsx(HitLabels, { hit: hit })] })) : (_jsx(Skeleton, { height: 124 }))), (hit?.howler?.links?.length > 0 ||
@@ -166,7 +151,7 @@ const InformationPane = ({ onClose }) => {
166
151
  .slice(0, 3)
167
152
  .map(l => _jsx(RelatedLink, { compact: true, ...l }, l.href)), dossiers.flatMap(_dossier => (_dossier.pivots ?? []).map((_pivot, index) => (
168
153
  // eslint-disable-next-line react/no-array-index-key
169
- _jsx(PivotLink, { pivot: _pivot, hit: hit, compact: true }, _dossier.dossier_id + index))))] })), _jsx(VSBoxHeader, { ml: -1, mr: -1, pb: 1, sx: { top: '0px' }, children: _jsxs(Tabs, { value: tab === 'overview' && !matchingOverview ? 'details' : tab, sx: {
154
+ _jsx(PivotLink, { pivot: _pivot, hit: hit, compact: true }, _dossier.dossier_id + index))))] })), _jsx(VSBoxHeader, { ml: -1, mr: -1, pb: 1, sx: { top: '0px' }, children: _jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: {
170
155
  display: 'flex',
171
156
  flexDirection: 'row',
172
157
  pr: 2,
@@ -195,7 +180,7 @@ const InformationPane = ({ onClose }) => {
195
180
  right: theme.spacing(-0.5)
196
181
  },
197
182
  '& > svg': { zIndex: 2 }
198
- }, badgeContent: hit?.howler.comment?.length ?? 0, children: _jsx(Comment, {}) }) }), value: "hit_comments", onClick: () => setTab('hit_comments') }), 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
183
+ }, badgeContent: hit?.howler.comment?.length ?? 0, children: _jsx(Comment, {}) }) }), value: "hit_comments", onClick: () => setTab('hit_comments') }), 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
199
184
  // eslint-disable-next-line react/no-array-index-key
200
185
  , { 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
201
186
  // eslint-disable-next-line react/no-array-index-key
@@ -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';
@@ -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,11 +3,9 @@ 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 useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
6
7
  import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
7
- import { DossierContext } from '@cccsaurora/howler-ui/components/app/providers/DossierProvider';
8
8
  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
9
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
12
10
  import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
13
11
  import BundleButton from '@cccsaurora/howler-ui/components/elements/display/icons/BundleButton';
@@ -49,10 +47,8 @@ const HitViewer = () => {
49
47
  const theme = useTheme();
50
48
  const isUnderLg = useMediaQuery(theme.breakpoints.down('lg'));
51
49
  const [orientation, setOrientation] = useMyLocalStorageItem(StorageKey.VIEWER_ORIENTATION, Orientation.VERTICAL);
52
- const refreshTemplates = useContextSelector(TemplateContext, ctx => ctx.refresh);
53
50
  const { getAnalyticFromName } = useContext(AnalyticContext);
54
- const { getMatchingOverview, refresh: refreshOverviews } = useContext(OverviewContext);
55
- const getMatchingDossiers = useContextSelector(DossierContext, ctx => ctx.getMatchingDossiers);
51
+ const { getMatchingOverview, getMatchingDossiers } = useMatchers();
56
52
  const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
57
53
  const hit = useContextSelector(HitContext, ctx => ctx.hits[params.id]);
58
54
  const [userIds, setUserIds] = useState(new Set());
@@ -60,22 +56,22 @@ const HitViewer = () => {
60
56
  const [tab, setTab] = useState('details');
61
57
  const [analytic, setAnalytic] = useState();
62
58
  const [dossiers, setDossiers] = useState([]);
59
+ const [hasOverview, setHasOverview] = useState(false);
63
60
  const fetchData = useCallback(async () => {
64
61
  try {
65
- let existingHit = hit;
66
- if (!existingHit) {
67
- existingHit = await getHit(params.id, true);
62
+ if (!hit) {
63
+ await getHit(params.id, true);
64
+ return;
68
65
  }
69
- setUserIds(getUserList(existingHit));
70
- setAnalytic(await getAnalyticFromName(existingHit.howler.analytic));
66
+ setUserIds(getUserList(hit));
67
+ setAnalytic(await getAnalyticFromName(hit.howler.analytic));
71
68
  }
72
69
  catch (err) {
73
70
  if (err.cause?.api_status_code === 404) {
74
71
  navigate('/404');
75
72
  }
76
73
  }
77
- // eslint-disable-next-line react-hooks/exhaustive-deps
78
- }, [getAnalyticFromName, getHit, params.id, navigate]);
74
+ }, [hit, getAnalyticFromName, getHit, params.id, navigate]);
79
75
  useEffect(() => {
80
76
  if (isUnderLg) {
81
77
  setOrientation(Orientation.HORIZONTAL);
@@ -83,22 +79,20 @@ const HitViewer = () => {
83
79
  }, [isUnderLg, setOrientation]);
84
80
  useEffect(() => {
85
81
  fetchData();
86
- }, [params.id, fetchData]);
82
+ }, [params.id, fetchData, hit]);
87
83
  const onOrientationChange = useCallback(() => setOrientation(orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL), [orientation, setOrientation]);
88
- const matchingOverview = useMemo(() => getMatchingOverview(hit), [getMatchingOverview, hit]);
89
84
  useEffect(() => {
90
- refreshTemplates();
91
- refreshOverviews();
92
- }, [refreshOverviews, refreshTemplates]);
85
+ getMatchingOverview(hit).then(_overview => setHasOverview(!!_overview));
86
+ }, [getMatchingOverview, hit]);
93
87
  useEffect(() => {
94
- if (matchingOverview && tab === 'details') {
88
+ if (hasOverview && tab === 'details') {
95
89
  setTab('overview');
96
90
  }
97
- else if (!matchingOverview && tab === 'overview') {
91
+ else if (!hasOverview && tab === 'overview') {
98
92
  setTab('details');
99
93
  }
100
94
  // eslint-disable-next-line react-hooks/exhaustive-deps
101
- }, [matchingOverview]);
95
+ }, [hasOverview]);
102
96
  const tabContent = useMemo(() => {
103
97
  if (!tab || !hit) {
104
98
  return;
@@ -122,7 +116,7 @@ const HitViewer = () => {
122
116
  if (!hit) {
123
117
  return;
124
118
  }
125
- getMatchingDossiers(hit.howler.id).then(setDossiers);
119
+ getMatchingDossiers(hit).then(setDossiers);
126
120
  }, [getMatchingDossiers, hit]);
127
121
  if (!hit) {
128
122
  return (_jsx(PageCenter, { children: _jsx(Skeleton, { variant: "rounded", height: "520px" }) }));
@@ -150,7 +144,7 @@ const HitViewer = () => {
150
144
  position: 'absolute',
151
145
  top: theme.spacing(2),
152
146
  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
147
+ }, 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
148
  // eslint-disable-next-line react/no-array-index-key
155
149
  , { 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
150
  // eslint-disable-next-line react/no-array-index-key
@@ -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;
@@ -1,18 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Article, KeyboardArrowDown } from '@mui/icons-material';
3
- import { Box, Card, Collapse, IconButton, Stack, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from '@mui/material';
2
+ import { Article } from '@mui/icons-material';
3
+ import { Stack, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
5
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
6
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
7
6
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
8
7
  import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
9
8
  import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
10
9
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
11
10
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
12
- import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
11
+ import { useCallback, useContext, useEffect, useState } from 'react';
13
12
  import { useTranslation } from 'react-i18next';
14
13
  import { useNavigate, useSearchParams } from 'react-router-dom';
15
- import { useContextSelector } from 'use-context-selector';
16
14
  import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
17
15
  import TemplateCard from './TemplateCard';
18
16
  const TemplatesBase = () => {
@@ -22,11 +20,9 @@ const TemplatesBase = () => {
22
20
  const { dispatchApi } = useMyApi();
23
21
  const [searchParams, setSearchParams] = useSearchParams();
24
22
  const { load } = useContext(TuiListMethodContext);
25
- const templates = useContextSelector(TemplateContext, ctx => ctx.templates);
26
23
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
27
24
  const [phrase, setPhrase] = useState('');
28
25
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
29
- const [showBuiltins, setShowBuiltins] = useState(true);
30
26
  const [response, setResponse] = useState(null);
31
27
  const [types, setTypes] = useState([]);
32
28
  const [hasError, setHasError] = useState(false);
@@ -100,15 +96,12 @@ const TemplatesBase = () => {
100
96
  }
101
97
  // eslint-disable-next-line react-hooks/exhaustive-deps
102
98
  }, [offset]);
103
- const builtInTemplates = useMemo(() => templates.filter(template => template.type === 'readonly'), [templates]);
104
99
  const renderer = useCallback((item, className) => _jsx(TemplateCard, { template: item, className: className }), []);
105
100
  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', alignSelf: 'start' }, size: "small", value: types, onChange: (__, _types) => {
106
101
  if (_types) {
107
102
  setTypes(_types.length < 2 ? _types : []);
108
103
  }
109
- }, children: [_jsx(ToggleButton, { value: "personal", "aria-label": "personal", children: t('route.templates.manager.personal') }), _jsx(ToggleButton, { value: "global", "aria-label": "global", children: t('route.templates.manager.global') })] }) }), aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.templates.search.prompt') }), belowSearch: types.length !== 1 &&
110
- offset < 1 &&
111
- builtInTemplates.length > 0 && (_jsx(Card, { sx: { p: 1, mb: 1 }, children: _jsxs(Stack, { children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Typography, { children: t('route.templates.builtin.show') }), _jsx(Tooltip, { title: t(`route.templates.builtin.${showBuiltins ? 'hide' : 'show'}`), children: _jsx(IconButton, { size: "small", onClick: () => setShowBuiltins(!showBuiltins), children: _jsx(KeyboardArrowDown, { fontSize: "small", sx: { transition: 'rotate 250ms', rotate: showBuiltins ? '180deg' : '0deg' } }) }) })] }), _jsxs(Collapse, { in: showBuiltins, children: [_jsx(Box, { sx: { mt: 1 } }), builtInTemplates.map(template => renderer(template))] })] }) })), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/templates/view?type=${item.item.type}&analytic=${item.item.analytic}${item.item.detection ? '&detection=' + item.item.detection : ''}`), onCreate: () => navigate('/templates/view'), createPrompt: "route.templates.create", searchPrompt: "route.templates.manager.search", createIcon: _jsx(Article, { sx: { mr: 1 } }) }));
104
+ }, children: [_jsx(ToggleButton, { value: "personal", "aria-label": "personal", children: t('route.templates.manager.personal') }), _jsx(ToggleButton, { value: "global", "aria-label": "global", children: t('route.templates.manager.global') })] }) }), aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.templates.search.prompt') }), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/templates/view?type=${item.item.type}&analytic=${item.item.analytic}${item.item.detection ? '&detection=' + item.item.detection : ''}`), onCreate: () => navigate('/templates/view'), createPrompt: "route.templates.create", searchPrompt: "route.templates.manager.search", createIcon: _jsx(Article, { sx: { mr: 1 } }) }));
112
105
  };
113
106
  const Templates = () => {
114
107
  return (_jsx(TuiListProvider, { children: _jsx(TemplatesBase, {}) }));
@@ -639,6 +639,7 @@
639
639
  "route.views.manager.search": "Search Views",
640
640
  "route.views.manager.default": "Default View",
641
641
  "route.views.name": "View Name",
642
+ "route.views.save": "Save Query as View",
642
643
  "route.views.saved": "Pinned Views",
643
644
  "route.views.search.prompt": "Search by name, query, or owner.",
644
645
  "search.open": "Open Search",
@@ -635,6 +635,7 @@
635
635
  "route.views.manager.readonly": "Intégré",
636
636
  "route.views.manager.search": "Rechercher les vues",
637
637
  "route.views.manager": "Gérer vue",
638
+ "route.views.save": "Enregistrer cette requête comme vue",
638
639
  "route.views.saved": "Vues épinglées",
639
640
  "route.views.show": "Voir les vues",
640
641
  "route.views": "Vues",