@cccsaurora/howler-ui 2.18.0-dev.841 → 2.19.0-cases.1040

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 (171) 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/App.d.ts +1 -0
  31. package/components/app/App.js +24 -6
  32. package/components/app/providers/FavouritesProvider.js +0 -4
  33. package/components/app/providers/ModalProvider.d.ts +1 -0
  34. package/components/app/providers/ModalProvider.js +5 -1
  35. package/components/app/providers/RecordProvider.d.ts +1 -1
  36. package/components/app/providers/RecordProvider.js +1 -1
  37. package/components/app/providers/SearchResponseProvider.d.ts +21 -0
  38. package/components/app/providers/SearchResponseProvider.js +81 -0
  39. package/components/app/providers/SearchResponseProvider.test.d.ts +1 -0
  40. package/components/app/providers/SearchResponseProvider.test.js +406 -0
  41. package/components/app/providers/ViewProvider.test.js +4 -1
  42. package/components/elements/addons/buttons/CustomButton.test.d.ts +1 -0
  43. package/components/elements/addons/buttons/CustomButton.test.js +106 -0
  44. package/components/elements/addons/layout/FlexOne.test.d.ts +1 -0
  45. package/components/elements/addons/layout/FlexOne.test.js +30 -0
  46. package/components/elements/addons/lists/TuiListBase.js +4 -0
  47. package/components/elements/addons/lists/TuiListElement.js +6 -3
  48. package/components/elements/addons/lists/index.d.ts +1 -0
  49. package/components/elements/addons/search/SearchPagination.d.ts +2 -1
  50. package/components/elements/addons/search/SearchPagination.js +4 -4
  51. package/components/elements/addons/search/SearchPagination.test.d.ts +1 -0
  52. package/components/elements/addons/search/SearchPagination.test.js +80 -0
  53. package/components/elements/addons/search/SearchTotal.test.d.ts +1 -0
  54. package/components/elements/addons/search/SearchTotal.test.js +47 -0
  55. package/components/elements/display/DocumentationButton.test.d.ts +1 -0
  56. package/components/elements/display/DocumentationButton.test.js +84 -0
  57. package/components/elements/display/DynamicTabs.test.d.ts +1 -0
  58. package/components/elements/display/DynamicTabs.test.js +93 -0
  59. package/components/elements/display/HowlerAvatar.js +2 -1
  60. package/components/elements/display/HowlerCard.test.d.ts +1 -0
  61. package/components/elements/display/HowlerCard.test.js +51 -0
  62. package/components/elements/display/Image.test.d.ts +1 -0
  63. package/components/elements/display/Image.test.js +79 -0
  64. package/components/elements/display/ItemManager.d.ts +2 -2
  65. package/components/elements/display/ItemManager.js +1 -1
  66. package/components/elements/display/Markdown.js +9 -2
  67. package/components/elements/display/QueryResultText.test.d.ts +1 -0
  68. package/components/elements/display/QueryResultText.test.js +48 -0
  69. package/components/elements/display/TextDivider.test.d.ts +1 -0
  70. package/components/elements/display/TextDivider.test.js +43 -0
  71. package/components/elements/display/TypingIndicator.test.d.ts +1 -0
  72. package/components/elements/display/TypingIndicator.test.js +33 -0
  73. package/components/elements/display/modals/ConfirmDeleteModal.d.ts +4 -0
  74. package/components/elements/display/modals/ConfirmDeleteModal.js +4 -2
  75. package/components/elements/hit/HitBanner.js +6 -3
  76. package/components/elements/hit/HitLinks.js +1 -1
  77. package/components/elements/hit/HitSummary.js +3 -2
  78. package/components/elements/hit/elements/AnalyticLink.js +1 -1
  79. package/components/elements/hit/elements/Assigned.d.ts +2 -0
  80. package/components/elements/hit/elements/Assigned.js +15 -13
  81. package/components/elements/record/RecordContextMenu.js +15 -3
  82. package/components/elements/record/RecordContextMenu.test.js +96 -0
  83. package/components/hooks/useMyApi.d.ts +1 -1
  84. package/components/hooks/useMyPreferences.js +2 -1
  85. package/components/routes/403.d.ts +3 -0
  86. package/components/routes/403.js +10 -0
  87. package/components/routes/403.test.d.ts +1 -0
  88. package/components/routes/403.test.js +27 -0
  89. package/components/routes/404.test.d.ts +1 -0
  90. package/components/routes/404.test.js +26 -0
  91. package/components/routes/ErrorBoundary.test.d.ts +1 -0
  92. package/components/routes/ErrorBoundary.test.js +43 -0
  93. package/components/routes/ErrorOccured.test.d.ts +1 -0
  94. package/components/routes/ErrorOccured.test.js +35 -0
  95. package/components/routes/Logout.test.d.ts +1 -0
  96. package/components/routes/Logout.test.js +59 -0
  97. package/components/routes/action/edit/ActionEditor.js +1 -1
  98. package/components/routes/action/useMyActionFunctions.js +30 -6
  99. package/components/routes/action/view/ActionDetails.js +15 -2
  100. package/components/routes/action/view/ActionSearch.js +32 -20
  101. package/components/routes/action/view/markdown/integrations.en.md.js +1 -1
  102. package/components/routes/action/view/markdown/integrations.fr.md.js +1 -1
  103. package/components/routes/analytics/AnalyticDetails.js +10 -6
  104. package/components/routes/analytics/AnalyticSearch.js +6 -6
  105. package/components/routes/cases/Cases.js +5 -6
  106. package/components/routes/cases/modals/ResolveModal.js +14 -13
  107. package/components/routes/cases/modals/ResolveModal.test.js +85 -25
  108. package/components/routes/dossiers/Dossiers.js +25 -19
  109. package/components/routes/help/ActionIntroductionDocumentation.js +1 -1
  110. package/components/routes/help/markdown/en/client.md.js +1 -1
  111. package/components/routes/help/markdown/en/retention.md.js +1 -1
  112. package/components/routes/help/markdown/fr/client.md.js +1 -1
  113. package/components/routes/help/markdown/fr/retention.md.js +1 -1
  114. package/components/routes/hits/search/shared/CustomSpan.js +7 -7
  115. package/components/routes/hits/search/shared/CustomSpan.test.d.ts +1 -0
  116. package/components/routes/hits/search/shared/CustomSpan.test.js +170 -0
  117. package/components/routes/home/AddNewCard.js +1 -1
  118. package/components/routes/home/AddNewCard.test.d.ts +1 -0
  119. package/components/routes/home/AddNewCard.test.js +86 -0
  120. package/components/routes/home/ViewCard.js +1 -1
  121. package/components/routes/home/ViewRefresh.test.d.ts +1 -0
  122. package/components/routes/home/ViewRefresh.test.js +79 -0
  123. package/components/routes/overviews/OverviewCard.test.d.ts +1 -0
  124. package/components/routes/overviews/OverviewCard.test.js +108 -0
  125. package/components/routes/overviews/OverviewViewer.js +16 -8
  126. package/components/routes/overviews/Overviews.js +25 -19
  127. package/components/routes/overviews/template/en.md.js +1 -1
  128. package/components/routes/overviews/template/fr.md.js +1 -1
  129. package/components/routes/settings/LocalSection.js +2 -1
  130. package/components/routes/settings/SettingsSection.test.d.ts +1 -0
  131. package/components/routes/settings/SettingsSection.test.js +19 -0
  132. package/components/routes/templates/TemplateCard.d.ts +3 -1
  133. package/components/routes/templates/TemplateCard.js +8 -4
  134. package/components/routes/templates/TemplateCard.test.d.ts +1 -0
  135. package/components/routes/templates/TemplateCard.test.js +115 -0
  136. package/components/routes/templates/TemplateViewer.js +62 -16
  137. package/components/routes/templates/Templates.js +35 -13
  138. package/components/routes/views/Views.js +21 -14
  139. package/locales/en/translation.json +25 -4
  140. package/locales/fr/translation.json +25 -4
  141. package/models/entities/generated/ApiType.d.ts +1 -1
  142. package/models/entities/generated/Hit.d.ts +1 -0
  143. package/models/entities/generated/Howler.d.ts +1 -0
  144. package/models/entities/generated/UserUser.d.ts +2 -0
  145. package/package.json +143 -148
  146. package/rest/FetchClient.test.d.ts +1 -0
  147. package/rest/FetchClient.test.js +81 -0
  148. package/setupTests.js +5 -0
  149. package/utils/Throttler.test.d.ts +1 -0
  150. package/utils/Throttler.test.js +135 -0
  151. package/utils/actionUtils.test.d.ts +1 -0
  152. package/utils/actionUtils.test.js +140 -0
  153. package/utils/constants.d.ts +1 -0
  154. package/utils/constants.js +1 -0
  155. package/utils/hitFunctions.test.d.ts +1 -0
  156. package/utils/hitFunctions.test.js +52 -0
  157. package/utils/localStorage.test.d.ts +1 -0
  158. package/utils/localStorage.test.js +96 -0
  159. package/utils/menuUtils.js +1 -1
  160. package/utils/menuUtils.test.d.ts +1 -0
  161. package/utils/menuUtils.test.js +198 -0
  162. package/utils/sessionStorage.test.d.ts +1 -0
  163. package/utils/sessionStorage.test.js +81 -0
  164. package/utils/stringUtils.test.d.ts +1 -0
  165. package/utils/stringUtils.test.js +159 -0
  166. package/utils/utils.test.d.ts +1 -0
  167. package/utils/utils.test.js +292 -0
  168. package/utils/viewUtils.test.d.ts +1 -0
  169. package/utils/viewUtils.test.js +45 -0
  170. package/utils/xsrf.test.d.ts +1 -0
  171. package/utils/xsrf.test.js +41 -0
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { render, screen } from '@testing-library/react';
4
+ import i18n from '@cccsaurora/howler-ui/i18n';
5
+ import { I18nextProvider } from 'react-i18next';
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import Logout from './Logout';
8
+ const mockHideMenus = vi.fn();
9
+ const mockClear = vi.fn();
10
+ vi.mock('commons/components/app/hooks', () => ({
11
+ useAppBanner: () => _jsx("div", { "data-testid": "app-banner", children: "Banner" }),
12
+ useAppLayout: () => ({
13
+ hideMenus: mockHideMenus
14
+ })
15
+ }));
16
+ vi.mock('components/hooks/useMyLocalStorage', () => ({
17
+ default: () => ({
18
+ clear: mockClear
19
+ })
20
+ }));
21
+ vi.mock('commons/components/pages/PageCardCentered', () => ({
22
+ default: ({ children }) => _jsx("div", { children: children })
23
+ }));
24
+ const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
25
+ describe('Logout', () => {
26
+ const originalLocation = window.location;
27
+ beforeEach(() => {
28
+ vi.useFakeTimers();
29
+ // Mock window.location.replace
30
+ Object.defineProperty(window, 'location', {
31
+ value: { ...originalLocation, replace: vi.fn() },
32
+ writable: true
33
+ });
34
+ });
35
+ afterEach(() => {
36
+ vi.useRealTimers();
37
+ Object.defineProperty(window, 'location', { value: originalLocation, writable: true });
38
+ vi.clearAllMocks();
39
+ });
40
+ it('should render the logout message', () => {
41
+ render(_jsx(Logout, {}), { wrapper: Wrapper });
42
+ expect(screen.getByText(/Logging out current user/)).toBeInTheDocument();
43
+ });
44
+ it('should render the app banner', () => {
45
+ render(_jsx(Logout, {}), { wrapper: Wrapper });
46
+ expect(screen.getByText('Banner')).toBeInTheDocument();
47
+ });
48
+ it('should hide menus on mount', () => {
49
+ render(_jsx(Logout, {}), { wrapper: Wrapper });
50
+ expect(mockHideMenus).toHaveBeenCalled();
51
+ });
52
+ it('should clear localStorage and redirect after 2 seconds', () => {
53
+ render(_jsx(Logout, {}), { wrapper: Wrapper });
54
+ expect(mockClear).not.toHaveBeenCalled();
55
+ vi.advanceTimersByTime(2000);
56
+ expect(mockClear).toHaveBeenCalled();
57
+ expect(window.location.replace).toHaveBeenCalledWith('/');
58
+ });
59
+ });
@@ -54,7 +54,7 @@ const ActionEditor = () => {
54
54
  }, [triggers]);
55
55
  useEffect(() => {
56
56
  dispatchApi(api.action.operations.get())
57
- .then(_operations => _operations.filter(a => difference(a.roles, user.roles).length < 1))
57
+ .then(_operations => _operations.filter(a => difference(a.roles, user.roles).length < a.roles.length))
58
58
  .then(setOperations);
59
59
  if (responseQuery) {
60
60
  onSearch(responseQuery);
@@ -123,12 +123,33 @@ const useMyActionFunctions = () => {
123
123
  hideIconVariant: true,
124
124
  style: { padding: 0, display: 'flex', flexDirection: 'column', alignItems: 'stretch' }
125
125
  });
126
- setReport(await dispatchApi(api.action.execute.post({
126
+ const result = await dispatchApi(api.action.execute.post({
127
127
  request_id: reqId,
128
128
  query,
129
129
  action_id: actionId
130
- }), { throwError: false, showError: true }));
131
- showSuccessMessage(_jsx(Trans, { i18nKey: "actions.succeeded", values: { action: (await action).items[0]?.name || t('unknown') } }));
130
+ }), { throwError: false, showError: false });
131
+ setReport(result);
132
+ const actionName = (await action).items[0]?.name || t('unknown');
133
+ if (!result) {
134
+ showErrorMessage(_jsx(Trans, { i18nKey: "actions.error", values: { action: actionName, message: t('unknown') } }));
135
+ return;
136
+ }
137
+ const allItems = Object.values(result).flat();
138
+ const errors = allItems.filter(item => item.outcome === 'error');
139
+ const skipped = allItems.filter(item => item.outcome === 'skipped');
140
+ const succeeded = allItems.filter(item => item.outcome === 'success');
141
+ if (errors.length > 0) {
142
+ showErrorMessage(_jsx(Trans, { i18nKey: "actions.error", values: { action: actionName, messages: errors.map(error => error.message).join(', ') } }));
143
+ }
144
+ if (skipped.length > 0) {
145
+ showInfoMessage(_jsx(Trans, { i18nKey: "actions.skipped", values: {
146
+ action: actionName,
147
+ messages: skipped.map(skippedAction => skippedAction.message).join(', ')
148
+ } }));
149
+ }
150
+ if (succeeded.length > 0) {
151
+ showSuccessMessage(_jsx(Trans, { i18nKey: "actions.succeeded", values: { action: actionName } }));
152
+ }
132
153
  }
133
154
  finally {
134
155
  if (key) {
@@ -138,12 +159,15 @@ const useMyActionFunctions = () => {
138
159
  setRequestId(null);
139
160
  setProgress([0, 0]);
140
161
  }
141
- }, [closeSnackbar, dispatchApi, showInfoMessage, showSuccessMessage, t]),
162
+ }, [closeSnackbar, dispatchApi, showInfoMessage, showSuccessMessage, showErrorMessage, t]),
142
163
  deleteAction: useCallback(async (actionId) => {
143
164
  setLoading(true);
144
165
  try {
145
- await dispatchApi(api.action.del(actionId));
146
- if (location.pathname.endsWith(actionId)) {
166
+ const detailedActionView = location.pathname.endsWith(actionId);
167
+ await dispatchApi(api.action.del(actionId, detailedActionView ? 'wait_for' : undefined), {
168
+ throwError: true
169
+ });
170
+ if (detailedActionView) {
147
171
  navigate('/action');
148
172
  }
149
173
  }
@@ -4,13 +4,15 @@ import { Button, Checkbox, FormControlLabel, FormGroup, IconButton, LinearProgre
4
4
  import api from '@cccsaurora/howler-ui/api';
5
5
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
6
6
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
7
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
7
8
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
8
9
  import Phrase from '@cccsaurora/howler-ui/components/elements/addons/search/phrase/Phrase';
9
10
  import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
10
11
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
12
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
11
13
  import OperationEntry from '@cccsaurora/howler-ui/components/routes/action/shared/OperationEntry';
12
14
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
13
- import { useCallback, useEffect, useState } from 'react';
15
+ import { useCallback, useContext, useEffect, useState } from 'react';
14
16
  import { useTranslation } from 'react-i18next';
15
17
  import { usePluginStore } from 'react-pluggable';
16
18
  import { Link, useParams } from 'react-router-dom';
@@ -26,6 +28,8 @@ const ActionDetails = () => {
26
28
  const { response, onSearch, loading, setLoading, executeAction, deleteAction, progress, report } = useMyActionFunctions();
27
29
  const [operations, setOperations] = useState([]);
28
30
  const [action, setAction] = useState();
31
+ const { withConfirmDeleteModal } = useContext(ModalContext);
32
+ const { showSuccessMessage } = useMySnackbar();
29
33
  const onTriggerChange = useCallback(async (e) => {
30
34
  let newTriggers = action.triggers ?? [];
31
35
  if (e.target.checked && !newTriggers.includes(e.target.name)) {
@@ -45,6 +49,10 @@ const ActionDetails = () => {
45
49
  setLoading(false);
46
50
  }
47
51
  }, [action, dispatchApi, setLoading]);
52
+ const onDelete = useCallback(() => withConfirmDeleteModal(async () => {
53
+ await deleteAction(action?.action_id);
54
+ showSuccessMessage(t('route.actions.manager.delete.success'));
55
+ }), [withConfirmDeleteModal, deleteAction, action?.action_id, showSuccessMessage, t]);
48
56
  useEffect(() => {
49
57
  setLoading(true);
50
58
  Promise.all([
@@ -58,7 +66,12 @@ const ActionDetails = () => {
58
66
  }
59
67
  // eslint-disable-next-line react-hooks/exhaustive-deps
60
68
  }, [action?.query]);
61
- return (_jsx(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", justifyContent: "space-between", children: [_jsx(Typography, { variant: "h5", children: action?.name }), action?.owner_id && _jsx(HowlerAvatar, { sx: { width: 32, height: 32 }, userId: action.owner_id })] }), _jsx(Phrase, { fullWidth: true, value: action?.query, disabled: true, size: "small", onChange: () => { }, startAdornment: _jsx(IconButton, { onClick: () => onSearch(action?.query), children: _jsx(Search, { fontSize: "small" }) }) }), _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [response && _jsx(QueryResultText, { count: response.total, query: action?.query }), _jsx(FlexOne, {}), (action?.owner_id === user.username || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Delete, {}), size: "small", variant: "outlined", color: "error", onClick: () => deleteAction(action?.action_id), children: t('delete') })), _jsx(Button, { startIcon: _jsx(PlayCircleOutline, {}), size: "small", variant: "outlined", color: "success", onClick: () => executeAction(action?.action_id), children: t('route.actions.execute') }), (action?.owner_id === user.username || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Edit, {}), size: "small", variant: "outlined", component: Link, to: `/action/${params.id}/edit`, children: t('route.actions.edit') }))] }), user.roles.includes('automation_advanced') && (_jsx(FormGroup, { children: _jsx(Stack, { direction: "row", spacing: 1, children: action?.operations
69
+ const editRoles = user.roles.includes('automation_basic') || user.roles.includes('automation_advanced');
70
+ const execRoles = editRoles ||
71
+ user.roles.includes('admin') ||
72
+ user.roles.includes('actionrunner_basic') ||
73
+ user.roles.includes('actionrunner_advanced');
74
+ return (_jsx(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", justifyContent: "space-between", children: [_jsx(Typography, { variant: "h5", children: action?.name }), action?.owner_id && _jsx(HowlerAvatar, { sx: { width: 32, height: 32 }, userId: action.owner_id })] }), _jsx(Phrase, { fullWidth: true, value: action?.query, disabled: true, size: "small", onChange: () => { }, startAdornment: _jsx(IconButton, { onClick: () => onSearch(action?.query), children: _jsx(Search, { fontSize: "small" }) }) }), _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [response && _jsx(QueryResultText, { count: response.total, query: action?.query }), _jsx(FlexOne, {}), ((action?.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Delete, {}), size: "small", variant: "outlined", color: "error", onClick: onDelete, children: t('button.delete') })), execRoles && (_jsx(Button, { startIcon: _jsx(PlayCircleOutline, {}), size: "small", variant: "outlined", color: "success", onClick: () => executeAction(action?.action_id), children: t('route.actions.execute') })), ((action?.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Edit, {}), size: "small", variant: "outlined", component: Link, to: `/action/${params.id}/edit`, children: t('route.actions.edit') }))] }), user.roles.includes('automation_advanced') && (_jsx(FormGroup, { children: _jsx(Stack, { direction: "row", spacing: 1, children: action?.operations
62
75
  ?.map(a => (operations ?? []).find(_action => _action.id === a.operation_id)?.triggers ?? [])
63
76
  .reduce((acc, triggers) => acc.filter(_t => triggers.includes(_t)))
64
77
  .map(trigger => (_jsx(FormControlLabel, { control: _jsx(Checkbox, { name: trigger, onChange: onTriggerChange, checked: action?.triggers?.includes(trigger) ?? false }), label: t(`route.actions.trigger.${trigger}`) }, trigger))) }) })), loading &&
@@ -3,13 +3,15 @@ import { Delete, Engineering, Terminal } from '@mui/icons-material';
3
3
  import { Autocomplete, Card, CardContent, CardHeader, Chip, Grid, IconButton, Stack, TextField, Tooltip, 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 { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
7
+ import SearchResponseProvider, { SearchResponseContext } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
6
8
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
7
9
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
8
10
  import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
9
11
  import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
10
12
  import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
11
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
12
13
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
14
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
13
15
  import { useCallback, useContext, useEffect, useState } from 'react';
14
16
  import { Trans, useTranslation } from 'react-i18next';
15
17
  import { useNavigate, useSearchParams } from 'react-router-dom';
@@ -20,16 +22,17 @@ const ActionSearch = () => {
20
22
  const navigate = useNavigate();
21
23
  const { t } = useTranslation();
22
24
  const { user } = useAppUser();
23
- const { dispatchApi } = useMyApi();
24
25
  const { load } = useContext(TuiListMethodContext);
26
+ const { withConfirmDeleteModal } = useContext(ModalContext);
27
+ const { showSuccessMessage } = useMySnackbar();
25
28
  const [searchParams, setSearchParams] = useSearchParams();
26
29
  const { deleteAction } = useMyActionFunctions();
27
30
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
31
+ const { response, request, remove, getSearchRequestData } = useContext(SearchResponseContext);
28
32
  const [searching, setSearching] = useState(false);
29
33
  const [hasError, setHasError] = useState(false);
30
34
  const [phrase, setPhrase] = useState(searchParams.get('phrase') || '');
31
35
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
32
- const [response, setResponse] = useState(null);
33
36
  const [searchModifiers, setSearchModifiers] = useState([]);
34
37
  // Search Handler.
35
38
  const onSearch = useCallback(async () => {
@@ -48,13 +51,11 @@ const ActionSearch = () => {
48
51
  if (searchModifiers.length > 0) {
49
52
  query = `(${query}) AND (triggers:(${searchModifiers.join(' OR ')}))`;
50
53
  }
51
- const _response = await dispatchApi(api.search.action.post({
54
+ await request(api.search.action.post, {
52
55
  query,
53
56
  rows: pageCount,
54
57
  offset
55
- }));
56
- setResponse(_response);
57
- load(_response.items.map(u => ({ id: u.action_id, item: u })));
58
+ });
58
59
  }
59
60
  catch (e) {
60
61
  setHasError(true);
@@ -62,14 +63,29 @@ const ActionSearch = () => {
62
63
  finally {
63
64
  setSearching(false);
64
65
  }
65
- }, [dispatchApi, load, offset, pageCount, phrase, searchModifiers, searchParams, setSearchParams]);
66
+ }, [phrase, setSearchParams, searchParams, searchModifiers, request, pageCount, offset]);
67
+ useEffect(() => {
68
+ if (response) {
69
+ load(response.items.map((item) => ({ id: item.action_id, item: item })));
70
+ }
71
+ }, [response, load]);
66
72
  const onPageChange = useCallback((_offset) => {
67
73
  if (_offset !== offset) {
68
- searchParams.set('offset', _offset.toString());
74
+ const modifiedRequest = getSearchRequestData({ offset: _offset });
75
+ searchParams.set('offset', modifiedRequest.offset.toString());
69
76
  setSearchParams(searchParams, { replace: true });
70
- setOffset(_offset);
77
+ setOffset(modifiedRequest.offset);
71
78
  }
72
- }, [offset, searchParams, setSearchParams]);
79
+ }, [offset, searchParams, setSearchParams, getSearchRequestData]);
80
+ const onDelete = useCallback((e, actionId) => {
81
+ e.preventDefault();
82
+ e.stopPropagation();
83
+ withConfirmDeleteModal(async () => {
84
+ await deleteAction(actionId);
85
+ remove(actionId);
86
+ showSuccessMessage(t('route.actions.manager.delete.success'));
87
+ });
88
+ }, [deleteAction, remove, withConfirmDeleteModal, showSuccessMessage, t]);
73
89
  // Effect to initialize list of users.
74
90
  useEffect(() => {
75
91
  onSearch();
@@ -97,6 +113,7 @@ const ActionSearch = () => {
97
113
  onSearch();
98
114
  // eslint-disable-next-line react-hooks/exhaustive-deps
99
115
  }, [searchModifiers]);
116
+ const editRoles = user.roles.includes('automation_basic') || user.roles.includes('automation_advanced');
100
117
  // Search result list item renderer.
101
118
  const renderer = useCallback(({ item }, classRenderer) => {
102
119
  return (_jsxs(Card, { onClick: () => navigate(`/action/${item.item.action_id}`), variant: "outlined", className: classRenderer(), sx: {
@@ -104,16 +121,11 @@ const ActionSearch = () => {
104
121
  transitionProperty: 'border-color',
105
122
  cursor: 'pointer',
106
123
  mt: 1
107
- }, children: [_jsx(CardHeader, { title: _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h5", children: item.item.name }), item.item.triggers.length > 0 && (_jsx(Tooltip, { title: _jsx(Trans, { i18nKey: "route.actions.trigger.description", values: { triggers: item.item.triggers.join(', ') }, components: { bold: _jsx("strong", {}) } }), children: _jsx(Engineering, {}) })), _jsx(FlexOne, {}), (item.item.owner_id === user.username || user.roles?.includes('admin')) && (_jsx(IconButton, { size: "small", onClick: async (e) => {
108
- e.preventDefault();
109
- e.stopPropagation();
110
- await deleteAction(item.item.action_id);
111
- onSearch();
112
- }, children: _jsx(Delete, {}) })), _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important' }, userId: item.item.owner_id })] }), subheader: item.item.query }), _jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsx(Grid, { container: true, spacing: 1, children: item.item.operations.map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { label: t(`operations.${d.operation_id}`) }) }, d.operation_id))) }) })] }, item.item.name));
113
- }, [deleteAction, navigate, onSearch, t, user.roles, user.username]);
114
- return (_jsx(ItemManager, { onSearch: onSearch, onCreate: () => navigate('/action/execute'), onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.actions.search.prompt') }), searchFilters: _jsx(Autocomplete, { multiple: true, size: "small", value: searchModifiers, onChange: (__, values) => setSearchModifiers(values), getOptionLabel: trigger => t(`route.actions.trigger.${trigger}`), options: VALID_ACTION_TRIGGERS, renderInput: params => (_jsx(TextField, { ...params, sx: { maxWidth: '500px' }, label: t('route.actions.trigger') })) }), renderer: renderer, response: response, createPrompt: "route.actions.create", searchPrompt: "route.actions.search", createIcon: _jsx(Terminal, { sx: { mr: 1 } }) }));
124
+ }, children: [_jsx(CardHeader, { title: _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h5", children: item.item.name }), item.item.triggers.length > 0 && (_jsx(Tooltip, { title: _jsx(Trans, { i18nKey: "route.actions.trigger.description", values: { triggers: item.item.triggers.join(', ') }, components: { bold: _jsx("strong", {}) } }), children: _jsx(Engineering, {}) })), _jsx(FlexOne, {}), ((item.item.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(IconButton, { size: "small", onClick: e => onDelete(e, item.item.action_id), children: _jsx(Delete, {}) })), _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important' }, userId: item.item.owner_id })] }), subheader: item.item.query }), _jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsx(Grid, { container: true, spacing: 1, children: item.item.operations.map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { label: t(`operations.${d.operation_id}`) }) }, d.operation_id))) }) })] }, item.item.name));
125
+ }, [editRoles, navigate, onDelete, t, user.roles, user.username]);
126
+ return (_jsx(ItemManager, { onSearch: onSearch, onCreate: editRoles ? () => navigate('/action/execute') : undefined, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.actions.search.prompt') }), searchFilters: _jsx(Autocomplete, { multiple: true, size: "small", value: searchModifiers, onChange: (__, values) => setSearchModifiers(values), getOptionLabel: trigger => t(`route.actions.trigger.${trigger}`), options: VALID_ACTION_TRIGGERS, renderInput: params => (_jsx(TextField, { ...params, sx: { maxWidth: '500px' }, label: t('route.actions.trigger') })) }), renderer: renderer, response: response, createPrompt: "route.actions.create", searchPrompt: "route.actions.search", createIcon: _jsx(Terminal, { sx: { mr: 1 } }) }));
115
127
  };
116
128
  const ActionSearchProvider = () => {
117
- return (_jsx(TuiListProvider, { children: _jsx(ActionSearch, {}) }));
129
+ return (_jsx(TuiListProvider, { children: _jsx(SearchResponseProvider, { idField: "action_id", children: _jsx(ActionSearch, {}) }) }));
118
130
  };
119
131
  export default ActionSearchProvider;
@@ -1 +1 @@
1
- export default "# Integrations and Plugins\n\n> **Note:** This page is fallback documentation. In `Integrations.tsx`, when plugins provide integration views, those plugin tabs/content are rendered and this markdown is replaced.\n\nHowler plugins let you extend both UI behavior and rendering paths without modifying core screens directly. Plugins are installed through the plugin store and then invoked across the app through `executeFunction(...)` hooks.\n\n## How the plugin system works\n\n- `HowlerPlugin` is the base class that defines extension points.\n- `howlerPluginStore` keeps global plugin state (installed plugins, lead formats, pivot formats, operations, routes, menus, sitemap entries).\n- On activation, each plugin can register named functions in the runtime plugin store.\n- The app calls those functions via `pluginStore.executeFunction(...)` in specific locations.\n\nIn practice, this means plugins can contribute features incrementally rather than replacing full pages.\n\n## What plugins can add\n\nFrom `HowlerPlugin.ts` and store usage, plugins can provide:\n\n- **Lead formats** (`addLead`) with:\n\t- a lead editor form (`lead.<format>.form`)\n\t- a lead renderer (`lead.<format>`)\n- **Pivot formats** (`addPivot`) with:\n\t- a pivot form (`pivot.<format>.form`)\n\t- a pivot link renderer (`pivot.<format>`)\n- **Custom action operations** (`addOperation`) with:\n\t- operation editor UI (`operation.<id>`)\n\t- operation help docs (`operation.<id>.documentation`)\n- **Menu entries**:\n\t- user menu items\n\t- admin menu items\n\t- main menu insertions/dividers\n- **Routing/navigation**:\n\t- routes\n\t- sitemap entries and breadcrumbs behavior\n- **Global extension hooks**:\n\t- `provider()` wrapper for app-wide context\n\t- `setup()` startup logic\n\t- `localization(...)` translation bundles\n\t- `helpers()` custom handlebars helpers\n\t- `typography(...)` and `chip(...)` custom UI rendering\n\t- `actions(...)` hit actions\n\t- `status(...)` hit banner/status widgets\n\t- `support()`, `help()`, and section-specific `settings(...)`\n\t- `documentation(md)` markdown post-processing\n\t- `on(event, hit)` event callback\n\n## Where hooks are executed\n\n`executeFunction(...)` is used throughout the app to render plugin output at runtime, for example:\n\n- lead rendering and lead form editors\n- pivot rendering and pivot form editors\n- custom operation editors and docs\n- plugin actions in hit views/context menus\n- hit status/banner components\n- typography/chip wrappers\n- plugin providers and startup setup\n- settings sections (`admin`, `local`, `profile`, `security`)\n- help/support panels\n- markdown documentation transforms\n\n## Clue plugin example\n\nThe Clue plugin (`ui/src/plugins/clue/index.tsx`) demonstrates a typical plugin:\n\n- registers localization bundles in English/French\n- provides a plugin provider + setup hook\n- adds a custom lead format (`clue`) with:\n\t- a lead form component\n\t- a renderer that parses lead metadata and renders a `Fetcher`\n- adds a custom pivot format (`clue`) with form + renderer\n- provides custom handlebars helpers\n- overrides plugin typography/chip renderers\n\nThis is the main pattern to follow when adding a new integration.\n"
1
+ export default "# Integrations and Plugins\n\n> **Note:** This page is fallback documentation. In `Integrations.tsx`, when plugins provide integration views, those plugin tabs/content are rendered and this markdown is replaced.\n\nHowler plugins let you extend both UI behavior and rendering paths without modifying core screens directly. Plugins are installed through the plugin store and then invoked across the app through `executeFunction(...)` hooks.\n\n## How the plugin system works\n\n- `HowlerPlugin` is the base class that defines extension points.\n- `howlerPluginStore` keeps global plugin state (installed plugins, lead formats, pivot formats, operations, routes, menus, sitemap entries).\n- On activation, each plugin can register named functions in the runtime plugin store.\n- The app calls those functions via `pluginStore.executeFunction(...)` in specific locations.\n\nIn practice, this means plugins can contribute features incrementally rather than replacing full pages.\n\n## What plugins can add\n\nFrom `HowlerPlugin.ts` and store usage, plugins can provide:\n\n- **Lead formats** (`addLead`) with:\n - a lead editor form (`lead.<format>.form`)\n - a lead renderer (`lead.<format>`)\n- **Pivot formats** (`addPivot`) with:\n - a pivot form (`pivot.<format>.form`)\n - a pivot link renderer (`pivot.<format>`)\n- **Custom action operations** (`addOperation`) with:\n - operation editor UI (`operation.<id>`)\n - operation help docs (`operation.<id>.documentation`)\n- **Menu entries**:\n - user menu items\n - admin menu items\n - main menu insertions/dividers\n- **Routing/navigation**:\n - routes\n - sitemap entries and breadcrumbs behavior\n- **Global extension hooks**:\n - `provider()` wrapper for app-wide context\n - `setup()` startup logic\n - `localization(...)` translation bundles\n - `helpers()` custom handlebars helpers\n - `typography(...)` and `chip(...)` custom UI rendering\n - `actions(...)` hit actions\n - `status(...)` hit banner/status widgets\n - `support()`, `help()`, and section-specific `settings(...)`\n - `documentation(md)` markdown post-processing\n - `on(event, hit)` event callback\n\n## Where hooks are executed\n\n`executeFunction(...)` is used throughout the app to render plugin output at runtime, for example:\n\n- lead rendering and lead form editors\n- pivot rendering and pivot form editors\n- custom operation editors and docs\n- plugin actions in hit views/context menus\n- hit status/banner components\n- typography/chip wrappers\n- plugin providers and startup setup\n- settings sections (`admin`, `local`, `profile`, `security`)\n- help/support panels\n- markdown documentation transforms\n\n## Clue plugin example\n\nThe Clue plugin (`ui/src/plugins/clue/index.tsx`) demonstrates a typical plugin:\n\n- registers localization bundles in English/French\n- provides a plugin provider + setup hook\n- adds a custom lead format (`clue`) with:\n - a lead form component\n - a renderer that parses lead metadata and renders a `Fetcher`\n- adds a custom pivot format (`clue`) with form + renderer\n- provides custom handlebars helpers\n- overrides plugin typography/chip renderers\n\nThis is the main pattern to follow when adding a new integration.\n"
@@ -1 +1 @@
1
- export default "# Int\u00e9grations et plugins\n\n> **Remarque :** cette page est une documentation de secours. Dans `Integrations.tsx`, quand des plugins fournissent des vues d\u2019int\u00e9gration, ces onglets/contenus plugins sont affich\u00e9s et ce markdown est remplac\u00e9.\n\nLes plugins Howler permettent d\u2019\u00e9tendre le comportement de l\u2019interface et le rendu sans modifier directement les \u00e9crans principaux. Les plugins sont install\u00e9s via le magasin de plugins, puis appel\u00e9s dans l\u2019application \u00e0 l\u2019aide des points d\u2019extension `executeFunction(...)`.\n\n## Fonctionnement du syst\u00e8me de plugins\n\n- `HowlerPlugin` est la classe de base qui d\u00e9finit les points d\u2019extension.\n- `howlerPluginStore` conserve l\u2019\u00e9tat global (plugins install\u00e9s, formats de lead, formats de pivot, op\u00e9rations, routes, menus, sitemap).\n- \u00c0 l\u2019activation, un plugin enregistre des fonctions nomm\u00e9es dans le magasin de plugins.\n- L\u2019application ex\u00e9cute ensuite ces fonctions via `pluginStore.executeFunction(...)` \u00e0 des endroits pr\u00e9cis.\n\nCe m\u00e9canisme permet d\u2019ajouter des capacit\u00e9s de fa\u00e7on incr\u00e9mentale, sans remplacer des pages compl\u00e8tes.\n\n## Ce qu\u2019un plugin peut ajouter\n\nD\u2019apr\u00e8s `HowlerPlugin.ts` et les usages du store, un plugin peut fournir :\n\n- **Formats de lead** (`addLead`) avec :\n\t- un formulaire d\u2019\u00e9dition (`lead.<format>.form`)\n\t- un rendu (`lead.<format>`)\n- **Formats de pivot** (`addPivot`) avec :\n\t- un formulaire (`pivot.<format>.form`)\n\t- un rendu de lien pivot (`pivot.<format>`)\n- **Op\u00e9rations d\u2019action personnalis\u00e9es** (`addOperation`) avec :\n\t- l\u2019UI d\u2019\u00e9dition de l\u2019op\u00e9ration (`operation.<id>`)\n\t- la documentation de l\u2019op\u00e9ration (`operation.<id>.documentation`)\n- **Entr\u00e9es de menu** :\n\t- menu utilisateur\n\t- menu administrateur\n\t- insertions/s\u00e9parateurs dans le menu principal\n- **Routage/navigation** :\n\t- routes\n\t- entr\u00e9es de sitemap et logique de fil d\u2019Ariane\n- **Points d\u2019extension globaux** :\n\t- `provider()` pour injecter un contexte global\n\t- `setup()` au d\u00e9marrage\n\t- `localization(...)` pour les traductions\n\t- `helpers()` pour les helpers handlebars\n\t- `typography(...)` et `chip(...)` pour le rendu UI\n\t- `actions(...)` pour les actions sur les hits\n\t- `status(...)` pour la banni\u00e8re/statut d\u2019un hit\n\t- `support()`, `help()` et `settings(...)` par section\n\t- `documentation(md)` pour post-traiter du markdown\n\t- `on(event, hit)` pour les \u00e9v\u00e9nements\n\n## O\u00f9 les hooks sont ex\u00e9cut\u00e9s\n\n`executeFunction(...)` est utilis\u00e9 dans plusieurs parties de l\u2019UI, notamment pour :\n\n- le rendu des leads et leurs formulaires\n- le rendu des pivots et leurs formulaires\n- les \u00e9diteurs d\u2019op\u00e9rations et leur documentation\n- les actions plugin dans les vues/context menus de hit\n- les composants de statut/banni\u00e8re des hits\n- les composants typographie/chip\n- les providers plugins et la logique `setup`\n- les sections de param\u00e8tres (`admin`, `local`, `profile`, `security`)\n- les vues d\u2019aide/support\n- la transformation de markdown de documentation\n\n## Exemple : plugin Clue\n\nLe plugin Clue (`ui/src/plugins/clue/index.tsx`) montre un exemple concret :\n\n- enregistre des bundles de traduction EN/FR\n- expose un provider et un hook de setup\n- ajoute un format de lead `clue` avec :\n\t- un composant de formulaire\n\t- un renderer qui lit les m\u00e9tadonn\u00e9es du lead et affiche un `Fetcher`\n- ajoute un format de pivot `clue` (formulaire + rendu)\n- fournit des helpers handlebars personnalis\u00e9s\n- fournit des rendus personnalis\u00e9s pour `typography` et `chip`\n\nC\u2019est le mod\u00e8le recommand\u00e9 pour d\u00e9velopper de nouvelles int\u00e9grations.\n"
1
+ export default "# Int\u00e9grations et plugins\n\n> **Remarque :** cette page est une documentation de secours. Dans `Integrations.tsx`, quand des plugins fournissent des vues d\u2019int\u00e9gration, ces onglets/contenus plugins sont affich\u00e9s et ce markdown est remplac\u00e9.\n\nLes plugins Howler permettent d\u2019\u00e9tendre le comportement de l\u2019interface et le rendu sans modifier directement les \u00e9crans principaux. Les plugins sont install\u00e9s via le magasin de plugins, puis appel\u00e9s dans l\u2019application \u00e0 l\u2019aide des points d\u2019extension `executeFunction(...)`.\n\n## Fonctionnement du syst\u00e8me de plugins\n\n- `HowlerPlugin` est la classe de base qui d\u00e9finit les points d\u2019extension.\n- `howlerPluginStore` conserve l\u2019\u00e9tat global (plugins install\u00e9s, formats de lead, formats de pivot, op\u00e9rations, routes, menus, sitemap).\n- \u00c0 l\u2019activation, un plugin enregistre des fonctions nomm\u00e9es dans le magasin de plugins.\n- L\u2019application ex\u00e9cute ensuite ces fonctions via `pluginStore.executeFunction(...)` \u00e0 des endroits pr\u00e9cis.\n\nCe m\u00e9canisme permet d\u2019ajouter des capacit\u00e9s de fa\u00e7on incr\u00e9mentale, sans remplacer des pages compl\u00e8tes.\n\n## Ce qu\u2019un plugin peut ajouter\n\nD\u2019apr\u00e8s `HowlerPlugin.ts` et les usages du store, un plugin peut fournir :\n\n- **Formats de lead** (`addLead`) avec :\n - un formulaire d\u2019\u00e9dition (`lead.<format>.form`)\n - un rendu (`lead.<format>`)\n- **Formats de pivot** (`addPivot`) avec :\n - un formulaire (`pivot.<format>.form`)\n - un rendu de lien pivot (`pivot.<format>`)\n- **Op\u00e9rations d\u2019action personnalis\u00e9es** (`addOperation`) avec :\n - l\u2019UI d\u2019\u00e9dition de l\u2019op\u00e9ration (`operation.<id>`)\n - la documentation de l\u2019op\u00e9ration (`operation.<id>.documentation`)\n- **Entr\u00e9es de menu** :\n - menu utilisateur\n - menu administrateur\n - insertions/s\u00e9parateurs dans le menu principal\n- **Routage/navigation** :\n - routes\n - entr\u00e9es de sitemap et logique de fil d\u2019Ariane\n- **Points d\u2019extension globaux** :\n - `provider()` pour injecter un contexte global\n - `setup()` au d\u00e9marrage\n - `localization(...)` pour les traductions\n - `helpers()` pour les helpers handlebars\n - `typography(...)` et `chip(...)` pour le rendu UI\n - `actions(...)` pour les actions sur les hits\n - `status(...)` pour la banni\u00e8re/statut d\u2019un hit\n - `support()`, `help()` et `settings(...)` par section\n - `documentation(md)` pour post-traiter du markdown\n - `on(event, hit)` pour les \u00e9v\u00e9nements\n\n## O\u00f9 les hooks sont ex\u00e9cut\u00e9s\n\n`executeFunction(...)` est utilis\u00e9 dans plusieurs parties de l\u2019UI, notamment pour :\n\n- le rendu des leads et leurs formulaires\n- le rendu des pivots et leurs formulaires\n- les \u00e9diteurs d\u2019op\u00e9rations et leur documentation\n- les actions plugin dans les vues/context menus de hit\n- les composants de statut/banni\u00e8re des hits\n- les composants typographie/chip\n- les providers plugins et la logique `setup`\n- les sections de param\u00e8tres (`admin`, `local`, `profile`, `security`)\n- les vues d\u2019aide/support\n- la transformation de markdown de documentation\n\n## Exemple : plugin Clue\n\nLe plugin Clue (`ui/src/plugins/clue/index.tsx`) montre un exemple concret :\n\n- enregistre des bundles de traduction EN/FR\n- expose un provider et un hook de setup\n- ajoute un format de lead `clue` avec :\n - un composant de formulaire\n - un renderer qui lit les m\u00e9tadonn\u00e9es du lead et affiche un `Fetcher`\n- ajoute un format de pivot `clue` (formulaire + rendu)\n- fournit des helpers handlebars personnalis\u00e9s\n- fournit des rendus personnalis\u00e9s pour `typography` et `chip`\n\nC\u2019est le mod\u00e8le recommand\u00e9 pour d\u00e9velopper de nouvelles int\u00e9grations.\n"
@@ -5,6 +5,7 @@ import api from '@cccsaurora/howler-ui/api';
5
5
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
6
6
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
7
7
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
8
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
8
9
  import { UserListContext } from '@cccsaurora/howler-ui/components/app/providers/UserListProvider';
9
10
  import UserList from '@cccsaurora/howler-ui/components/elements/UserList';
10
11
  import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
@@ -31,6 +32,7 @@ const AnalyticDetails = () => {
31
32
  const { dispatchApi } = useMyApi();
32
33
  const theme = useTheme();
33
34
  const { showSuccessMessage } = useMySnackbar();
35
+ const { withConfirmDeleteModal } = useContext(ModalContext);
34
36
  const { users, searchUsers } = useContext(UserListContext);
35
37
  const { config } = useContext(ApiConfigContext);
36
38
  const [analytic, setAnalytic] = useState(null);
@@ -58,11 +60,13 @@ const AnalyticDetails = () => {
58
60
  });
59
61
  setAnalytic(result);
60
62
  }, [analytic?.analytic_id, dispatchApi]);
61
- const onDelete = useCallback(async () => {
62
- await dispatchApi(api.analytic.del(analytic?.analytic_id));
63
- showSuccessMessage(t('route.analytics.deleted'));
64
- navigate('/analytics');
65
- }, [analytic?.analytic_id, dispatchApi, navigate, showSuccessMessage, t]);
63
+ const onDelete = useCallback(() => {
64
+ withConfirmDeleteModal(async () => {
65
+ await dispatchApi(api.analytic.del(analytic?.analytic_id, 'wait_for'));
66
+ showSuccessMessage(t('route.analytics.deleted'));
67
+ navigate('/analytics');
68
+ });
69
+ }, [analytic?.analytic_id, dispatchApi, navigate, withConfirmDeleteModal, showSuccessMessage, t]);
66
70
  const onEdit = useCallback(async () => {
67
71
  if (editingInterval) {
68
72
  setIntervalLoading(true);
@@ -115,7 +119,7 @@ const AnalyticDetails = () => {
115
119
  borderRadius: theme.shape.borderRadius,
116
120
  border: `thin solid ${theme.palette.divider}`,
117
121
  width: '100%'
118
- }, children: analytic.rule_crontab }))] }), (analytic?.owner === user.username || user.roles.includes('admin')) && (_jsx(Tooltip, { title: editingInterval ? t('rule.interval.save') : t('rule.interval.edit'), children: _jsx(IconButton, { disabled: intervalLoading, sx: { alignSelf: 'end' }, onClick: onEdit, children: intervalLoading ? _jsx(CircularProgress, { size: 20 }) : editingInterval ? _jsx(Check, {}) : _jsx(Edit, {}) }) }))] }))] }), _jsxs(Grid, { container: true, children: [_jsx(Grid, { item: true, xs: 12, md: 9, children: _jsxs(Tabs, { value: tab, onChange: (_, _tab) => setTab(_tab), children: [_jsx(Tab, { label: t('route.analytics.tab.overview'), value: "overview" }), _jsx(Tab, { label: t('route.analytics.tab.comments'), value: "comments" }), _jsx(Tab, { label: t('route.analytics.tab.hit_comments'), value: "hit_comments" }), _jsx(Tab, { label: t('route.analytics.tab.templates'), value: "templates" }), _jsx(Tab, { label: t('route.analytics.tab.overviews'), value: "overviews" }), analytic?.rule && _jsx(Tab, { label: t('route.analytics.tab.rule'), value: "rule" }), config?.configuration.features.notebook && (_jsx(Tab, { label: t('route.analytics.tab.notebooks'), value: "notebooks" })), _jsx(Tab, { label: t('route.analytics.tab.triage'), value: "triage" })] }) }), ['comments', 'hit_comments'].includes(tab) && (_jsx(Grid, { item: true, xs: 12, md: 3, children: _jsx(Autocomplete, { options: analytic?.detections ?? [], renderInput: param => _jsx(TextField, { ...param, label: "Detection" }), value: filter, onChange: (_, v) => setFilter(v) }) }))] }), {
122
+ }, children: analytic.rule_crontab }))] }), (analytic?.owner === user.username || user.roles.includes('admin')) && (_jsx(Tooltip, { title: editingInterval ? t('rule.interval.save') : t('rule.interval.edit'), children: _jsx(IconButton, { disabled: intervalLoading, sx: { alignSelf: 'end' }, onClick: onEdit, children: intervalLoading ? _jsx(CircularProgress, { size: 20 }) : editingInterval ? _jsx(Check, {}) : _jsx(Edit, {}) }) }))] }))] }), _jsxs(Grid, { container: true, children: [_jsx(Grid, { item: true, xs: 12, md: 9, children: _jsxs(Tabs, { value: tab, onChange: (_, _tab) => setTab(_tab), children: [_jsx(Tab, { label: t('route.analytics.tab.overview'), value: "overview" }), _jsx(Tab, { label: t('route.analytics.tab.comments'), value: "comments" }), _jsx(Tab, { label: t('route.analytics.tab.hit_comments'), value: "hit_comments" }), _jsx(Tab, { label: t('route.analytics.tab.templates'), value: "templates" }), _jsx(Tab, { label: t('route.analytics.tab.overviews'), value: "overviews" }), analytic?.rule && _jsx(Tab, { label: t('route.analytics.tab.rule'), value: "rule" }), config?.configuration.features.notebook && (_jsx(Tab, { label: t('route.analytics.tab.notebooks'), value: "notebooks" })), _jsx(Tab, { label: t('route.analytics.tab.triage'), value: "triage" })] }) }), ['comments', 'hit_comments'].includes(tab) && (_jsx(Grid, { item: true, xs: 12, md: 3, children: _jsx(Autocomplete, { options: analytic?.detections ?? [], renderInput: param => _jsx(TextField, { ...param, label: t('route.analytics.dropdown.detection') }), value: filter, onChange: (_, v) => setFilter(v) }) }))] }), {
119
123
  comments: _jsx(AnalyticComments, { analytic: analytic, setAnalytic: setAnalytic }),
120
124
  hit_comments: _jsx(AnalyticHitComments, { analytic: analytic }),
121
125
  overview: _jsx(AnalyticOverview, { analytic: analytic, setAnalytic: setAnalytic }),
@@ -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;
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Topic } from '@mui/icons-material';
3
3
  import { Stack, Typography } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
+ import { SearchResponseContext } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
5
6
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
6
7
  import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
7
8
  import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
8
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
9
9
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
10
10
  import dayjs from 'dayjs';
11
11
  import { useCallback, useContext, useEffect, useRef, useState } from 'react';
@@ -28,13 +28,12 @@ const buildPhraseQuery = (phrase) => {
28
28
  const CasesBase = () => {
29
29
  const { t } = useTranslation();
30
30
  const navigate = useNavigate();
31
- const { dispatchApi } = useMyApi();
32
31
  const [searchParams, setSearchParams] = useSearchParams();
33
32
  const { load } = useContext(TuiListMethodContext);
34
33
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
34
+ const { response, request } = useContext(SearchResponseContext);
35
35
  const [phrase, setPhrase] = useState('');
36
36
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
37
- const [response, setResponse] = useState(null);
38
37
  const [hasError, setHasError] = useState(false);
39
38
  const [loading, setLoading] = useState(false);
40
39
  const [statusFilter, setStatusFilter] = useState([]);
@@ -74,12 +73,12 @@ const CasesBase = () => {
74
73
  }
75
74
  setSearchParams(searchParams, { replace: true });
76
75
  const filters = buildFilters();
77
- setResponse(await dispatchApi(api.search.case.post({
76
+ await request(api.search.case.post, {
78
77
  query: buildPhraseQuery(phrase),
79
78
  filters,
80
79
  rows: pageCount,
81
80
  offset
82
- })));
81
+ });
83
82
  }
84
83
  catch (e) {
85
84
  setHasError(true);
@@ -87,7 +86,7 @@ const CasesBase = () => {
87
86
  finally {
88
87
  setLoading(false);
89
88
  }
90
- }, [buildFilters, phrase, setSearchParams, searchParams, dispatchApi, pageCount, offset]);
89
+ }, [phrase, setSearchParams, searchParams, buildFilters, request, pageCount, offset]);
91
90
  // Load the items into list when response changes.
92
91
  useEffect(() => {
93
92
  if (response) {
@@ -43,6 +43,7 @@ const ResolveModal = ({ case: _case, onConfirm }) => {
43
43
  const [loading, setLoading] = useState(true);
44
44
  const [rationale, setRationale] = useState('');
45
45
  const [assessment, setAssessment] = useState(null);
46
+ const [allowUnresolvedHits, setAllowUnresolvedHits] = useState(false);
46
47
  const [selectedHitIds, setSelectedHitIds] = useState(new Set());
47
48
  const hitIds = useMemo(() => uniq((_case?.items ?? [])
48
49
  .filter(item => item.type === 'hit')
@@ -64,8 +65,15 @@ const ResolveModal = ({ case: _case, onConfirm }) => {
64
65
  const handleConfirm = async () => {
65
66
  setLoading(true);
66
67
  try {
67
- await assess(assessment, true, rationale);
68
- setSelectedHitIds(new Set());
68
+ if (selectedHitIds.size > 0) {
69
+ await assess(assessment, true, rationale);
70
+ setSelectedHitIds(new Set());
71
+ }
72
+ if (unresolvedHits.length === 0 || allowUnresolvedHits) {
73
+ await updateCase({ status: 'resolved' });
74
+ onConfirm();
75
+ close();
76
+ }
69
77
  }
70
78
  finally {
71
79
  setLoading(false);
@@ -97,19 +105,12 @@ const ResolveModal = ({ case: _case, onConfirm }) => {
97
105
  }
98
106
  })();
99
107
  }, [dispatchApi, hitIds, loadRecords]);
100
- useEffect(() => {
101
- if (loading || unresolvedHits.length > 0) {
102
- return;
103
- }
104
- updateCase({ status: 'resolved' }).then(() => {
105
- onConfirm();
106
- close();
107
- });
108
- }, [close, loading, onConfirm, unresolvedHits.length, updateCase]);
109
- return (_jsxs(Stack, { spacing: 2, p: 2, alignItems: "start", sx: { minWidth: 'min(1000px, 60vw)', maxHeight: '100%', height: '100%' }, children: [_jsx(Typography, { variant: "h4", children: t('modal.cases.resolve') }), _jsx(Typography, { children: t('modal.cases.resolve.description') }), _jsxs(Stack, { spacing: 1, overflow: "auto", width: "100%", flex: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Box, { flex: 1, children: _jsx(TextField, { size: "small", fullWidth: true, placeholder: t('modal.rationale.label'), "aria-label": t('modal.rationale.label'), value: rationale, onChange: ev => setRationale(ev.target.value) }) }), _jsx(Box, { flex: 1, children: _jsx(Autocomplete, { size: "small", value: assessment, onChange: (_ev, _assessment) => setAssessment(_assessment), options: config.lookups['howler.assessment'], disablePortal: true, renderInput: params => (_jsx(TextField, { ...params, placeholder: t('hit.details.actions.assessment'), "aria-label": t('hit.details.actions.assessment'), fullWidth: true })) }) })] }), _jsxs(Stack, { position: "relative", children: [_jsx(Divider, {}), _jsx(LinearProgress, { sx: { opacity: +loading } })] }), hits
108
+ return (_jsxs(Stack, { spacing: 2, p: 2, alignItems: "start", sx: { minWidth: 'min(1000px, 60vw)', maxHeight: '100%', height: '100%' }, children: [_jsx(Typography, { variant: "h4", children: t('modal.cases.resolve') }), _jsx(Typography, { children: t('modal.cases.resolve.description') }), _jsxs(Stack, { spacing: 1, overflow: "auto", width: "100%", flex: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Box, { flex: 1, children: _jsx(TextField, { size: "small", fullWidth: true, placeholder: t('modal.rationale.label'), "aria-label": t('modal.rationale.label'), value: rationale, onChange: ev => setRationale(ev.target.value) }) }), _jsx(Box, { flex: 1, children: _jsx(Autocomplete, { size: "small", value: assessment, onChange: (_ev, _assessment) => setAssessment(_assessment), options: config.lookups['howler.assessment'], disablePortal: true, renderInput: params => (_jsx(TextField, { ...params, placeholder: t('hit.details.actions.assessment'), "aria-label": t('hit.details.actions.assessment'), fullWidth: true })) }) })] }), _jsxs(Stack, { position: "relative", children: [_jsx(Divider, {}), _jsx(LinearProgress, { sx: { opacity: +loading } })] }), _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Checkbox, { size: "small", checked: allowUnresolvedHits, onChange: (_ev, checked) => setAllowUnresolvedHits(checked), inputProps: { 'aria-label': t('modal.cases.resolve.allow_unresolved.aria_label') } }), _jsx(Typography, { children: t('modal.cases.resolve.allow_unresolved') })] }), hits
110
109
  .filter(hit => unresolvedHits.includes(hit.howler.id))
111
110
  .map(hit => (_jsx(HitEntry, { hit: hit, checked: selectedHitIds.has(hit.howler.id), onChange: () => handleToggleHit(hit.howler.id) }, hit.howler.id))), _jsxs(Accordion, { variant: "outlined", children: [_jsx(AccordionSummary, { expandIcon: _jsx(KeyboardArrowDown, {}), children: t('modal.cases.alerts.resolved') }), _jsx(AccordionDetails, { children: _jsx(Stack, { spacing: 1, children: hits
112
111
  .filter(hit => !unresolvedHits.includes(hit.howler.id))
113
- .map(hit => (_jsx(HitEntry, { hit: hit }, hit.howler.id))) }) })] })] }), _jsxs(Stack, { direction: "row", spacing: 1, alignSelf: "end", children: [_jsx(Button, { variant: "outlined", color: "error", onClick: close, children: t('cancel') }), _jsx(Button, { variant: "outlined", color: "success", disabled: loading || !assessment || !rationale || selectedHitIds.size === 0, startIcon: loading ? _jsx(CircularProgress, { size: 16, color: "inherit" }) : undefined, onClick: handleConfirm, children: t('confirm') })] })] }));
112
+ .map(hit => (_jsx(HitEntry, { hit: hit }, hit.howler.id))) }) })] })] }), _jsxs(Stack, { direction: "row", spacing: 1, alignSelf: "end", children: [_jsx(Button, { variant: "outlined", color: "error", onClick: close, children: t('cancel') }), _jsx(Button, { variant: "outlined", color: "success", disabled: loading ||
113
+ (selectedHitIds.size > 0 && (!assessment || !rationale)) ||
114
+ (selectedHitIds.size === 0 && unresolvedHits.length > 0 && !allowUnresolvedHits), startIcon: loading ? _jsx(CircularProgress, { size: 16, color: "inherit" }) : undefined, onClick: handleConfirm, children: t('confirm') })] })] }));
114
115
  };
115
116
  export default ResolveModal;