@cccsaurora/howler-ui 2.19.0-dev.830 → 2.19.0-dev.836

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.
@@ -13,6 +13,7 @@ import useMyTheme from '@cccsaurora/howler-ui/components/hooks/useMyTheme';
13
13
  import useMyUser from '@cccsaurora/howler-ui/components/hooks/useMyUser';
14
14
  import LoginScreen from '@cccsaurora/howler-ui/components/logins/Login';
15
15
  import useLogin from '@cccsaurora/howler-ui/components/logins/hooks/useLogin';
16
+ import PermissionDeniedPage from '@cccsaurora/howler-ui/components/routes/403';
16
17
  import NotFoundPage from '@cccsaurora/howler-ui/components/routes/404';
17
18
  import ErrorBoundary from '@cccsaurora/howler-ui/components/routes/ErrorBoundary';
18
19
  import Logout from '@cccsaurora/howler-ui/components/routes/Logout';
@@ -84,12 +85,12 @@ dayjs.extend(duration);
84
85
  dayjs.extend(relativeTime);
85
86
  dayjs.extend(localizedFormat);
86
87
  loader.config({ monaco });
87
- const RoleRoute = ({ role }) => {
88
+ const RoleRoute = ({ roles }) => {
88
89
  const appUser = useAppUser();
89
- if (appUser.user?.roles?.includes(role)) {
90
+ if (roles.some((role) => appUser.user?.roles?.includes(role))) {
90
91
  return _jsx(Outlet, {});
91
92
  }
92
- return _jsx(NotFoundPage, {});
93
+ return _jsx(PermissionDeniedPage, {});
93
94
  };
94
95
  // Your application's initialization flow.
95
96
  const MyApp = () => {
@@ -304,7 +305,13 @@ const createRouter = () => createBrowserRouter([
304
305
  },
305
306
  {
306
307
  path: 'action',
307
- element: _jsx(RoleRoute, { role: "automation_basic" }),
308
+ element: (_jsx(RoleRoute, { roles: [
309
+ 'admin',
310
+ 'automation_basic',
311
+ 'automation_advanced',
312
+ 'actionrunner_basic',
313
+ 'actionrunner_advanced'
314
+ ] })),
308
315
  children: [
309
316
  {
310
317
  index: true,
@@ -0,0 +1,3 @@
1
+ import type { FC } from 'react';
2
+ declare const PermissionDeniedPage: FC;
3
+ export default PermissionDeniedPage;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PersonOff } from '@mui/icons-material';
3
+ import { Box, Typography } from '@mui/material';
4
+ import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
5
+ import { useTranslation } from 'react-i18next';
6
+ const PermissionDeniedPage = () => {
7
+ const { t } = useTranslation();
8
+ return (_jsxs(PageCenter, { width: "75%", children: [_jsx(Box, { pt: 6, textAlign: "center", fontSize: 200, children: _jsx(PersonOff, { color: "secondary", fontSize: "inherit" }) }), _jsx(Box, { pb: 2, children: _jsx(Typography, { variant: "h2", children: t('page.403.title') }) }), _jsx(Box, { children: _jsx(Typography, { variant: "h5", children: t('page.403.description') }) })] }));
9
+ };
10
+ export default PermissionDeniedPage;
@@ -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);
@@ -142,7 +142,10 @@ const useMyActionFunctions = () => {
142
142
  showErrorMessage(_jsx(Trans, { i18nKey: "actions.error", values: { action: actionName, messages: errors.map(error => error.message).join(', ') } }));
143
143
  }
144
144
  if (skipped.length > 0) {
145
- showInfoMessage(_jsx(Trans, { i18nKey: "actions.skipped", values: { action: actionName, messages: skipped.map(skippedAction => skippedAction.message).join(', ') } }));
145
+ showInfoMessage(_jsx(Trans, { i18nKey: "actions.skipped", values: {
146
+ action: actionName,
147
+ messages: skipped.map(skippedAction => skippedAction.message).join(', ')
148
+ } }));
146
149
  }
147
150
  if (succeeded.length > 0) {
148
151
  showSuccessMessage(_jsx(Trans, { i18nKey: "actions.succeeded", values: { action: actionName } }));
@@ -58,7 +58,12 @@ const ActionDetails = () => {
58
58
  }
59
59
  // eslint-disable-next-line react-hooks/exhaustive-deps
60
60
  }, [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
61
+ const editRoles = user.roles.includes('automation_basic') || user.roles.includes('automation_advanced');
62
+ const execRoles = editRoles ||
63
+ user.roles.includes('admin') ||
64
+ user.roles.includes('actionrunner_basic') ||
65
+ user.roles.includes('actionrunner_advanced');
66
+ 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: () => deleteAction(action?.action_id), children: t('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
67
  ?.map(a => (operations ?? []).find(_action => _action.id === a.operation_id)?.triggers ?? [])
63
68
  .reduce((acc, triggers) => acc.filter(_t => triggers.includes(_t)))
64
69
  .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 &&
@@ -97,6 +97,7 @@ const ActionSearch = () => {
97
97
  onSearch();
98
98
  // eslint-disable-next-line react-hooks/exhaustive-deps
99
99
  }, [searchModifiers]);
100
+ const editRoles = user.roles.includes('automation_basic') || user.roles.includes('automation_advanced');
100
101
  // Search result list item renderer.
101
102
  const renderer = useCallback(({ item }, classRenderer) => {
102
103
  return (_jsxs(Card, { onClick: () => navigate(`/action/${item.item.action_id}`), variant: "outlined", className: classRenderer(), sx: {
@@ -104,14 +105,14 @@ const ActionSearch = () => {
104
105
  transitionProperty: 'border-color',
105
106
  cursor: 'pointer',
106
107
  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
+ }, 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: async (e) => {
108
109
  e.preventDefault();
109
110
  e.stopPropagation();
110
111
  await deleteAction(item.item.action_id);
111
112
  onSearch();
112
113
  }, 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, { size: "small", 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 } }) }));
114
+ }, [deleteAction, editRoles, navigate, onSearch, t, user.roles, user.username]);
115
+ 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
116
  };
116
117
  const ActionSearchProvider = () => {
117
118
  return (_jsx(TuiListProvider, { children: _jsx(ActionSearch, {}) }));
@@ -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"
@@ -25,7 +25,7 @@ const ActionIntroductionDocumentation = () => {
25
25
  useEffect(() => {
26
26
  api.action.operations
27
27
  .get()
28
- .then(_operations => _operations.filter(a => difference(a.roles, user.roles).length < 1))
28
+ .then(_operations => _operations.filter(a => difference(a.roles, user.roles).length < a.roles.length))
29
29
  .then(setOperations)
30
30
  // eslint-disable-next-line no-console
31
31
  .catch(console.debug);
@@ -1 +1 @@
1
- export default "# Retention in Howler\n\nIn order to comply with organizational policies, Howler is configured to purge stale alerts after a specific amount of\ntime. On this instance, that duration is `duration`.\n\n## How Retention Works\n\nHowler uses an automated retention job that runs on a configurable schedule (typically nightly) to remove\nalerts that have exceeded their retention period. The system evaluates two criteria for deletion:\n\n1. **Standard Retention**: Alerts are deleted when `event.created` exceeds the configured retention period\n2. **Custom Expiry**: Alerts are deleted when the `howler.expiry` field indicates the alert should expire\n\nAn alert will be removed when **either** condition is met - whichever comes first.\n\n## Custom Expiry (`howler.expiry`)\n\nThe `howler.expiry` field allows detection engineers to set custom retention periods for specific alerts\nduring ingestion. This field overrides the standard retention calculation and is commonly used when:\n\n- Clients have requested shorter data retention periods than the deployment default\n- Specific operations require time-limited data storage (e.g., a cybersecurity operation where data can\n only be retained for two weeks after ingest)\n- Regulatory requirements mandate earlier deletion for certain types of data\n\n```alert\nThe howler.expiry field can only shorten retention periods, not extend them. No matter\nwhat, alerts cannot be retained longer than the system-wide retention cutoff based on event.created.\n```\n\n## Configuration\n\nAdministrators can configure retention settings in the system configuration:\n\n```yaml\nsystem:\n type: staging\n retention:\n limit_amount: 120 # Retention period duration\n limit_unit: days # Time unit (days, hours, etc.)\n crontab: \"0 0 * * *\" # Schedule (nightly at midnight)\n enabled: true # Whether retention is active\n```\n\n## User Interface\n\nTo communicate retention timing to users, see the example alert below:\n\n`alert`\n\nIn the top right, hovering over the timestamp will outline how long users have before the alert is\nremoved. In order to ensure compliance with policy, ensure that `event.created` matches the date the\nunderlying data was collected, allowing Howler to ensure data is purged in time.\n"
1
+ export default "# Retention in Howler\n\nIn order to comply with organizational policies, Howler is configured to purge stale alerts after a specific amount of\ntime. On this instance, that duration is `duration`.\n\n## How Retention Works\n\nHowler uses an automated retention job that runs on a configurable schedule (typically nightly) to remove\nalerts that have exceeded their retention period. The system evaluates two criteria for deletion:\n\n1. **Standard Retention**: Alerts are deleted when `event.created` exceeds the configured retention period\n2. **Custom Expiry**: Alerts are deleted when the `howler.expiry` field indicates the alert should expire\n\nAn alert will be removed when **either** condition is met - whichever comes first.\n\n## Custom Expiry (`howler.expiry`)\n\nThe `howler.expiry` field allows detection engineers to set custom retention periods for specific alerts\nduring ingestion. This field overrides the standard retention calculation and is commonly used when:\n\n- Clients have requested shorter data retention periods than the deployment default\n- Specific operations require time-limited data storage (e.g., a cybersecurity operation where data can\n only be retained for two weeks after ingest)\n- Regulatory requirements mandate earlier deletion for certain types of data\n\n```alert\nThe howler.expiry field can only shorten retention periods, not extend them. No matter\nwhat, alerts cannot be retained longer than the system-wide retention cutoff based on event.created.\n```\n\n## Configuration\n\nAdministrators can configure retention settings in the system configuration:\n\n```yaml\nsystem:\n type: staging\n retention:\n limit_amount: 120 # Retention period duration\n limit_unit: days # Time unit (days, hours, etc.)\n crontab: '0 0 * * *' # Schedule (nightly at midnight)\n enabled: true # Whether retention is active\n```\n\n## User Interface\n\nTo communicate retention timing to users, see the example alert below:\n\n`alert`\n\nIn the top right, hovering over the timestamp will outline how long users have before the alert is\nremoved. In order to ensure compliance with policy, ensure that `event.created` matches the date the\nunderlying data was collected, allowing Howler to ensure data is purged in time.\n"
@@ -1 +1 @@
1
- export default "# R\u00e9tention dans Howler\n\nAfin de se conformer aux politiques organisationnelles, Howler est configur\u00e9 pour purger les alertes\np\u00e9rim\u00e9es apr\u00e8s une p\u00e9riode de temps sp\u00e9cifique. Dans cette instance, cette dur\u00e9e est `duration`.\n\n## Comment fonctionne la r\u00e9tention\n\nHowler utilise un travail de r\u00e9tention automatis\u00e9 qui s'ex\u00e9cute selon un calendrier configurable\n(g\u00e9n\u00e9ralement nocturne) pour supprimer les alertes qui ont d\u00e9pass\u00e9 leur p\u00e9riode de r\u00e9tention. Le syst\u00e8me\n\u00e9value deux crit\u00e8res de suppression :\n\n1. **R\u00e9tention standard** : Les alertes sont supprim\u00e9es lorsque `event.created` d\u00e9passe la p\u00e9riode de\n r\u00e9tention configur\u00e9e\n2. **Expiration personnalis\u00e9e** : Les alertes sont supprim\u00e9es lorsque le champ `howler.expiry` indique\n que l'alerte doit expirer\n\nUne alerte sera supprim\u00e9e lorsque **l'une ou l'autre** condition est remplie - selon celle qui arrive en\npremier.\n\n## Expiration personnalis\u00e9e (`howler.expiry`)\n\nLe champ `howler.expiry` permet aux ing\u00e9nieurs de d\u00e9tection de d\u00e9finir des p\u00e9riodes de r\u00e9tention\npersonnalis\u00e9es pour des alertes sp\u00e9cifiques lors de l'ingestion. Ce champ remplace le calcul de\nr\u00e9tention standard et est couramment utilis\u00e9 quand :\n\n- Les clients ont demand\u00e9 des p\u00e9riodes de r\u00e9tention de donn\u00e9es plus courtes que la valeur par d\u00e9faut\n du d\u00e9ploiement\n- Des op\u00e9rations sp\u00e9cifiques n\u00e9cessitent un stockage de donn\u00e9es \u00e0 dur\u00e9e limit\u00e9e (par ex., une op\u00e9ration\n de cybers\u00e9curit\u00e9 o\u00f9 les donn\u00e9es ne peuvent \u00eatre conserv\u00e9es que deux semaines apr\u00e8s ingestion)\n- Les exigences r\u00e9glementaires imposent une suppression plus pr\u00e9coce pour certains types de donn\u00e9es\n\n```alert\nLe champ howler.expiry ne peut que raccourcir les p\u00e9riodes de r\u00e9tention, pas les\nprolonger. Quoi qu'il arrive, les alertes ne peuvent pas \u00eatre conserv\u00e9es plus longtemps que la limite de\nr\u00e9tention syst\u00e8me bas\u00e9e sur event.created.\n```\n\n## Configuration\n\nLes administrateurs peuvent configurer les param\u00e8tres de r\u00e9tention dans la configuration syst\u00e8me :\n\n```yaml\nsystem:\n type: staging\n retention:\n limit_amount: 120 # Dur\u00e9e de la p\u00e9riode de r\u00e9tention\n limit_unit: days # Unit\u00e9 de temps (days, hours, etc.)\n crontab: \"0 0 * * *\" # Calendrier (nocturne \u00e0 minuit)\n enabled: true # Si la r\u00e9tention est active\n```\n\n## Interface utilisateur\n\nAfin de communiquer le d\u00e9lai de r\u00e9tention aux utilisateurs, voir l'exemple d'alerte ci-dessous :\n\n`alert`\n\nEn haut \u00e0 droite, le survol de l'horodatage indique le temps dont dispose l'utilisateur avant que\nl'alerte ne soit supprim\u00e9e. Afin de se conformer aux politiques, assurez-vous que `event.created`\ncorrespond \u00e0 la date \u00e0 laquelle les donn\u00e9es sous-jacentes ont \u00e9t\u00e9 collect\u00e9es, permettant \u00e0 Howler de\ns'assurer que les donn\u00e9es sont purg\u00e9es \u00e0 temps.\n"
1
+ export default "# R\u00e9tention dans Howler\n\nAfin de se conformer aux politiques organisationnelles, Howler est configur\u00e9 pour purger les alertes\np\u00e9rim\u00e9es apr\u00e8s une p\u00e9riode de temps sp\u00e9cifique. Dans cette instance, cette dur\u00e9e est `duration`.\n\n## Comment fonctionne la r\u00e9tention\n\nHowler utilise un travail de r\u00e9tention automatis\u00e9 qui s'ex\u00e9cute selon un calendrier configurable\n(g\u00e9n\u00e9ralement nocturne) pour supprimer les alertes qui ont d\u00e9pass\u00e9 leur p\u00e9riode de r\u00e9tention. Le syst\u00e8me\n\u00e9value deux crit\u00e8res de suppression :\n\n1. **R\u00e9tention standard** : Les alertes sont supprim\u00e9es lorsque `event.created` d\u00e9passe la p\u00e9riode de\n r\u00e9tention configur\u00e9e\n2. **Expiration personnalis\u00e9e** : Les alertes sont supprim\u00e9es lorsque le champ `howler.expiry` indique\n que l'alerte doit expirer\n\nUne alerte sera supprim\u00e9e lorsque **l'une ou l'autre** condition est remplie - selon celle qui arrive en\npremier.\n\n## Expiration personnalis\u00e9e (`howler.expiry`)\n\nLe champ `howler.expiry` permet aux ing\u00e9nieurs de d\u00e9tection de d\u00e9finir des p\u00e9riodes de r\u00e9tention\npersonnalis\u00e9es pour des alertes sp\u00e9cifiques lors de l'ingestion. Ce champ remplace le calcul de\nr\u00e9tention standard et est couramment utilis\u00e9 quand :\n\n- Les clients ont demand\u00e9 des p\u00e9riodes de r\u00e9tention de donn\u00e9es plus courtes que la valeur par d\u00e9faut\n du d\u00e9ploiement\n- Des op\u00e9rations sp\u00e9cifiques n\u00e9cessitent un stockage de donn\u00e9es \u00e0 dur\u00e9e limit\u00e9e (par ex., une op\u00e9ration\n de cybers\u00e9curit\u00e9 o\u00f9 les donn\u00e9es ne peuvent \u00eatre conserv\u00e9es que deux semaines apr\u00e8s ingestion)\n- Les exigences r\u00e9glementaires imposent une suppression plus pr\u00e9coce pour certains types de donn\u00e9es\n\n```alert\nLe champ howler.expiry ne peut que raccourcir les p\u00e9riodes de r\u00e9tention, pas les\nprolonger. Quoi qu'il arrive, les alertes ne peuvent pas \u00eatre conserv\u00e9es plus longtemps que la limite de\nr\u00e9tention syst\u00e8me bas\u00e9e sur event.created.\n```\n\n## Configuration\n\nLes administrateurs peuvent configurer les param\u00e8tres de r\u00e9tention dans la configuration syst\u00e8me :\n\n```yaml\nsystem:\n type: staging\n retention:\n limit_amount: 120 # Dur\u00e9e de la p\u00e9riode de r\u00e9tention\n limit_unit: days # Unit\u00e9 de temps (days, hours, etc.)\n crontab: '0 0 * * *' # Calendrier (nocturne \u00e0 minuit)\n enabled: true # Si la r\u00e9tention est active\n```\n\n## Interface utilisateur\n\nAfin de communiquer le d\u00e9lai de r\u00e9tention aux utilisateurs, voir l'exemple d'alerte ci-dessous :\n\n`alert`\n\nEn haut \u00e0 droite, le survol de l'horodatage indique le temps dont dispose l'utilisateur avant que\nl'alerte ne soit supprim\u00e9e. Afin de se conformer aux politiques, assurez-vous que `event.created`\ncorrespond \u00e0 la date \u00e0 laquelle les donn\u00e9es sous-jacentes ont \u00e9t\u00e9 collect\u00e9es, permettant \u00e0 Howler de\ns'assurer que les donn\u00e9es sont purg\u00e9es \u00e0 temps.\n"
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { AddCircleOutline, Assignment, Edit, HowToVote, KeyboardArrowRight, OpenInNew, QueryStats, RemoveCircleOutline, SettingsSuggest, Terminal } from '@mui/icons-material';
3
3
  import { Box, Divider, Fade, ListItemIcon, ListItemText, Menu, MenuItem, MenuList, Paper } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
+ import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
5
6
  import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
6
7
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
7
8
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
@@ -46,6 +47,7 @@ const HitContextMenu = ({ children, getSelectedId, Component = Box }) => {
46
47
  const { t } = useTranslation();
47
48
  const { dispatchApi } = useMyApi();
48
49
  const { executeAction } = useMyActionFunctions();
50
+ const appUser = useAppUser();
49
51
  const { config } = useContext(ApiConfigContext);
50
52
  const pluginStore = usePluginStore();
51
53
  const { getMatchingAnalytic, getMatchingTemplate } = useMatchers();
@@ -62,6 +64,14 @@ const HitContextMenu = ({ children, getSelectedId, Component = Box }) => {
62
64
  const [show, setShow] = useState({});
63
65
  const hits = useMemo(() => (selectedHits.some(_hit => _hit.howler.id === hit?.howler.id) ? selectedHits : [hit]), [hit, selectedHits]);
64
66
  const { availableTransitions, canVote, canAssess, assess, vote } = useHitActions(hits);
67
+ /**
68
+ * Checks if the current user has permission to run actions.
69
+ * Users must have one of the automation or actionrunner roles, or be an admin.
70
+ */
71
+ const canRunActions = useCallback(() => {
72
+ const roles = ['admin', 'automation_advanced', 'automation_basic', 'actionrunner_advanced', 'actionrunner_basic'];
73
+ return roles.some((role) => appUser.user?.roles?.includes(role));
74
+ }, [appUser.user?.roles]);
65
75
  /**
66
76
  * Handles right-click context menu events.
67
77
  * Opens the context menu at the click location and loads available actions.
@@ -174,7 +184,7 @@ const HitContextMenu = ({ children, getSelectedId, Component = Box }) => {
174
184
  overflow: 'visible !important'
175
185
  }
176
186
  }
177
- }, MenuListProps: { dense: true, sx: { minWidth: '250px' } }, anchorOrigin: { vertical: 'top', horizontal: 'left' }, onClick: () => setAnchorEl(null), children: [_jsxs(MenuItem, { component: Link, to: `/hits/${hit?.howler.id}`, disabled: !hit, children: [_jsx(ListItemIcon, { children: _jsx(OpenInNew, {}) }), _jsx(ListItemText, { children: t('hit.panel.open') })] }), _jsxs(MenuItem, { component: Link, to: `/analytics/${analytic?.analytic_id}`, disabled: !analytic, children: [_jsx(ListItemIcon, { children: _jsx(QueryStats, {}) }), _jsx(ListItemText, { children: t('hit.panel.analytic.open') })] }), _jsx(Divider, {}), entries.map(([type, items]) => (_jsxs(MenuItem, { id: `${type}-menu-item`, sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, [type]: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, [type]: null })), disabled: rowStatus[type] === false, children: [_jsx(ListItemIcon, { children: ICON_MAP[type] ?? _jsx(Terminal, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t(`hit.details.actions.${type}`) }), rowStatus[type] !== false && (_jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } })), _jsx(Fade, { in: !!show[type], unmountOnExit: true, children: _jsx(Paper, { id: `${type}-submenu`, sx: calculateSubMenuStyles(show[type]), elevation: 8, children: _jsx(MenuList, { sx: { p: 0, borderTopLeftRadius: 0 }, dense: true, role: "group", children: items.map(a => (_jsx(MenuItem, { value: a.name, onClick: a.actionFunction, children: a.i18nKey ? t(a.i18nKey) : capitalize(a.name) }, a.name))) }) }) })] }, type))), _jsxs(MenuItem, { id: "actions-menu-item", sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, actions: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, actions: null })), disabled: actions.length < 1, children: [_jsx(ListItemIcon, { children: _jsx(SettingsSuggest, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t('route.actions.change') }), actions.length > 0 && _jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } }), _jsx(Fade, { in: !!show.actions, unmountOnExit: true, children: _jsx(Paper, { id: "actions-submenu", sx: calculateSubMenuStyles(show.actions), elevation: 8, children: _jsx(MenuList, { sx: { p: 0 }, dense: true, role: "group", children: actions.map(action => (_jsx(MenuItem, { onClick: () => executeAction(action.action_id, `howler.id:${hit?.howler.id}`), children: _jsx(ListItemText, { children: action.name }) }, action.action_id))) }) }) })] }), !isEmpty(template?.keys ?? []) && setQuery && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(MenuItem, { id: "excludes-menu-item", sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, excludes: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, excludes: null })), children: [_jsx(ListItemIcon, { children: _jsx(RemoveCircleOutline, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t('hit.panel.exclude') }), _jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } }), _jsx(Fade, { in: !!show.excludes, unmountOnExit: true, children: _jsx(Paper, { id: "excludes-submenu", sx: calculateSubMenuStyles(show.excludes), elevation: 8, children: _jsx(MenuList, { sx: { p: 0 }, dense: true, role: "group", children: template?.keys.map(key => {
187
+ }, MenuListProps: { dense: true, sx: { minWidth: '250px' } }, anchorOrigin: { vertical: 'top', horizontal: 'left' }, onClick: () => setAnchorEl(null), children: [_jsxs(MenuItem, { component: Link, to: `/hits/${hit?.howler.id}`, disabled: !hit, children: [_jsx(ListItemIcon, { children: _jsx(OpenInNew, {}) }), _jsx(ListItemText, { children: t('hit.panel.open') })] }), _jsxs(MenuItem, { component: Link, to: `/analytics/${analytic?.analytic_id}`, disabled: !analytic, children: [_jsx(ListItemIcon, { children: _jsx(QueryStats, {}) }), _jsx(ListItemText, { children: t('hit.panel.analytic.open') })] }), _jsx(Divider, {}), entries.map(([type, items]) => (_jsxs(MenuItem, { id: `${type}-menu-item`, sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, [type]: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, [type]: null })), disabled: rowStatus[type] === false, children: [_jsx(ListItemIcon, { children: ICON_MAP[type] ?? _jsx(Terminal, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t(`hit.details.actions.${type}`) }), rowStatus[type] !== false && (_jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } })), _jsx(Fade, { in: !!show[type], unmountOnExit: true, children: _jsx(Paper, { id: `${type}-submenu`, sx: calculateSubMenuStyles(show[type]), elevation: 8, children: _jsx(MenuList, { sx: { p: 0, borderTopLeftRadius: 0 }, dense: true, role: "group", children: items.map(a => (_jsx(MenuItem, { value: a.name, onClick: a.actionFunction, children: a.i18nKey ? t(a.i18nKey) : capitalize(a.name) }, a.name))) }) }) })] }, type))), _jsxs(MenuItem, { id: "actions-menu-item", sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, actions: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, actions: null })), disabled: actions.length < 1 || !canRunActions(), children: [_jsx(ListItemIcon, { children: _jsx(SettingsSuggest, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t('route.actions.change') }), actions.length > 0 && _jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } }), _jsx(Fade, { in: !!show.actions, unmountOnExit: true, children: _jsx(Paper, { id: "actions-submenu", sx: calculateSubMenuStyles(show.actions), elevation: 8, children: _jsx(MenuList, { sx: { p: 0 }, dense: true, role: "group", children: actions.map(action => (_jsx(MenuItem, { onClick: () => executeAction(action.action_id, `howler.id:${hit?.howler.id}`), children: _jsx(ListItemText, { children: action.name }) }, action.action_id))) }) }) })] }), !isEmpty(template?.keys ?? []) && setQuery && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(MenuItem, { id: "excludes-menu-item", sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, excludes: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, excludes: null })), children: [_jsx(ListItemIcon, { children: _jsx(RemoveCircleOutline, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t('hit.panel.exclude') }), _jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } }), _jsx(Fade, { in: !!show.excludes, unmountOnExit: true, children: _jsx(Paper, { id: "excludes-submenu", sx: calculateSubMenuStyles(show.excludes), elevation: 8, children: _jsx(MenuList, { sx: { p: 0 }, dense: true, role: "group", children: template?.keys.map(key => {
178
188
  // Build exclusion query based on current query and field value
179
189
  let newQuery = '';
180
190
  if (query !== DEFAULT_QUERY) {
@@ -8,6 +8,16 @@ import { vi } from 'vitest';
8
8
  // Mock API
9
9
  vi.mock('api', { spy: true });
10
10
  setupContextSelectorMock();
11
+ // Mock useAppUser hook
12
+ const mockUseAppUser = vi.hoisted(() => vi.fn(() => ({
13
+ user: {
14
+ username: 'test-user',
15
+ roles: ['automation_basic', 'actionrunner_basic']
16
+ }
17
+ })));
18
+ vi.mock('commons/components/app/hooks/useAppUser', () => ({
19
+ useAppUser: mockUseAppUser
20
+ }));
11
21
  // Mock react-router-dom
12
22
  const mockNavigate = vi.fn();
13
23
  vi.mock('react-router-dom', async () => {
@@ -893,4 +903,90 @@ describe('HitContextMenu', () => {
893
903
  expect(mockPluginStoreExecuteFunction).toHaveBeenCalled();
894
904
  });
895
905
  });
906
+ describe('Role-Based Action Permissions', () => {
907
+ afterEach(() => {
908
+ // Reset to default user with required roles
909
+ mockUseAppUser.mockReturnValue({
910
+ user: {
911
+ username: 'test-user',
912
+ roles: ['automation_basic', 'actionrunner_basic']
913
+ }
914
+ });
915
+ });
916
+ it('should disable actions menu when user lacks required roles', async () => {
917
+ mockUseAppUser.mockReturnValue({
918
+ user: {
919
+ username: 'test-user',
920
+ roles: ['user', 'viewer']
921
+ }
922
+ });
923
+ const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
924
+ mockDispatchApi.mockResolvedValue({ items: mockActions });
925
+ rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
926
+ act(() => {
927
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
928
+ fireEvent.contextMenu(contextMenuWrapper);
929
+ });
930
+ await waitFor(() => {
931
+ const actionsMenuItem = screen.getByTestId('actions-menu-item');
932
+ expect(actionsMenuItem).toHaveAttribute('aria-disabled', 'true');
933
+ });
934
+ });
935
+ it('should enable actions menu when user has automation_basic role', async () => {
936
+ mockUseAppUser.mockReturnValue({
937
+ user: {
938
+ username: 'test-user',
939
+ roles: ['automation_basic']
940
+ }
941
+ });
942
+ const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
943
+ mockDispatchApi.mockResolvedValue({ items: mockActions });
944
+ rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
945
+ act(() => {
946
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
947
+ fireEvent.contextMenu(contextMenuWrapper);
948
+ });
949
+ await waitFor(() => {
950
+ const actionsMenuItem = screen.getByTestId('actions-menu-item');
951
+ expect(actionsMenuItem).not.toHaveAttribute('aria-disabled', 'true');
952
+ });
953
+ });
954
+ it('should enable actions menu when user has actionrunner_advanced role', async () => {
955
+ mockUseAppUser.mockReturnValue({
956
+ user: {
957
+ username: 'test-user',
958
+ roles: ['actionrunner_advanced']
959
+ }
960
+ });
961
+ const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
962
+ mockDispatchApi.mockResolvedValue({ items: mockActions });
963
+ rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
964
+ act(() => {
965
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
966
+ fireEvent.contextMenu(contextMenuWrapper);
967
+ });
968
+ await waitFor(() => {
969
+ const actionsMenuItem = screen.getByTestId('actions-menu-item');
970
+ expect(actionsMenuItem).not.toHaveAttribute('aria-disabled', 'true');
971
+ });
972
+ });
973
+ it('should still disable actions menu when user has roles but no actions available', async () => {
974
+ mockUseAppUser.mockReturnValue({
975
+ user: {
976
+ username: 'test-user',
977
+ roles: ['automation_advanced']
978
+ }
979
+ });
980
+ mockDispatchApi.mockResolvedValue({ items: [] });
981
+ rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
982
+ act(() => {
983
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
984
+ fireEvent.contextMenu(contextMenuWrapper);
985
+ });
986
+ await waitFor(() => {
987
+ const actionsMenuItem = screen.getByTestId('actions-menu-item');
988
+ expect(actionsMenuItem).toHaveAttribute('aria-disabled', 'true');
989
+ });
990
+ });
991
+ });
896
992
  });
@@ -64,7 +64,7 @@ const AddNewCard = ({ dashboard, addCard }) => {
64
64
  fetchAllAnalytics();
65
65
  }, []);
66
66
  const filteredAnalyticVisualizations = useMemo(() => {
67
- const existingAnalyticCards = dashboard.filter(_card => _card.type === "analytic");
67
+ const existingAnalyticCards = dashboard.filter(_card => _card.type === 'analytic');
68
68
  return VISUALIZATIONS.filter(viz => {
69
69
  return !existingAnalyticCards.some(_card => {
70
70
  const parsedConfig = JSON.parse(_card.config);
@@ -1 +1 @@
1
- export default "# Creating an Overview\n\nOverviews can be used to modify the way data is presented on alerts that match the overview's settings. Overviews are, by design, easy to create and quite flexible.\n\n## Getting Started\n\nThe basic building blocks of overviews are:\n\n1. Markdown\n2. Handlebars\n\nWe will quickly explain these.\n\n### Markdown\n\nQuoting from the excellent [markdownguide.org](https://www.markdownguide.org/getting-started/):\n\n> Markdown is a lightweight markup language that you can use to add formatting elements to plaintext text documents. Created by [John Gruber](https://daringfireball.net/projects/markdown/) in 2004, Markdown is now one of the world's most popular markup languages.\n>\n> Using Markdown is different than using a [WYSIWYG](https://en.wikipedia.org/wiki/WYSIWYG) editor. In an application like Microsoft Word, you click buttons to format words and phrases, and the changes are visible immediately. Markdown isn't like that. When you create a Markdown-formatted file, you add Markdown syntax to the text to indicate which words and phrases should look different.\n>\n> For example, to denote a heading, you add a number sign before it (e.g., `# Heading One`). Or to make a phrase bold, you add two asterisks before and after it (e.g., `**this text is bold**`). It may take a while to get used to seeing Markdown syntax in your text, especially if you're accustomed to WYSIWYG applications.\n\n---\n\n### Handlebars\n\nQuoting from [handlebarsjs.com](https://handlebarsjs.com/guide/):\n\n> Handlebars is a simple templating language.\n>\n> It uses a template and an input object to generate HTML or other text formats. Handlebars templates look like regular text with embedded Handlebars expressions.\n>\n>```html\n> <p>{{curly \"firstname\"}} {{curly \"lastname\"}}</p>\n>```\n>\n> A handlebars expression is a double curly bracket, some contents, followed by a set of closing double curly brackets. When the template is executed, these expressions are replaced with values from an input object.\n\n---\n\nFor our cases, we use handlebars to replace specific parts of markdown with the values included in a given howler hit. For example:\n\n```markdown\nThis analytic is **{{curly \"howler.analytic\"}}**\n```\n\nbecomes:\n\n> This analytic is **{{howler.analytic}}**.\n\nFor more information on handlebars, check out:\n\n- [What is Handlebars?](https://handlebarsjs.com/guide/#what-is-handlebars)\n- [Handlebars Expressions](https://handlebarsjs.com/guide/expressions.html)\n\n## Combining Markdown and Handlebars\n\nYou can use handlebars for template replacement throughout your markdown. Below is an example table using handlebars and markdown:\n\n```markdown\n\n| Source IP | Destination IP |\n| --- | --- |\n| {{curly \"source.ip\"}} |{{curly \"destination.ip\"}} |\n```\n\nrenders as:\n\n| Source IP | Destination IP |\n| --- | --- |\n| {{source.ip}} |{{destination.ip}} |\n\n## Advanced Handlebars\n\nHowler integrates a number of helper functions for you to work with.\n\n### Control Expressions\n\nFor use as subexpressions, we expose a number of conditional checks:\n\n**Equality:**\n\nGiven `howler.status` is {{howler.status}}:\n\n```markdown\n{{curly '#if (equals howler.status \"open\")'}}\nHit is open!\n{{curly \"/if\"}}\n{{curly '#if (equals howler.status \"resolved\")'}}\nHit is resolved!\n{{curly \"/if\"}}\n```\n\n{{#if (equals howler.status \"open\")}}\nHit is open!\n{{/if}}\n{{#if (equals howler.status \"resolved\")}}\nHit is resolved!\n{{/if}}\n\n**AND/OR/NOT:**\n\nGiven `howler.status` is {{howler.status}}, and `howler.escalation` is {{howler.escalation}}:\n\n```markdown\n{{curly '#if (and (equals howler.status \"open\") (equals howler.escalation \"alert\"))'}}\nThis is correct!\n{{curly \"/if\"}}\n{{curly '#if (and (equals howler.status \"resolved\") (equals howler.escalation \"hit\"))'}}\nThis is wrong!\n{{curly \"/if\"}}\n```\n\n{{#if (and (equals howler.status \"open\") (equals howler.escalation \"alert\"))}}\nThis is correct!\n{{/if}}\n{{#if (and (equals howler.status \"resolved\") (equals howler.escalation \"hit\"))}}\nThis is wrong!\n{{/if}}\n\n```markdown\n{{curly '#if (or howler.is_bundle (not howler.is_bundle))'}}\nAlways shows!\n{{curly \"/if\"}}\n```\n\n{{#if (or howler.is_bundle (not howler.is_bundle))}}\nAlways shows!\n{{/if}}\n\n---\n\n### String Operations\n\n**String Concatenation:**\n\n```markdown\n{{curly 'join \"string one \" \"string two\"'}}\n```\n\n{{join \"string one \" \"string two\"}}\n\n**Uppercase/Lowercase:**\n\n```markdown\n{{curly 'upper \"make this uppercase\"'}}\n{{curly 'lower \"MAKE THIS LOWERCASE\"'}}\n```\n\n{{upper \"make this uppercase\"}}\n\n{{lower \"MAKE THIS LOWERCASE\"}}\n\n---\n\n### Fetching Data\n\nYou can also make basic fetch requests for, and parse, JSON data from external sources:\n\n```markdown\n{{curly 'fetch \"/api/v1/configs\" \"api_response.c12nDef.UNRESTRICTED\"'}}\n```\n\n{{fetch \"/api/v1/configs\" \"api_response.c12nDef.UNRESTRICTED\"}}\n\n## Full Helper List\n"
1
+ export default "# Creating an Overview\n\nOverviews can be used to modify the way data is presented on alerts that match the overview's settings. Overviews are, by design, easy to create and quite flexible.\n\n## Getting Started\n\nThe basic building blocks of overviews are:\n\n1. Markdown\n2. Handlebars\n\nWe will quickly explain these.\n\n### Markdown\n\nQuoting from the excellent [markdownguide.org](https://www.markdownguide.org/getting-started/):\n\n> Markdown is a lightweight markup language that you can use to add formatting elements to plaintext text documents. Created by [John Gruber](https://daringfireball.net/projects/markdown/) in 2004, Markdown is now one of the world's most popular markup languages.\n>\n> Using Markdown is different than using a [WYSIWYG](https://en.wikipedia.org/wiki/WYSIWYG) editor. In an application like Microsoft Word, you click buttons to format words and phrases, and the changes are visible immediately. Markdown isn't like that. When you create a Markdown-formatted file, you add Markdown syntax to the text to indicate which words and phrases should look different.\n>\n> For example, to denote a heading, you add a number sign before it (e.g., `# Heading One`). Or to make a phrase bold, you add two asterisks before and after it (e.g., `**this text is bold**`). It may take a while to get used to seeing Markdown syntax in your text, especially if you're accustomed to WYSIWYG applications.\n\n---\n\n### Handlebars\n\nQuoting from [handlebarsjs.com](https://handlebarsjs.com/guide/):\n\n> Handlebars is a simple templating language.\n>\n> It uses a template and an input object to generate HTML or other text formats. Handlebars templates look like regular text with embedded Handlebars expressions.\n>\n> ```html\n> <p>{{curly \"firstname\"}} {{curly \"lastname\"}}</p>\n> ```\n>\n> A handlebars expression is a double curly bracket, some contents, followed by a set of closing double curly brackets. When the template is executed, these expressions are replaced with values from an input object.\n\n---\n\nFor our cases, we use handlebars to replace specific parts of markdown with the values included in a given howler hit. For example:\n\n```markdown\nThis analytic is **{{curly \"howler.analytic\"}}**\n```\n\nbecomes:\n\n> This analytic is **{{howler.analytic}}**.\n\nFor more information on handlebars, check out:\n\n- [What is Handlebars?](https://handlebarsjs.com/guide/#what-is-handlebars)\n- [Handlebars Expressions](https://handlebarsjs.com/guide/expressions.html)\n\n## Combining Markdown and Handlebars\n\nYou can use handlebars for template replacement throughout your markdown. Below is an example table using handlebars and markdown:\n\n```markdown\n| Source IP | Destination IP |\n| --------------------- | -------------------------- |\n| {{curly \"source.ip\"}} | {{curly \"destination.ip\"}} |\n```\n\nrenders as:\n\n| Source IP | Destination IP |\n| ------------- | ------------------ |\n| {{source.ip}} | {{destination.ip}} |\n\n## Advanced Handlebars\n\nHowler integrates a number of helper functions for you to work with.\n\n### Control Expressions\n\nFor use as subexpressions, we expose a number of conditional checks:\n\n**Equality:**\n\nGiven `howler.status` is {{howler.status}}:\n\n```markdown\n{{curly '#if (equals howler.status \"open\")'}}\nHit is open!\n{{curly \"/if\"}}\n{{curly '#if (equals howler.status \"resolved\")'}}\nHit is resolved!\n{{curly \"/if\"}}\n```\n\n{{#if (equals howler.status \"open\")}}\nHit is open!\n{{/if}}\n{{#if (equals howler.status \"resolved\")}}\nHit is resolved!\n{{/if}}\n\n**AND/OR/NOT:**\n\nGiven `howler.status` is {{howler.status}}, and `howler.escalation` is {{howler.escalation}}:\n\n```markdown\n{{curly '#if (and (equals howler.status \"open\") (equals howler.escalation \"alert\"))'}}\nThis is correct!\n{{curly \"/if\"}}\n{{curly '#if (and (equals howler.status \"resolved\") (equals howler.escalation \"hit\"))'}}\nThis is wrong!\n{{curly \"/if\"}}\n```\n\n{{#if (and (equals howler.status \"open\") (equals howler.escalation \"alert\"))}}\nThis is correct!\n{{/if}}\n{{#if (and (equals howler.status \"resolved\") (equals howler.escalation \"hit\"))}}\nThis is wrong!\n{{/if}}\n\n```markdown\n{{curly '#if (or howler.is_bundle (not howler.is_bundle))'}}\nAlways shows!\n{{curly \"/if\"}}\n```\n\n{{#if (or howler.is_bundle (not howler.is_bundle))}}\nAlways shows!\n{{/if}}\n\n---\n\n### String Operations\n\n**String Concatenation:**\n\n```markdown\n{{curly 'join \"string one \" \"string two\"'}}\n```\n\n{{join \"string one \" \"string two\"}}\n\n**Uppercase/Lowercase:**\n\n```markdown\n{{curly 'upper \"make this uppercase\"'}}\n{{curly 'lower \"MAKE THIS LOWERCASE\"'}}\n```\n\n{{upper \"make this uppercase\"}}\n\n{{lower \"MAKE THIS LOWERCASE\"}}\n\n---\n\n### Fetching Data\n\nYou can also make basic fetch requests for, and parse, JSON data from external sources:\n\n```markdown\n{{curly 'fetch \"/api/v1/configs\" \"api_response.c12nDef.UNRESTRICTED\"'}}\n```\n\n{{fetch \"/api/v1/configs\" \"api_response.c12nDef.UNRESTRICTED\"}}\n\n## Full Helper List\n"
@@ -1 +1 @@
1
- export default "# Cr\u00e9er un aper\u00e7u\n\nLes aper\u00e7us peuvent \u00eatre utilis\u00e9s pour modifier la fa\u00e7on dont les donn\u00e9es sont pr\u00e9sent\u00e9es sur les alertes qui correspondent aux param\u00e8tres de l'aper\u00e7u. Les aper\u00e7us sont, par conception, faciles \u00e0 cr\u00e9er et assez flexibles.\n\n## Premiers pas\n\nLes \u00e9l\u00e9ments de base des aper\u00e7us sont :\n\n1. Markdown\n2. Handlebars\n\nNous allons rapidement expliquer ces \u00e9l\u00e9ments.\n\n### Markdown\n\nCitation de l'excellent [markdownguide.org](https://www.markdownguide.org/getting-started/) :\n\n> Markdown est un langage de balisage l\u00e9ger que vous pouvez utiliser pour ajouter des \u00e9l\u00e9ments de formatage aux documents texte en texte brut. Cr\u00e9\u00e9 par [John Gruber](https://daringfireball.net/projects/markdown/) en 2004, Markdown est maintenant l'un des langages de balisage les plus populaires au monde.\n>\n> L'utilisation de Markdown est diff\u00e9rente de l'utilisation d'un \u00e9diteur [WYSIWYG](https://en.wikipedia.org/wiki/WYSIWYG). Dans une application comme Microsoft Word, vous cliquez sur des boutons pour formater les mots et les phrases, et les changements sont visibles imm\u00e9diatement. Markdown n'est pas comme cela. Lorsque vous cr\u00e9ez un fichier format\u00e9 en Markdown, vous ajoutez une syntaxe Markdown au texte pour indiquer quels mots et phrases doivent appara\u00eetre diff\u00e9remment.\n>\n> Par exemple, pour d\u00e9signer un titre, vous ajoutez un signe di\u00e8se avant celui-ci (par ex., `# Titre Un`). Ou pour mettre une phrase en gras, vous ajoutez deux ast\u00e9risques avant et apr\u00e8s (par ex., `**ce texte est en gras**`). Il peut falloir un certain temps pour s'habituer \u00e0 voir la syntaxe Markdown dans votre texte, surtout si vous \u00eates habitu\u00e9 aux applications WYSIWYG.\n\n---\n\n### Handlebars\n\nCitation de [handlebarsjs.com](https://handlebarsjs.com/guide/) :\n\n> Handlebars est un langage de template simple.\n>\n> Il utilise un template et un objet d'entr\u00e9e pour g\u00e9n\u00e9rer du HTML ou d'autres formats de texte. Les templates Handlebars ressemblent \u00e0 du texte normal avec des expressions Handlebars int\u00e9gr\u00e9es.\n>\n>```html\n> <p>{{curly \"firstname\"}} {{curly \"lastname\"}}</p>\n>```\n>\n> Une expression handlebars est une double accolade, du contenu, suivi d'un ensemble d'accolades fermantes doubles. Lorsque le template est ex\u00e9cut\u00e9, ces expressions sont remplac\u00e9es par des valeurs d'un objet d'entr\u00e9e.\n\n---\n\nDans nos cas, nous utilisons handlebars pour remplacer des parties sp\u00e9cifiques du markdown par les valeurs incluses dans un r\u00e9sultat howler donn\u00e9. Par exemple :\n\n```markdown\nCette analytique est **{{curly \"howler.analytic\"}}**\n```\n\ndevient :\n\n> Cette analytique est **{{howler.analytic}}**.\n\nPour plus d'informations sur handlebars, consultez :\n\n- [Qu'est-ce que Handlebars ?](https://handlebarsjs.com/guide/#what-is-handlebars)\n- [Expressions Handlebars](https://handlebarsjs.com/guide/expressions.html)\n\n## Combiner Markdown et Handlebars\n\nVous pouvez utiliser handlebars pour le remplacement de template dans tout votre markdown. Voici un exemple de tableau utilisant handlebars et markdown :\n\n```markdown\n\n| IP Source | IP Destination |\n| --- | --- |\n| {{curly \"source.ip\"}} |{{curly \"destination.ip\"}} |\n```\n\ns'affiche comme :\n\n| IP Source | IP Destination |\n| --- | --- |\n| {{source.ip}} |{{destination.ip}} |\n\n## Handlebars avanc\u00e9s\n\nHowler int\u00e8gre un certain nombre de fonctions d'aide avec lesquelles vous pouvez travailler.\n\n### Expressions de contr\u00f4le\n\nPour utilisation comme sous-expressions, nous exposons un certain nombre de v\u00e9rifications conditionnelles :\n\n**\u00c9galit\u00e9 :**\n\n\u00c9tant donn\u00e9 que `howler.status` est {{howler.status}} :\n\n```markdown\n{{curly '#if (equals howler.status \"open\")'}}\nLe r\u00e9sultat est ouvert !\n{{curly \"/if\"}}\n{{curly '#if (equals howler.status \"resolved\")'}}\nLe r\u00e9sultat est r\u00e9solu !\n{{curly \"/if\"}}\n```\n\n{{#if (equals howler.status \"open\")}}\nLe r\u00e9sultat est ouvert !\n{{/if}}\n{{#if (equals howler.status \"resolved\")}}\nLe r\u00e9sultat est r\u00e9solu !\n{{/if}}\n\n**ET/OU/NON :**\n\n\u00c9tant donn\u00e9 que `howler.status` est {{howler.status}}, et `howler.escalation` est {{howler.escalation}} :\n\n```markdown\n{{curly '#if (and (equals howler.status \"open\") (equals howler.escalation \"alert\"))'}}\nC'est correct !\n{{curly \"/if\"}}\n{{curly '#if (and (equals howler.status \"resolved\") (equals howler.escalation \"hit\"))'}}\nC'est incorrect !\n{{curly \"/if\"}}\n```\n\n{{#if (and (equals howler.status \"open\") (equals howler.escalation \"alert\"))}}\nC'est correct !\n{{/if}}\n{{#if (and (equals howler.status \"resolved\") (equals howler.escalation \"hit\"))}}\nC'est incorrect !\n{{/if}}\n\n```markdown\n{{curly '#if (or howler.is_bundle (not howler.is_bundle))'}}\nS'affiche toujours !\n{{curly \"/if\"}}\n```\n\n{{#if (or howler.is_bundle (not howler.is_bundle))}}\nS'affiche toujours !\n{{/if}}\n\n---\n\n### Op\u00e9rations sur les cha\u00eenes\n\n**Concat\u00e9nation de cha\u00eenes :**\n\n```markdown\n{{curly 'join \"cha\u00eene une \" \"cha\u00eene deux\"'}}\n```\n\n{{join \"cha\u00eene une \" \"cha\u00eene deux\"}}\n\n**Majuscules/Minuscules :**\n\n```markdown\n{{curly 'upper \"mettre ceci en majuscules\"'}}\n{{curly 'lower \"METTRE CECI EN MINUSCULES\"'}}\n```\n\n{{upper \"mettre ceci en majuscules\"}}\n\n{{lower \"METTRE CECI EN MINUSCULES\"}}\n\n---\n\n### R\u00e9cup\u00e9ration de donn\u00e9es\n\nVous pouvez \u00e9galement faire des requ\u00eates fetch de base pour r\u00e9cup\u00e9rer et analyser des donn\u00e9es JSON de sources externes :\n\n```markdown\n{{curly 'fetch \"/api/v1/configs\" \"api_response.c12nDef.UNRESTRICTED\"'}}\n```\n\n{{fetch \"/api/v1/configs\" \"api_response.c12nDef.UNRESTRICTED\"}}\n\n## Liste compl\u00e8te des assistants\n"
1
+ export default "# Cr\u00e9er un aper\u00e7u\n\nLes aper\u00e7us peuvent \u00eatre utilis\u00e9s pour modifier la fa\u00e7on dont les donn\u00e9es sont pr\u00e9sent\u00e9es sur les alertes qui correspondent aux param\u00e8tres de l'aper\u00e7u. Les aper\u00e7us sont, par conception, faciles \u00e0 cr\u00e9er et assez flexibles.\n\n## Premiers pas\n\nLes \u00e9l\u00e9ments de base des aper\u00e7us sont :\n\n1. Markdown\n2. Handlebars\n\nNous allons rapidement expliquer ces \u00e9l\u00e9ments.\n\n### Markdown\n\nCitation de l'excellent [markdownguide.org](https://www.markdownguide.org/getting-started/) :\n\n> Markdown est un langage de balisage l\u00e9ger que vous pouvez utiliser pour ajouter des \u00e9l\u00e9ments de formatage aux documents texte en texte brut. Cr\u00e9\u00e9 par [John Gruber](https://daringfireball.net/projects/markdown/) en 2004, Markdown est maintenant l'un des langages de balisage les plus populaires au monde.\n>\n> L'utilisation de Markdown est diff\u00e9rente de l'utilisation d'un \u00e9diteur [WYSIWYG](https://en.wikipedia.org/wiki/WYSIWYG). Dans une application comme Microsoft Word, vous cliquez sur des boutons pour formater les mots et les phrases, et les changements sont visibles imm\u00e9diatement. Markdown n'est pas comme cela. Lorsque vous cr\u00e9ez un fichier format\u00e9 en Markdown, vous ajoutez une syntaxe Markdown au texte pour indiquer quels mots et phrases doivent appara\u00eetre diff\u00e9remment.\n>\n> Par exemple, pour d\u00e9signer un titre, vous ajoutez un signe di\u00e8se avant celui-ci (par ex., `# Titre Un`). Ou pour mettre une phrase en gras, vous ajoutez deux ast\u00e9risques avant et apr\u00e8s (par ex., `**ce texte est en gras**`). Il peut falloir un certain temps pour s'habituer \u00e0 voir la syntaxe Markdown dans votre texte, surtout si vous \u00eates habitu\u00e9 aux applications WYSIWYG.\n\n---\n\n### Handlebars\n\nCitation de [handlebarsjs.com](https://handlebarsjs.com/guide/) :\n\n> Handlebars est un langage de template simple.\n>\n> Il utilise un template et un objet d'entr\u00e9e pour g\u00e9n\u00e9rer du HTML ou d'autres formats de texte. Les templates Handlebars ressemblent \u00e0 du texte normal avec des expressions Handlebars int\u00e9gr\u00e9es.\n>\n> ```html\n> <p>{{curly \"firstname\"}} {{curly \"lastname\"}}</p>\n> ```\n>\n> Une expression handlebars est une double accolade, du contenu, suivi d'un ensemble d'accolades fermantes doubles. Lorsque le template est ex\u00e9cut\u00e9, ces expressions sont remplac\u00e9es par des valeurs d'un objet d'entr\u00e9e.\n\n---\n\nDans nos cas, nous utilisons handlebars pour remplacer des parties sp\u00e9cifiques du markdown par les valeurs incluses dans un r\u00e9sultat howler donn\u00e9. Par exemple :\n\n```markdown\nCette analytique est **{{curly \"howler.analytic\"}}**\n```\n\ndevient :\n\n> Cette analytique est **{{howler.analytic}}**.\n\nPour plus d'informations sur handlebars, consultez :\n\n- [Qu'est-ce que Handlebars ?](https://handlebarsjs.com/guide/#what-is-handlebars)\n- [Expressions Handlebars](https://handlebarsjs.com/guide/expressions.html)\n\n## Combiner Markdown et Handlebars\n\nVous pouvez utiliser handlebars pour le remplacement de template dans tout votre markdown. Voici un exemple de tableau utilisant handlebars et markdown :\n\n```markdown\n| IP Source | IP Destination |\n| --------------------- | -------------------------- |\n| {{curly \"source.ip\"}} | {{curly \"destination.ip\"}} |\n```\n\ns'affiche comme :\n\n| IP Source | IP Destination |\n| ------------- | ------------------ |\n| {{source.ip}} | {{destination.ip}} |\n\n## Handlebars avanc\u00e9s\n\nHowler int\u00e8gre un certain nombre de fonctions d'aide avec lesquelles vous pouvez travailler.\n\n### Expressions de contr\u00f4le\n\nPour utilisation comme sous-expressions, nous exposons un certain nombre de v\u00e9rifications conditionnelles :\n\n**\u00c9galit\u00e9 :**\n\n\u00c9tant donn\u00e9 que `howler.status` est {{howler.status}} :\n\n```markdown\n{{curly '#if (equals howler.status \"open\")'}}\nLe r\u00e9sultat est ouvert !\n{{curly \"/if\"}}\n{{curly '#if (equals howler.status \"resolved\")'}}\nLe r\u00e9sultat est r\u00e9solu !\n{{curly \"/if\"}}\n```\n\n{{#if (equals howler.status \"open\")}}\nLe r\u00e9sultat est ouvert !\n{{/if}}\n{{#if (equals howler.status \"resolved\")}}\nLe r\u00e9sultat est r\u00e9solu !\n{{/if}}\n\n**ET/OU/NON :**\n\n\u00c9tant donn\u00e9 que `howler.status` est {{howler.status}}, et `howler.escalation` est {{howler.escalation}} :\n\n```markdown\n{{curly '#if (and (equals howler.status \"open\") (equals howler.escalation \"alert\"))'}}\nC'est correct !\n{{curly \"/if\"}}\n{{curly '#if (and (equals howler.status \"resolved\") (equals howler.escalation \"hit\"))'}}\nC'est incorrect !\n{{curly \"/if\"}}\n```\n\n{{#if (and (equals howler.status \"open\") (equals howler.escalation \"alert\"))}}\nC'est correct !\n{{/if}}\n{{#if (and (equals howler.status \"resolved\") (equals howler.escalation \"hit\"))}}\nC'est incorrect !\n{{/if}}\n\n```markdown\n{{curly '#if (or howler.is_bundle (not howler.is_bundle))'}}\nS'affiche toujours !\n{{curly \"/if\"}}\n```\n\n{{#if (or howler.is_bundle (not howler.is_bundle))}}\nS'affiche toujours !\n{{/if}}\n\n---\n\n### Op\u00e9rations sur les cha\u00eenes\n\n**Concat\u00e9nation de cha\u00eenes :**\n\n```markdown\n{{curly 'join \"cha\u00eene une \" \"cha\u00eene deux\"'}}\n```\n\n{{join \"cha\u00eene une \" \"cha\u00eene deux\"}}\n\n**Majuscules/Minuscules :**\n\n```markdown\n{{curly 'upper \"mettre ceci en majuscules\"'}}\n{{curly 'lower \"METTRE CECI EN MINUSCULES\"'}}\n```\n\n{{upper \"mettre ceci en majuscules\"}}\n\n{{lower \"METTRE CECI EN MINUSCULES\"}}\n\n---\n\n### R\u00e9cup\u00e9ration de donn\u00e9es\n\nVous pouvez \u00e9galement faire des requ\u00eates fetch de base pour r\u00e9cup\u00e9rer et analyser des donn\u00e9es JSON de sources externes :\n\n```markdown\n{{curly 'fetch \"/api/v1/configs\" \"api_response.c12nDef.UNRESTRICTED\"'}}\n```\n\n{{fetch \"/api/v1/configs\" \"api_response.c12nDef.UNRESTRICTED\"}}\n\n## Liste compl\u00e8te des assistants\n"
@@ -331,6 +331,8 @@
331
331
  "outline.assemblyline.tags": "Tags",
332
332
  "overview.search": "Search Hit Data",
333
333
  "owner": "Owner",
334
+ "page.403.description": "You do not have permission to access this page.",
335
+ "page.403.title": "403: Access Forbidden",
334
336
  "page.404.description": "The page you are looking for cannot be found...",
335
337
  "page.404.title": "404: Not found",
336
338
  "page.dashboard.settings.edit": "Edit Dashboard",
@@ -335,6 +335,8 @@
335
335
  "outline.assemblyline.tags": "Tags",
336
336
  "overview.search": "Recherche dans les champs du Hit",
337
337
  "owner": "Propriétaire",
338
+ "page.403.description": "Vous n'avez pas la permission d'accéder à cette page.",
339
+ "page.403.title": "403: Accès interdit",
338
340
  "page.404.description": "La page que vous recherchez est introuvable ...",
339
341
  "page.404.title": "404: Introuvable",
340
342
  "page.dashboard.settings.edit": "Modifier le tableau de bord",
@@ -51,7 +51,7 @@ export interface APILookups {
51
51
  techniques: { [index: string]: { key: string; name: string; url: string } };
52
52
  tactics: { [index: string]: { key: string; name: string; url: string } };
53
53
  icons: string[];
54
- roles: ['admin', 'automation_advanced', 'automation_basic', 'user'];
54
+ roles: ['admin', 'actionrunner_advanced', 'actionrunner_basic', 'automation_advanced', 'automation_basic', 'user'];
55
55
  }
56
56
 
57
57
  export interface APIConfiguration {
package/package.json CHANGED
@@ -75,11 +75,6 @@
75
75
  "!node_modules/url-join"
76
76
  ]
77
77
  },
78
- "lint-staged": {
79
- "src/**/*.{ts,tsx,js,jsx}": [
80
- "prettier --write"
81
- ]
82
- },
83
78
  "name": "@cccsaurora/howler-ui",
84
79
  "overrides": {
85
80
  "handlebars-async-helpers": {
@@ -101,7 +96,7 @@
101
96
  "internal-slot": "1.0.7"
102
97
  },
103
98
  "type": "module",
104
- "version": "2.19.0-dev.830",
99
+ "version": "2.19.0-dev.836",
105
100
  "exports": {
106
101
  "./i18n": "./i18n.js",
107
102
  "./index.css": "./index.css",
@@ -1,5 +1,5 @@
1
- import { MainMenuInsertOperation } from '../plugins/store';
2
1
  import { isNil } from 'lodash-es';
2
+ import { MainMenuInsertOperation } from '../plugins/store';
3
3
  class AppMenuBuilder {
4
4
  items;
5
5
  indexMap;