@cccsaurora/howler-ui 2.19.0-dev.1000 → 2.19.0-dev.1085

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 (56) hide show
  1. package/api/action/index.d.ts +5 -4
  2. package/api/action/index.js +8 -8
  3. package/api/analytic/index.d.ts +3 -2
  4. package/api/analytic/index.js +4 -4
  5. package/api/analytic/owner.d.ts +2 -1
  6. package/api/analytic/owner.js +2 -2
  7. package/api/analytic/rules.d.ts +2 -1
  8. package/api/analytic/rules.js +2 -2
  9. package/api/dossier/index.d.ts +4 -3
  10. package/api/dossier/index.js +6 -6
  11. package/api/hit/index.d.ts +3 -2
  12. package/api/hit/index.js +4 -4
  13. package/api/hit/labels.d.ts +3 -2
  14. package/api/hit/labels.js +4 -4
  15. package/api/hit/overwrite.d.ts +2 -1
  16. package/api/hit/overwrite.js +2 -2
  17. package/api/hit/transition.d.ts +2 -1
  18. package/api/hit/transition.js +2 -2
  19. package/api/index.d.ts +5 -4
  20. package/api/index.js +8 -8
  21. package/api/overview/index.d.ts +4 -3
  22. package/api/overview/index.js +6 -6
  23. package/api/search/overview.d.ts +2 -2
  24. package/api/template/index.d.ts +4 -3
  25. package/api/template/index.js +6 -6
  26. package/api/user/index.d.ts +2 -1
  27. package/api/user/index.js +2 -2
  28. package/api/view/index.d.ts +4 -3
  29. package/api/view/index.js +6 -6
  30. package/components/app/providers/SearchResponseProvider.d.ts +21 -0
  31. package/components/app/providers/SearchResponseProvider.js +81 -0
  32. package/components/app/providers/SearchResponseProvider.test.d.ts +1 -0
  33. package/components/app/providers/SearchResponseProvider.test.js +406 -0
  34. package/components/app/providers/ViewProvider.test.js +4 -1
  35. package/components/elements/addons/search/SearchPagination.d.ts +2 -1
  36. package/components/elements/addons/search/SearchPagination.js +4 -4
  37. package/components/elements/addons/search/SearchTotal.test.js +3 -1
  38. package/components/elements/display/ItemManager.d.ts +2 -2
  39. package/components/elements/display/ItemManager.js +1 -1
  40. package/components/hooks/useMyApi.d.ts +1 -1
  41. package/components/routes/action/useMyActionFunctions.js +5 -2
  42. package/components/routes/action/view/ActionDetails.js +7 -1
  43. package/components/routes/action/view/ActionSearch.js +20 -14
  44. package/components/routes/analytics/AnalyticDetails.js +1 -1
  45. package/components/routes/analytics/AnalyticSearch.js +6 -6
  46. package/components/routes/dossiers/Dossiers.js +13 -11
  47. package/components/routes/home/AddNewCard.test.js +20 -2
  48. package/components/routes/home/ViewRefresh.test.js +1 -1
  49. package/components/routes/overviews/OverviewViewer.js +6 -2
  50. package/components/routes/overviews/Overviews.js +13 -11
  51. package/components/routes/templates/TemplateViewer.js +7 -2
  52. package/components/routes/templates/Templates.js +16 -11
  53. package/components/routes/views/Views.js +15 -12
  54. package/locales/en/translation.json +4 -1
  55. package/locales/fr/translation.json +4 -1
  56. package/package.json +103 -106
@@ -4,6 +4,7 @@ import { AvatarGroup, Card, CardContent, CardHeader, Chip, Divider, Grid, IconBu
4
4
  import api from '@cccsaurora/howler-ui/api';
5
5
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
6
6
  import useLocalStorageItem from '@cccsaurora/howler-ui/commons/components/utils/hooks/useLocalStorageItem';
7
+ import SearchResponseProvider, { SearchResponseContext } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
7
8
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
8
9
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
9
10
  import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
@@ -25,12 +26,12 @@ const AnalyticSearchBase = () => {
25
26
  const [searchParams, setSearchParams] = useSearchParams();
26
27
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
27
28
  const appUser = useAppUser();
29
+ const { response, request } = useContext(SearchResponseContext);
28
30
  const [onlyRules, setOnlyRules] = useLocalStorageItem(StorageKey.ONLY_RULES, 0);
29
31
  const [searching, setSearching] = useState(false);
30
32
  const [hasError, setHasError] = useState(false);
31
33
  const [phrase, setPhrase] = useState(searchParams.get('phrase') || '');
32
34
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
33
- const [response, setResponse] = useState(null);
34
35
  const addFavourite = useCallback(async (analytic) => {
35
36
  await dispatchApi(api.analytic.favourite.post(analytic.analytic_id));
36
37
  appUser.setUser({
@@ -58,13 +59,12 @@ const AnalyticSearchBase = () => {
58
59
  setSearchParams(searchParams, { replace: true });
59
60
  try {
60
61
  const sanitizedPhrase = sanitizeLuceneQuery(phrase);
61
- const _response = await dispatchApi(api.search.analytic.post({
62
+ const _response = await request(api.search.analytic.post, {
62
63
  query: `name:*${sanitizedPhrase}* OR detections:*${sanitizedPhrase}*` +
63
64
  (onlyRules > 0 ? ' AND _exists_:rule_type' : onlyRules < 0 ? ' AND -_exists_:rule_type' : ''),
64
65
  rows: pageCount,
65
66
  offset
66
- }));
67
- setResponse(_response);
67
+ });
68
68
  load(_response.items.map(u => ({ id: u.analytic_id, item: u })));
69
69
  }
70
70
  catch (e) {
@@ -73,7 +73,7 @@ const AnalyticSearchBase = () => {
73
73
  finally {
74
74
  setSearching(false);
75
75
  }
76
- }, [dispatchApi, load, offset, onlyRules, pageCount, phrase, searchParams, setSearchParams]);
76
+ }, [request, load, offset, onlyRules, pageCount, phrase, searchParams, setSearchParams]);
77
77
  const onPageChange = useCallback((_offset) => {
78
78
  if (_offset !== offset) {
79
79
  searchParams.set('offset', _offset.toString());
@@ -133,6 +133,6 @@ const AnalyticSearchBase = () => {
133
133
  return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, searchAdornment: _jsx(InputAdornment, { position: "end", children: _jsx(Tooltip, { title: t(`route.analytics.search.filter.rules.${onlyRules < 0 ? 'hide' : onlyRules > 0 ? 'show' : 'toggle'}`), children: _jsx(IconButton, { onClick: () => setOnlyRules((((onlyRules + 2) % 3) - 1)), children: _jsx(SsidChart, { color: onlyRules < 0 ? 'error' : onlyRules > 0 ? 'info' : 'inherit', sx: { transition: theme.transitions.create(['color']) } }) }) }) }), aboveSearch: _jsx(Typography, { sx: { fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }, variant: "body2", children: t('route.analytics.search.prompt') }), renderer: renderer, response: response, searchPrompt: "route.analytics.manager.search" }));
134
134
  };
135
135
  const AnalyticSearch = () => {
136
- return (_jsx(TuiListProvider, { children: _jsx(AnalyticSearchBase, {}) }));
136
+ return (_jsx(TuiListProvider, { children: _jsx(SearchResponseProvider, { idField: "analytic_id", children: _jsx(AnalyticSearchBase, {}) }) }));
137
137
  };
138
138
  export default AnalyticSearch;
@@ -3,6 +3,7 @@ import { Topic } from '@mui/icons-material';
3
3
  import { Typography } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
5
  import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
6
+ import SearchResponseProvider, { SearchResponseContext } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
6
7
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
7
8
  import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
8
9
  import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
@@ -23,9 +24,9 @@ const DossiersBase = () => {
23
24
  const [searchParams, setSearchParams] = useSearchParams();
24
25
  const { load } = useContext(TuiListMethodContext);
25
26
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
27
+ const { response, request, remove, getSearchRequestData } = useContext(SearchResponseContext);
26
28
  const [phrase, setPhrase] = useState('');
27
29
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
28
- const [response, setResponse] = useState(null);
29
30
  const [hasError, setHasError] = useState(false);
30
31
  const [loading, setLoading] = useState(false);
31
32
  const onSearch = useCallback(async () => {
@@ -42,11 +43,11 @@ const DossiersBase = () => {
42
43
  // Check for the actual search query
43
44
  const query = phrase ? `*:*${phrase}*` : '*:*';
44
45
  // Ensure the overview should be visible and/or matches the type we are filtering for
45
- setResponse(await dispatchApi(api.search.dossier.post({
46
+ await request(api.search.dossier.post, {
46
47
  query,
47
48
  rows: pageCount,
48
49
  offset
49
- })));
50
+ });
50
51
  }
51
52
  catch (e) {
52
53
  setHasError(true);
@@ -54,7 +55,7 @@ const DossiersBase = () => {
54
55
  finally {
55
56
  setLoading(false);
56
57
  }
57
- }, [phrase, setSearchParams, searchParams, dispatchApi, pageCount, offset]);
58
+ }, [phrase, setSearchParams, searchParams, request, pageCount, offset]);
58
59
  // Load the items into list when response changes.
59
60
  // This hook should only trigger when the 'response' changes.
60
61
  useEffect(() => {
@@ -70,18 +71,19 @@ const DossiersBase = () => {
70
71
  }, [response, load]);
71
72
  const onPageChange = useCallback((_offset) => {
72
73
  if (_offset !== offset) {
73
- searchParams.set('offset', _offset.toString());
74
+ const modifiedRequest = getSearchRequestData({ offset: _offset });
75
+ searchParams.set('offset', modifiedRequest.offset.toString());
74
76
  setSearchParams(searchParams, { replace: true });
75
- setOffset(_offset);
77
+ setOffset(modifiedRequest.offset);
76
78
  }
77
- }, [offset, searchParams, setSearchParams]);
79
+ }, [offset, searchParams, setSearchParams, getSearchRequestData]);
78
80
  const onDelete = useCallback((e, id) => {
79
81
  e.preventDefault();
80
82
  e.stopPropagation();
81
83
  withConfirmDeleteModal(async () => {
82
84
  try {
83
- await dispatchApi(api.dossier.del(id), { throwError: false, showError: true });
84
- await onSearch();
85
+ await dispatchApi(api.dossier.del(id), { throwError: true, showError: true });
86
+ remove(id);
85
87
  showSuccessMessage(t('route.dossiers.manager.delete.success'));
86
88
  }
87
89
  catch (_err) {
@@ -89,7 +91,7 @@ const DossiersBase = () => {
89
91
  console.warn(_err);
90
92
  }
91
93
  });
92
- }, [dispatchApi, onSearch, withConfirmDeleteModal, showSuccessMessage, t]);
94
+ }, [dispatchApi, remove, withConfirmDeleteModal, showSuccessMessage, t]);
93
95
  useEffect(() => {
94
96
  onSearch();
95
97
  if (!searchParams.has('offset')) {
@@ -115,6 +117,6 @@ const DossiersBase = () => {
115
117
  return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: loading, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.dossiers.search.prompt') }), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/dossiers/${item.id}/edit`), onCreate: () => navigate('/dossiers/create'), createPrompt: "route.dossiers.create", searchPrompt: "route.dossiers.manager.search", createIcon: _jsx(Topic, { sx: { mr: 1 } }) }));
116
118
  };
117
119
  const Dossiers = () => {
118
- return (_jsx(TuiListProvider, { children: _jsx(DossiersBase, {}) }));
120
+ return (_jsx(TuiListProvider, { children: _jsx(SearchResponseProvider, { idField: "dossier_id", children: _jsx(DossiersBase, {}) }) }));
119
121
  };
120
122
  export default Dossiers;
@@ -25,8 +25,26 @@ const mockFetchViews = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
25
25
  vi.mock('components/app/providers/ViewProvider', () => ({
26
26
  ViewContext: React.createContext({
27
27
  views: {
28
- 'view-1': { view_id: 'view-1', title: 'My View', query: 'howler.status:open', sort: 'event.created desc', span: 'date.range.1.month', type: 'personal', owner: 'testuser', settings: { advance_on_triage: false } },
29
- 'view-2': { view_id: 'view-2', title: 'Second View', query: 'howler.status:open', sort: 'event.created desc', span: 'date.range.1.month', type: 'personal', owner: 'testuser', settings: { advance_on_triage: false } }
28
+ 'view-1': {
29
+ view_id: 'view-1',
30
+ title: 'My View',
31
+ query: 'howler.status:open',
32
+ sort: 'event.created desc',
33
+ span: 'date.range.1.month',
34
+ type: 'personal',
35
+ owner: 'testuser',
36
+ settings: { advance_on_triage: false }
37
+ },
38
+ 'view-2': {
39
+ view_id: 'view-2',
40
+ title: 'Second View',
41
+ query: 'howler.status:open',
42
+ sort: 'event.created desc',
43
+ span: 'date.range.1.month',
44
+ type: 'personal',
45
+ owner: 'testuser',
46
+ settings: { advance_on_triage: false }
47
+ }
30
48
  },
31
49
  fetchViews: mockFetchViews
32
50
  })
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /// <reference types="vitest" />
3
- import { render, screen, act } from '@testing-library/react';
3
+ import { act, render, screen } from '@testing-library/react';
4
4
  import i18n from '@cccsaurora/howler-ui/i18n';
5
5
  import React from 'react';
6
6
  import { I18nextProvider } from 'react-i18next';
@@ -12,6 +12,7 @@ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/Mod
12
12
  import { OverviewContext } from '@cccsaurora/howler-ui/components/app/providers/OverviewProvider';
13
13
  import HitOverview from '@cccsaurora/howler-ui/components/elements/hit/HitOverview';
14
14
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
15
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
15
16
  import useMyTheme from '@cccsaurora/howler-ui/components/hooks/useMyTheme';
16
17
  import { useSearchParams } from 'react-router-dom';
17
18
  import hitsData from '@cccsaurora/howler-ui/utils/hit.json';
@@ -27,6 +28,7 @@ const OverviewViewer = () => {
27
28
  const { getOverviews } = useContext(OverviewContext);
28
29
  const { dispatchApi } = useMyApi();
29
30
  const { withConfirmDeleteModal } = useContext(ModalContext);
31
+ const { showSuccessMessage } = useMySnackbar();
30
32
  const [overviewList, setOverviewList] = useState([]);
31
33
  const [selectedOverview, setSelectedOverview] = useState(null);
32
34
  const [content, setContent] = useState('');
@@ -131,12 +133,14 @@ const OverviewViewer = () => {
131
133
  await dispatchApi(api.overview.del(selectedOverview.overview_id), {
132
134
  logError: false,
133
135
  showError: true,
134
- throwError: false
136
+ throwError: true
135
137
  });
138
+ setOverviewList(l => l.filter(v => v.overview_id !== selectedOverview.overview_id));
136
139
  setSelectedOverview(null);
137
140
  setContent('');
141
+ showSuccessMessage(t('route.overviews.manager.delete.success'));
138
142
  });
139
- }, [dispatchApi, selectedOverview?.overview_id, withConfirmDeleteModal]);
143
+ }, [dispatchApi, selectedOverview?.overview_id, withConfirmDeleteModal, showSuccessMessage, t]);
140
144
  const onSave = useCallback(async () => {
141
145
  if (analytic && detection) {
142
146
  try {
@@ -3,6 +3,7 @@ import { Article } from '@mui/icons-material';
3
3
  import { Typography } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
5
  import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
6
+ import SearchResponseProvider, { SearchResponseContext } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
6
7
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
7
8
  import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
8
9
  import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
@@ -25,9 +26,9 @@ const OverviewsBase = () => {
25
26
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
26
27
  const [phrase, setPhrase] = useState('');
27
28
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
28
- const [response, setResponse] = useState(null);
29
29
  const [hasError, setHasError] = useState(false);
30
30
  const [loading, setLoading] = useState(false);
31
+ const { response, request, remove, getSearchRequestData } = useContext(SearchResponseContext);
31
32
  const onSearch = useCallback(async () => {
32
33
  try {
33
34
  setLoading(true);
@@ -42,11 +43,11 @@ const OverviewsBase = () => {
42
43
  // Check for the actual search query
43
44
  const query = phrase ? `*:*${phrase}*` : '*:*';
44
45
  // Ensure the overview should be visible and/or matches the type we are filtering for
45
- setResponse(await dispatchApi(api.search.overview.post({
46
+ await request(api.search.overview.post, {
46
47
  query,
47
48
  rows: pageCount,
48
49
  offset
49
- })));
50
+ });
50
51
  }
51
52
  catch (e) {
52
53
  setHasError(true);
@@ -54,7 +55,7 @@ const OverviewsBase = () => {
54
55
  finally {
55
56
  setLoading(false);
56
57
  }
57
- }, [phrase, setSearchParams, searchParams, dispatchApi, pageCount, offset]);
58
+ }, [phrase, setSearchParams, searchParams, request, pageCount, offset]);
58
59
  // Load the items into list when response changes.
59
60
  // This hook should only trigger when the 'response' changes.
60
61
  useEffect(() => {
@@ -70,18 +71,19 @@ const OverviewsBase = () => {
70
71
  }, [response, load]);
71
72
  const onPageChange = useCallback((_offset) => {
72
73
  if (_offset !== offset) {
73
- searchParams.set('offset', _offset.toString());
74
+ const modifiedRequest = getSearchRequestData({ offset: _offset });
75
+ searchParams.set('offset', modifiedRequest.offset.toString());
74
76
  setSearchParams(searchParams, { replace: true });
75
- setOffset(_offset);
77
+ setOffset(modifiedRequest.offset);
76
78
  }
77
- }, [offset, searchParams, setSearchParams]);
79
+ }, [offset, searchParams, setSearchParams, getSearchRequestData]);
78
80
  const onDelete = useCallback((e, id) => {
79
81
  e.preventDefault();
80
82
  e.stopPropagation();
81
83
  withConfirmDeleteModal(async () => {
82
84
  try {
83
- await dispatchApi(api.overview.del(id), { throwError: false, showError: true });
84
- await onSearch();
85
+ await dispatchApi(api.overview.del(id), { throwError: true, showError: true });
86
+ remove(id);
85
87
  showSuccessMessage(t('route.overviews.manager.delete.success'));
86
88
  }
87
89
  catch (_err) {
@@ -89,7 +91,7 @@ const OverviewsBase = () => {
89
91
  console.warn(_err);
90
92
  }
91
93
  });
92
- }, [dispatchApi, onSearch, withConfirmDeleteModal, showSuccessMessage, t]);
94
+ }, [dispatchApi, remove, withConfirmDeleteModal, showSuccessMessage, t]);
93
95
  useEffect(() => {
94
96
  onSearch();
95
97
  if (!searchParams.has('offset')) {
@@ -115,6 +117,6 @@ const OverviewsBase = () => {
115
117
  return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: loading, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.overviews.search.prompt') }), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/overviews/view?analytic=${item.item.analytic}${item.item.detection ? '&detection=' + item.item.detection : ''}`), onCreate: () => navigate('/overviews/view'), createPrompt: "route.overviews.create", searchPrompt: "route.overviews.manager.search", createIcon: _jsx(Article, { sx: { mr: 1 } }) }));
116
118
  };
117
119
  const Overviews = () => {
118
- return (_jsx(TuiListProvider, { children: _jsx(OverviewsBase, {}) }));
120
+ return (_jsx(TuiListProvider, { children: _jsx(SearchResponseProvider, { idField: "overview_id", children: _jsx(OverviewsBase, {}) }) }));
119
121
  };
120
122
  export default Overviews;
@@ -10,6 +10,7 @@ import AppInfoPanel from '@cccsaurora/howler-ui/commons/components/display/AppIn
10
10
  import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
11
11
  import { DEFAULT_FIELDS } from '@cccsaurora/howler-ui/components/elements/hit/HitOutline';
12
12
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
13
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
13
14
  import isEqual from 'lodash-es/isEqual';
14
15
  import { useSearchParams } from 'react-router-dom';
15
16
  import hitsData from '@cccsaurora/howler-ui/utils/hit.json';
@@ -19,6 +20,7 @@ const TemplateViewer = () => {
19
20
  const [params, setParams] = useSearchParams();
20
21
  const { dispatchApi } = useMyApi();
21
22
  const { withConfirmDeleteModal } = useContext(ModalContext);
23
+ const { showSuccessMessage } = useMySnackbar();
22
24
  const [templateList, setTemplateList] = useState([]);
23
25
  const [selectedTemplate, setSelectedTemplate] = useState(null);
24
26
  const [sessionTemplateList, setSessionTemplateList] = useState([]);
@@ -122,7 +124,7 @@ const TemplateViewer = () => {
122
124
  await dispatchApi(api.template.del(selectedTemplate.template_id), {
123
125
  logError: false,
124
126
  showError: true,
125
- throwError: false
127
+ throwError: true
126
128
  });
127
129
  setSessionTemplateList(l => l.filter(v => v.analytic != selectedTemplate.analytic ||
128
130
  v.detection != selectedTemplate.detection ||
@@ -130,6 +132,7 @@ const TemplateViewer = () => {
130
132
  setTemplateList(l => l.filter(v => v.analytic != selectedTemplate.analytic ||
131
133
  v.detection != selectedTemplate.detection ||
132
134
  v.type != selectedTemplate.type));
135
+ showSuccessMessage(t('route.templates.manager.delete.success'));
133
136
  });
134
137
  }, [
135
138
  dispatchApi,
@@ -137,7 +140,9 @@ const TemplateViewer = () => {
137
140
  selectedTemplate?.detection,
138
141
  selectedTemplate?.template_id,
139
142
  selectedTemplate?.type,
140
- withConfirmDeleteModal
143
+ withConfirmDeleteModal,
144
+ showSuccessMessage,
145
+ t
141
146
  ]);
142
147
  const onSave = useCallback(async () => {
143
148
  if (analytic && detection) {
@@ -4,11 +4,13 @@ import { Stack, ToggleButton, ToggleButtonGroup, Typography } from '@mui/materia
4
4
  import api from '@cccsaurora/howler-ui/api';
5
5
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
6
6
  import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
7
+ import SearchResponseProvider, { SearchResponseContext } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
7
8
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
8
9
  import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
9
10
  import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
10
11
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
11
12
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
13
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
12
14
  import { useCallback, useContext, useEffect, useState } from 'react';
13
15
  import { useTranslation } from 'react-i18next';
14
16
  import { useNavigate, useSearchParams } from 'react-router-dom';
@@ -20,12 +22,13 @@ const TemplatesBase = () => {
20
22
  const navigate = useNavigate();
21
23
  const { dispatchApi } = useMyApi();
22
24
  const [searchParams, setSearchParams] = useSearchParams();
23
- const { load, remove } = useContext(TuiListMethodContext);
25
+ const { load } = useContext(TuiListMethodContext);
24
26
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
27
+ const { showSuccessMessage } = useMySnackbar();
25
28
  const { analytics } = useContext(AnalyticContext);
29
+ const { response, request, remove, getSearchRequestData } = useContext(SearchResponseContext);
26
30
  const [phrase, setPhrase] = useState('');
27
31
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
28
- const [response, setResponse] = useState(null);
29
32
  const [types, setTypes] = useState([]);
30
33
  const [hasError, setHasError] = useState(false);
31
34
  const [searching, setSearching] = useState(false);
@@ -44,11 +47,11 @@ const TemplatesBase = () => {
44
47
  const phraseQuery = phrase ? `*:*${phrase}*` : '*:*';
45
48
  // Ensure the template should be visible and/or matches the type we are filtering for
46
49
  const typeQuery = `(type:global OR owner:(${user.username} OR none)) AND type:(${types.join(' ') || '*'})`;
47
- setResponse(await dispatchApi(api.search.template.post({
50
+ await request(api.search.template.post, {
48
51
  query: `${phraseQuery} AND ${typeQuery}`,
49
52
  rows: pageCount,
50
53
  offset
51
- })));
54
+ });
52
55
  }
53
56
  catch (e) {
54
57
  setHasError(true);
@@ -56,7 +59,7 @@ const TemplatesBase = () => {
56
59
  finally {
57
60
  setSearching(false);
58
61
  }
59
- }, [phrase, setSearchParams, searchParams, user.username, types, dispatchApi, pageCount, offset]);
62
+ }, [phrase, setSearchParams, searchParams, user.username, types, request, pageCount, offset]);
60
63
  // Load the items into list when response changes.
61
64
  // This hook should only trigger when the 'response' changes.
62
65
  // or if the analytic list changes to refresh the disabled state
@@ -77,11 +80,12 @@ const TemplatesBase = () => {
77
80
  }, [response, load, analytics]);
78
81
  const onPageChange = useCallback((_offset) => {
79
82
  if (_offset !== offset) {
80
- searchParams.set('offset', _offset.toString());
83
+ const modifiedRequest = getSearchRequestData({ offset: _offset });
84
+ searchParams.set('offset', modifiedRequest.offset.toString());
81
85
  setSearchParams(searchParams, { replace: true });
82
- setOffset(_offset);
86
+ setOffset(modifiedRequest.offset);
83
87
  }
84
- }, [offset, searchParams, setSearchParams]);
88
+ }, [offset, searchParams, setSearchParams, getSearchRequestData]);
85
89
  useEffect(() => {
86
90
  onSearch();
87
91
  if (!searchParams.has('offset')) {
@@ -107,10 +111,11 @@ const TemplatesBase = () => {
107
111
  await dispatchApi(api.template.del(templateId), {
108
112
  logError: false,
109
113
  showError: true,
110
- throwError: false
114
+ throwError: true
111
115
  });
112
116
  remove(templateId);
113
- }, [dispatchApi, remove]);
117
+ showSuccessMessage(t('route.templates.manager.delete.success'));
118
+ }, [dispatchApi, remove, showSuccessMessage, t]);
114
119
  const renderer = useCallback((item, error, className) => (_jsx(TemplateCard, { template: item, error: error, onRemove: removeTemplate, className: className })), [removeTemplate]);
115
120
  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) => {
116
121
  if (_types) {
@@ -121,6 +126,6 @@ const TemplatesBase = () => {
121
126
  }, onCreate: () => navigate('/templates/view'), createPrompt: "route.templates.create", searchPrompt: "route.templates.manager.search", createIcon: _jsx(Article, { sx: { mr: 1 } }) }));
122
127
  };
123
128
  const Templates = () => {
124
- return (_jsx(TuiListProvider, { children: _jsx(TemplatesBase, {}) }));
129
+ return (_jsx(TuiListProvider, { children: _jsx(SearchResponseProvider, { idField: "template_id", children: _jsx(TemplatesBase, {}) }) }));
125
130
  };
126
131
  export default Templates;
@@ -5,6 +5,7 @@ import { Autocomplete, Card, Checkbox, IconButton, Skeleton, Stack, TextField, T
5
5
  import api from '@cccsaurora/howler-ui/api';
6
6
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
7
7
  import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
8
+ import SearchResponseProvider, { SearchResponseContext } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
8
9
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
9
10
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
10
11
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
@@ -12,8 +13,8 @@ import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/
12
13
  import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
13
14
  import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
14
15
  import { ViewTitle } from '@cccsaurora/howler-ui/components/elements/view/ViewTitle';
15
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
16
16
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
17
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
17
18
  import { isNull, omitBy, size } from 'lodash-es';
18
19
  import React, { useCallback, useContext, useEffect, useState } from 'react';
19
20
  import { useTranslation } from 'react-i18next';
@@ -27,8 +28,8 @@ const ViewsBase = () => {
27
28
  const { t } = useTranslation();
28
29
  const { user } = useAppUser();
29
30
  const navigate = useNavigate();
30
- const { dispatchApi } = useMyApi();
31
31
  const { withConfirmDeleteModal } = useContext(ModalContext);
32
+ const { showSuccessMessage } = useMySnackbar();
32
33
  const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
33
34
  const addFavourite = useContextSelector(ViewContext, ctx => ctx.addFavourite);
34
35
  const removeFavourite = useContextSelector(ViewContext, ctx => ctx.removeFavourite);
@@ -39,9 +40,9 @@ const ViewsBase = () => {
39
40
  const [searchParams, setSearchParams] = useSearchParams();
40
41
  const { load } = useContext(TuiListMethodContext);
41
42
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
43
+ const { response, request, remove, getSearchRequestData } = useContext(SearchResponseContext);
42
44
  const [phrase, setPhrase] = useState('');
43
45
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
44
- const [response, setResponse] = useState(null);
45
46
  const [type, setType] = useState(searchParams.get('type') || null);
46
47
  const [hasError, setHasError] = useState(false);
47
48
  const [searching, setSearching] = useState(false);
@@ -63,11 +64,11 @@ const ViewsBase = () => {
63
64
  const phraseQuery = FIELDS_TO_SEARCH.map(_field => `${_field}:${searchTerm}`).join(' OR ');
64
65
  const typeQuery = `(type:global OR owner:(${user.username} OR none)) AND type:(${type ?? '*'}${type === 'personal' ? ' OR readonly' : ''})`;
65
66
  const favouritesQuery = favouritesOnly && user.favourite_views.length > 0 ? ` AND view_id:(${user.favourite_views.join(' OR ')})` : '';
66
- setResponse(await dispatchApi(api.search.view.post({
67
+ await request(api.search.view.post, {
67
68
  query: `(${phraseQuery}) AND ${typeQuery}${favouritesQuery}`,
68
69
  rows: pageCount,
69
70
  offset
70
- })));
71
+ });
71
72
  }
72
73
  catch (e) {
73
74
  setHasError(true);
@@ -83,7 +84,7 @@ const ViewsBase = () => {
83
84
  user.favourite_views,
84
85
  type,
85
86
  favouritesOnly,
86
- dispatchApi,
87
+ request,
87
88
  pageCount,
88
89
  offset
89
90
  ]);
@@ -102,19 +103,21 @@ const ViewsBase = () => {
102
103
  }, [response, load]);
103
104
  const onPageChange = useCallback((_offset) => {
104
105
  if (_offset !== offset) {
105
- searchParams.set('offset', _offset.toString());
106
+ const modifiedRequest = getSearchRequestData({ offset: _offset });
107
+ searchParams.set('offset', modifiedRequest.offset.toString());
106
108
  setSearchParams(searchParams, { replace: true });
107
- setOffset(_offset);
109
+ setOffset(modifiedRequest.offset);
108
110
  }
109
- }, [offset, searchParams, setSearchParams]);
111
+ }, [offset, searchParams, setSearchParams, getSearchRequestData]);
110
112
  const onDelete = useCallback((event, id) => {
111
113
  event.preventDefault();
112
114
  event.stopPropagation();
113
115
  withConfirmDeleteModal(async () => {
114
116
  await removeView(id);
115
- onSearch();
117
+ remove(id);
118
+ showSuccessMessage(t('route.views.manager.delete.success'));
116
119
  });
117
- }, [onSearch, removeView, withConfirmDeleteModal]);
120
+ }, [remove, removeView, withConfirmDeleteModal, showSuccessMessage, t]);
118
121
  const onFavourite = useCallback(async (event, id) => {
119
122
  event.preventDefault();
120
123
  if (user.favourite_views?.includes(id)) {
@@ -182,6 +185,6 @@ const ViewsBase = () => {
182
185
  (item.item.type === 'global' && user.is_admin)) && (_jsx(Tooltip, { title: t('button.edit'), children: _jsx(IconButton, { component: Link, to: `/views/${item.item.view_id}/edit?query=${item.item.query}`, children: _jsx(Edit, {}) }) })), item.item.owner === user.username && item.item.type !== 'readonly' && (_jsx(Tooltip, { title: t('button.delete'), children: _jsx(IconButton, { onClick: event => onDelete(event, item.item.view_id), children: _jsx(Clear, {}) }) })), item.item.type === 'global' && item.item.owner !== user.username && (_jsx(Tooltip, { title: item.item.owner, children: _jsx("div", { children: _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important', marginLeft: '8px !important' }, userId: item.item.owner }) }) })), _jsx(Tooltip, { title: t('button.pin'), children: _jsx(IconButton, { onClick: e => onFavourite(e, item.item.view_id), children: user.favourite_views?.includes(item.item.view_id) ? _jsx(Star, {}) : _jsx(StarBorder, {}) }) })] }) }, item.item.view_id)), response: response, searchPrompt: "route.views.manager.search", onCreate: () => navigate('/views/create'), createPrompt: "route.views.create", createIcon: _jsx(SavedSearch, { sx: { mr: 1 } }) }));
183
186
  };
184
187
  const Views = () => {
185
- return (_jsx(TuiListProvider, { children: _jsx(ViewsBase, {}) }));
188
+ return (_jsx(TuiListProvider, { children: _jsx(SearchResponseProvider, { idField: "view_id", children: _jsx(ViewsBase, {}) }) }));
186
189
  };
187
190
  export default Views;
@@ -448,6 +448,7 @@
448
448
  "route.actions.edit": "Edit",
449
449
  "route.actions.execute": "Execute",
450
450
  "route.actions.manager": "Action Manager",
451
+ "route.actions.manager.delete.success": "Action removed.",
451
452
  "route.actions.name": "Action Name",
452
453
  "route.actions.open": "Open Query",
453
454
  "route.actions.operation.add": "Add New Operation",
@@ -661,7 +662,7 @@
661
662
  "route.overviews.default": "Default",
662
663
  "route.overviews.detection": "Choose Detection",
663
664
  "route.overviews.manager.delete": "Delete Overview",
664
- "route.overviews.manager.delete.success": "Overview Removed.",
665
+ "route.overviews.manager.delete.success": "Overview removed.",
665
666
  "route.overviews.manager.search": "Search Overviews",
666
667
  "route.overviews.prompt": "Activate autocomplete using [ctrl + space].",
667
668
  "route.overviews.search.prompt": "Search by title, content, analytic or detection.",
@@ -679,6 +680,7 @@
679
680
  "route.templates.default": "Default",
680
681
  "route.templates.detection": "Choose Detection",
681
682
  "route.templates.global": "Global",
683
+ "route.templates.manager.delete.success": "Template removed.",
682
684
  "route.templates.manager.error.action": "Click to open quick fix options",
683
685
  "route.templates.manager.error.message": "Invalid Detection",
684
686
  "route.templates.manager.error.modal.description": "The template fields are read only and will not be used. Do you want to remove the template?",
@@ -702,6 +704,7 @@
702
704
  "route.views.manager": "View Manager",
703
705
  "route.views.manager.all": "All",
704
706
  "route.views.manager.default": "Default View",
707
+ "route.views.manager.delete.success": "View removed.",
705
708
  "route.views.manager.favourites": "Show favourites only",
706
709
  "route.views.manager.global": "Global",
707
710
  "route.views.manager.open": "Open View",
@@ -448,6 +448,7 @@
448
448
  "route.actions.edit": "Modifier",
449
449
  "route.actions.execute": "Exécuter",
450
450
  "route.actions.manager": "Gestion des actions",
451
+ "route.actions.manager.delete.success": "Action supprimée.",
451
452
  "route.actions.name": "Nom de l'action",
452
453
  "route.actions.open": "Ouvrir la requête",
453
454
  "route.actions.operation.add": "Ajouter une nouvelle opération",
@@ -661,7 +662,7 @@
661
662
  "route.overviews.default": "Défaut",
662
663
  "route.overviews.detection": "Choisir une détection",
663
664
  "route.overviews.manager.delete": "Supprimer la vue d'ensemble",
664
- "route.overviews.manager.delete.success": "Vue d'ensemble Supprimée.",
665
+ "route.overviews.manager.delete.success": "Vue d'ensemble supprimée.",
665
666
  "route.overviews.manager.search": "Rechercher les vues d'ensemble",
666
667
  "route.overviews.prompt": "Activer l'autocomplétion en utilisant [ctrl + espace].",
667
668
  "route.overviews.search.prompt": "Recherche par titre, contenu, clé d'analyse ou de détection.",
@@ -679,6 +680,7 @@
679
680
  "route.templates.default": "Défaut",
680
681
  "route.templates.detection": "Choisir une détection",
681
682
  "route.templates.global": "Global",
683
+ "route.templates.manager.delete.success": "Modèle supprimé.",
682
684
  "route.templates.manager.error.action": "Cliquez ici pour afficher les options de correction rapide",
683
685
  "route.templates.manager.error.message": "Détection non valide",
684
686
  "route.templates.manager.error.modal.description": "Les clés du modèle sont en lecture seule et ne seront pas utilisées. Voulez-vous supprimer le modèle ?",
@@ -702,6 +704,7 @@
702
704
  "route.views.manager": "Gérer vue",
703
705
  "route.views.manager.all": "Tous",
704
706
  "route.views.manager.default": "Vue par défaut",
707
+ "route.views.manager.delete.success": "Vue supprimée.",
705
708
  "route.views.manager.favourites": "Afficher uniquement les favoris",
706
709
  "route.views.manager.global": "Global",
707
710
  "route.views.manager.open": "Ouvrier la vue",