@cccsaurora/howler-ui 2.17.1 → 2.17.2-dev.649

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.
@@ -66,6 +66,7 @@ import useMySearch from '../hooks/useMySearch';
66
66
  import AppContainer from './AppContainer';
67
67
  import AnalyticProvider from './providers/AnalyticProvider';
68
68
  import ApiConfigProvider, { ApiConfigContext } from './providers/ApiConfigProvider';
69
+ import AppBarProvider from './providers/AppBarProvider';
69
70
  import AvatarProvider from './providers/AvatarProvider';
70
71
  import CustomPluginProvider from './providers/CustomPluginProvider';
71
72
  import FavouriteProvider from './providers/FavouritesProvider';
@@ -150,7 +151,7 @@ const MyAppProvider = ({ children }) => {
150
151
  return (_jsx(ErrorBoundary, { children: _jsx(AppProvider, { preferences: myPreferences, theme: myTheme, sitemap: mySitemap, user: myUser, search: mySearch, children: _jsx(CustomPluginProvider, { children: _jsx(ErrorBoundary, { children: _jsx(ErrorBoundary, { children: _jsx(ViewProvider, { children: _jsx(AvatarProvider, { children: _jsx(ModalProvider, { children: _jsx(FieldProvider, { children: _jsx(LocalStorageProvider, { children: _jsx(SocketProvider, { children: _jsx(HitProvider, { children: _jsx(OverviewProvider, { children: _jsx(AnalyticProvider, { children: _jsx(FavouriteProvider, { children: _jsx(UserListProvider, { children: children }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
151
152
  };
152
153
  const AppProviderWrapper = () => {
153
- return (_jsx(I18nextProvider, { i18n: i18n, defaultNS: "translation", children: _jsx(ApiConfigProvider, { children: _jsx(PluginProvider, { pluginStore: howlerPluginStore.pluginStore, children: _jsxs(MyAppProvider, { children: [_jsx(MyApp, {}), _jsx(Modal, {})] }) }) }) }));
154
+ return (_jsx(I18nextProvider, { i18n: i18n, defaultNS: "translation", children: _jsx(ApiConfigProvider, { children: _jsx(PluginProvider, { pluginStore: howlerPluginStore.pluginStore, children: _jsx(AppBarProvider, { children: _jsxs(MyAppProvider, { children: [_jsx(MyApp, {}), _jsx(Modal, {})] }) }) }) }) }));
154
155
  };
155
156
  const router = createBrowserRouter([
156
157
  {
@@ -0,0 +1,14 @@
1
+ import type { FC, PropsWithChildren, ReactNode } from 'react';
2
+ interface AppBarItem {
3
+ id: string;
4
+ component: ReactNode;
5
+ }
6
+ export interface AppBarContextType {
7
+ leftItems: AppBarItem[];
8
+ rightItems: AppBarItem[];
9
+ addToAppBar: (alignment: 'left' | 'right', id: string, component: ReactNode) => void;
10
+ removeFromAppBar: (id: string) => void;
11
+ }
12
+ export declare const AppBarContext: import("react").Context<AppBarContextType>;
13
+ declare const AppBarProvider: FC<PropsWithChildren>;
14
+ export default AppBarProvider;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useState } from 'react';
3
+ export const AppBarContext = createContext(null);
4
+ const AppBarProvider = ({ children }) => {
5
+ const [leftItems, setLeftItems] = useState([]);
6
+ const [rightItems, setRightItems] = useState([]);
7
+ const addToAppBar = useCallback((alignment, id, component) => {
8
+ const setter = alignment === 'left' ? setLeftItems : setRightItems;
9
+ setter(prev => {
10
+ if (prev.some(item => item.id === id)) {
11
+ return prev;
12
+ }
13
+ return [...prev, { id, component }];
14
+ });
15
+ }, []);
16
+ const removeFromAppBar = useCallback((id) => {
17
+ setLeftItems(prev => prev.filter(item => item.id !== id));
18
+ setRightItems(prev => prev.filter(item => item.id !== id));
19
+ }, []);
20
+ return (_jsx(AppBarContext.Provider, { value: { leftItems, rightItems, addToAppBar, removeFromAppBar }, children: children }));
21
+ };
22
+ export default AppBarProvider;
@@ -5,5 +5,6 @@ export declare const DEFAULT_FIELDS: string[];
5
5
  declare const _default: import("react").NamedExoticComponent<{
6
6
  hit: WithMetadata<Hit>;
7
7
  layout: HitLayout;
8
+ forceAllFields?: boolean;
8
9
  }>;
9
10
  export default _default;
@@ -9,7 +9,7 @@ import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
9
9
  import { HitLayout } from './HitLayout';
10
10
  import DefaultOutline from './outlines/DefaultOutline';
11
11
  export const DEFAULT_FIELDS = ['event.created', 'howler.id', 'howler.hash'];
12
- const HitOutline = ({ hit, layout }) => {
12
+ const HitOutline = ({ hit, layout, forceAllFields = false }) => {
13
13
  const { t } = useTranslation();
14
14
  const { getMatchingTemplate } = useMatchers();
15
15
  const [templateFieldCount] = useMyLocalStorageItem(StorageKey.TEMPLATE_FIELD_COUNT, null);
@@ -23,7 +23,9 @@ const HitOutline = ({ hit, layout }) => {
23
23
  hit,
24
24
  layout,
25
25
  template,
26
- fields: !isNil(templateFieldCount) ? [...template.keys].slice(0, templateFieldCount) : template.keys,
26
+ fields: !isNil(templateFieldCount) && !forceAllFields
27
+ ? [...template.keys].slice(0, templateFieldCount)
28
+ : template.keys,
27
29
  readonly: template.type === 'readonly'
28
30
  });
29
31
  }
@@ -1,14 +1,17 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Api, Article, Book, Code, Dashboard, Description, ExitToApp, FormatListBulleted, Help, HelpCenter, Key, ManageSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, SupervisorAccount, Terminal, Topic } from '@mui/icons-material';
3
+ import { Stack } from '@mui/material';
3
4
  import { AppBrand } from '@cccsaurora/howler-ui/branding/AppBrand';
5
+ import { AppBarContext } from '@cccsaurora/howler-ui/components/app/providers/AppBarProvider';
4
6
  import Classification from '@cccsaurora/howler-ui/components/elements/display/Classification';
5
7
  import DocumentationButton from '@cccsaurora/howler-ui/components/elements/display/DocumentationButton';
6
8
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
7
- import { useMemo } from 'react';
9
+ import { Fragment, useContext, useMemo } from 'react';
8
10
  import AppMenuBuilder from '@cccsaurora/howler-ui/utils/menuUtils';
9
11
  // This is your App Name that will be displayed in the left drawer and the top navbar
10
12
  const APP_NAME = 'howler';
11
13
  const useMyPreferences = () => {
14
+ const { leftItems, rightItems } = useContext(AppBarContext);
12
15
  // The following menu items will show up in the Left Navigation Drawer
13
16
  const MENU_ITEMS = useMemo(() => {
14
17
  let defaultMenu = [
@@ -269,12 +272,12 @@ const useMyPreferences = () => {
269
272
  adminMenuI18nKey: 'adminmenu',
270
273
  quickSearchParam: 'query',
271
274
  quickSearchURI: '/hits',
272
- leftAfterBreadcrumbs: _jsx(DocumentationButton, {}),
273
- rightBeforeSearch: _jsx(Classification, {})
275
+ leftAfterBreadcrumbs: (_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(DocumentationButton, {}), leftItems.map(item => (_jsx(Fragment, { children: item.component }, item.id)))] })),
276
+ rightBeforeSearch: (_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", pr: 1, children: [rightItems.map(item => (_jsx(Fragment, { children: item.component }, item.id))), _jsx(Classification, {})] }))
274
277
  },
275
278
  leftnav: {
276
279
  elements: MENU_ITEMS
277
280
  }
278
- }), [USER_MENU_ITEMS, ADMIN_MENU_ITEMS, MENU_ITEMS]);
281
+ }), [USER_MENU_ITEMS, ADMIN_MENU_ITEMS, MENU_ITEMS, leftItems, rightItems]);
279
282
  };
280
283
  export default useMyPreferences;
@@ -6,6 +6,7 @@ import api from '@cccsaurora/howler-ui/api';
6
6
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
7
7
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
8
8
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
9
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
9
10
  import { isEqual, omit, uniqBy } from 'lodash-es';
10
11
  import { memo, useCallback, useEffect, useMemo, useState } from 'react';
11
12
  import { useTranslation } from 'react-i18next';
@@ -19,6 +20,7 @@ const DossierEditor = () => {
19
20
  const { t, i18n } = useTranslation();
20
21
  const params = useParams();
21
22
  const { dispatchApi } = useMyApi();
23
+ const { showSuccessMessage } = useMySnackbar();
22
24
  const navigate = useNavigate();
23
25
  const [searchParams, setSearchParams] = useSearchParams();
24
26
  const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
@@ -127,16 +129,18 @@ const DossierEditor = () => {
127
129
  try {
128
130
  if (!params.id) {
129
131
  const result = await dispatchApi(api.dossier.post(dossier));
132
+ showSuccessMessage(t('route.dossiers.manager.create.success'));
130
133
  navigate(`/dossiers/${result.dossier_id}/edit`);
131
134
  }
132
135
  else {
133
136
  setDossier(await dispatchApi(api.dossier.put(dossier.dossier_id, omit(dossier, ['dossier_id', 'id']))));
137
+ showSuccessMessage(t('route.dossiers.manager.edit.success'));
134
138
  }
135
139
  }
136
140
  finally {
137
141
  setLoading(false);
138
142
  }
139
- }, [dispatchApi, dossier, navigate, params.id]);
143
+ }, [dispatchApi, dossier, navigate, params.id, showSuccessMessage, t]);
140
144
  useEffect(() => {
141
145
  if (!params.id) {
142
146
  return;
@@ -163,7 +163,7 @@ const InformationPane = ({ onClose }) => {
163
163
  const hasError = useMemo(() => !validateRegex(filter), [filter]);
164
164
  return (_jsxs(VSBox, { top: 10, sx: { height: '100%', flex: 1 }, children: [_jsxs(Stack, { direction: "column", flex: 1, sx: { overflowY: 'auto', flexGrow: 1 }, position: "relative", spacing: 1, ml: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, flexShrink: 0, pr: 2, sx: [hit?.howler?.is_bundle && { position: 'absolute', top: 1, right: 0, zIndex: 1100 }], children: [_jsx(FlexOne, {}), onClose && !location.pathname.startsWith('/bundles') && (_jsx(TuiIconButton, { size: "small", onClick: onClose, tooltip: t('hit.panel.details.exit'), children: _jsx(Clear, {}) })), _jsx(SocketBadge, { size: "small" }), analytic && (_jsx(TuiIconButton, { size: "small", tooltip: t('hit.panel.analytic.open'), disabled: !analytic || loading, route: `/analytics/${analytic.analytic_id}`, children: _jsx(QueryStats, {}) })), hit?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles, disabled: loading }), !!hit && !hit.howler.is_bundle && (_jsx(TuiIconButton, { tooltip: t('hit.panel.open'), href: `/hits/${selected}`, disabled: !hit || loading, size: "small", target: "_blank", children: _jsx(OpenInNew, {}) }))] }), _jsx(Box, { pr: 2, children: header }), !!hit &&
165
165
  !hit.howler.is_bundle &&
166
- (!loading ? (_jsxs(_Fragment, { children: [_jsx(HitOutline, { hit: hit, layout: HitLayout.DENSE }), _jsx(HitLabels, { hit: hit })] })) : (_jsx(Skeleton, { height: 124 }))), _jsx(HitLinks, { hit: hit, analytic: analytic, dossiers: dossiers }), _jsxs(VSBoxHeader, { ml: -1, mr: -1, pb: 1, sx: { top: '0px' }, children: [_jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: {
166
+ (!loading ? (_jsxs(_Fragment, { children: [_jsx(HitOutline, { hit: hit, layout: HitLayout.DENSE, forceAllFields: true }), _jsx(HitLabels, { hit: hit })] })) : (_jsx(Skeleton, { height: 124 }))), _jsx(HitLinks, { hit: hit, analytic: analytic, dossiers: dossiers }), _jsxs(VSBoxHeader, { ml: -1, mr: -1, pb: 1, sx: { top: '0px' }, children: [_jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: {
167
167
  display: 'flex',
168
168
  flexDirection: 'row',
169
169
  pr: 2,
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ArrowDropDown, List, Settings, TableChart, ViewComfy, ViewCompact, ViewModule } from '@mui/icons-material';
3
- import { FormLabel, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material';
2
+ import { ArrowDropDown, InfoOutlined, List, Settings, TableChart, ViewComfy, ViewCompact, ViewModule } from '@mui/icons-material';
3
+ import { Checkbox, Divider, FormLabel, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip } from '@mui/material';
4
4
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
5
5
  import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
6
6
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
@@ -13,6 +13,12 @@ const LayoutSettings = () => {
13
13
  const displayType = useContextSelector(HitSearchContext, ctx => ctx.displayType);
14
14
  const setDisplayType = useContextSelector(HitSearchContext, ctx => ctx.setDisplayType);
15
15
  const [hitLayout, setHitLayout] = useMyLocalStorageItem(StorageKey.HIT_LAYOUT, false);
16
- return (_jsx(ChipPopper, { icon: _jsx(Settings, {}), deleteIcon: _jsx(ArrowDropDown, {}), toggleOnDelete: true, disablePortal: false, slotProps: { chip: { size: 'medium' } }, placement: "bottom-end", children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", alignItems: "center", justifyContent: "space-between", spacing: 1, children: [_jsx(FormLabel, { children: t('page.settings.local.hits.display_type') }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", children: [_jsx(ToggleButton, { value: "list", children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(List, {}), _jsx("span", { children: t('page.settings.local.hits.display_type.list') })] }) }), _jsx(ToggleButton, { value: "grid", children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(TableChart, {}), _jsx("span", { children: t('page.settings.local.hits.display_type.grid') })] }) })] })] }), _jsxs(Stack, { direction: "row", alignItems: "center", justifyContent: "space-between", spacing: 1, children: [_jsx(FormLabel, { children: t('page.settings.local.hits.layout') }), _jsxs(ToggleButtonGroup, { exclusive: true, size: "small", value: hitLayout, onChange: (_, value) => setHitLayout(value), children: [_jsx(ToggleButton, { value: HitLayout.DENSE, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewCompact, {}), _jsx("span", { children: t('page.settings.local.hits.layout.dense') })] }) }), _jsx(ToggleButton, { value: HitLayout.NORMAL, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewModule, {}), _jsx("span", { children: t('page.settings.local.hits.layout.normal') })] }) }), _jsx(ToggleButton, { value: HitLayout.COMFY, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewComfy, {}), _jsx("span", { children: t('page.settings.local.hits.layout.comfy') })] }) })] })] })] }) }));
16
+ const [templateFieldCount, setTemplateFieldCount] = useMyLocalStorageItem(StorageKey.TEMPLATE_FIELD_COUNT, null);
17
+ return (_jsx(ChipPopper, { icon: _jsx(Tooltip, { title: t('search.layout.settings'), children: _jsx(Settings, {}) }), deleteIcon: _jsx(ArrowDropDown, {}), toggleOnDelete: true, disablePortal: false, slotProps: { chip: { size: 'medium', 'aria-label': t('search.layout.settings') } }, placement: "bottom-end", children: _jsxs(Stack, { spacing: 1, alignItems: "start", children: [_jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", alignSelf: "stretch", children: [_jsx(FormLabel, { id: "display_type", children: t('page.settings.local.hits.display_type') }), _jsx("div", { style: { flex: 1 } }), _jsx(Tooltip, { title: t('page.settings.local.hits.display_type.description'), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", "aria-labelledby": "display_type", children: [_jsx(ToggleButton, { value: "list", children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(List, {}), _jsx("span", { children: t('page.settings.local.hits.display_type.list') })] }) }), _jsx(ToggleButton, { value: "grid", children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(TableChart, {}), _jsx("span", { children: t('page.settings.local.hits.display_type.grid') })] }) })] }), _jsx(Divider, { flexItem: true }), _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", alignSelf: "stretch", children: [_jsx(FormLabel, { id: "layout", children: t('page.settings.local.hits.layout') }), _jsx("div", { style: { flex: 1 } }), _jsx(Tooltip, { title: t('page.settings.local.hits.layout.description'), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }), _jsxs(ToggleButtonGroup, { exclusive: true, size: "small", value: hitLayout, onChange: (_, value) => setHitLayout(value), "aria-labelledby": "layout", children: [_jsx(ToggleButton, { value: HitLayout.DENSE, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewCompact, {}), _jsx("span", { children: t('page.settings.local.hits.layout.dense') })] }) }), _jsx(ToggleButton, { value: HitLayout.NORMAL, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewModule, {}), _jsx("span", { children: t('page.settings.local.hits.layout.normal') })] }) }), _jsx(ToggleButton, { value: HitLayout.COMFY, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewComfy, {}), _jsx("span", { children: t('page.settings.local.hits.layout.comfy') })] }) })] }), _jsx(Divider, { flexItem: true }), _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", alignSelf: "stretch", children: [_jsx(FormLabel, { id: "field_count", children: t('page.settings.local.hits.field_count') }), _jsx("div", { style: { flex: 1 } }), _jsx(Tooltip, { title: t('page.settings.local.hits.field_count.description'), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }), _jsxs(Stack, { direction: "row", spacing: 0.5, alignSelf: "stretch", children: [_jsx(Checkbox, { checked: templateFieldCount !== null, onChange: (_, checked) => setTemplateFieldCount(checked ? 3 : null), size: "small" }), _jsx(TextField, { type: "number", size: "small", disabled: templateFieldCount === null, value: templateFieldCount ?? 3, fullWidth: true, onChange: e => {
18
+ const val = parseInt(e.target.value);
19
+ if (!isNaN(val)) {
20
+ setTemplateFieldCount(Math.min(15, Math.max(0, val)));
21
+ }
22
+ }, inputProps: { min: 0, max: 15, 'aria-labelledby': 'field_count' } })] })] }) }));
17
23
  };
18
24
  export default LayoutSettings;
@@ -1,7 +1,4 @@
1
1
  declare const _default: import("react").NamedExoticComponent<{
2
- open: boolean;
3
- onClose: () => void;
4
- anchorEl: HTMLElement;
5
2
  addColumn: (key: string) => void;
6
3
  columns: string[];
7
4
  }>;
@@ -1,14 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Add, Check } from '@mui/icons-material';
3
- import { Autocomplete, Chip, Divider, Grid, IconButton, Popover, Stack, TextField } from '@mui/material';
2
+ import { Add, Check, Settings, TableChart } from '@mui/icons-material';
3
+ import { Autocomplete, Chip, Divider, Grid, IconButton, Stack, TextField } from '@mui/material';
4
4
  import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
5
5
  import { FieldContext } from '@cccsaurora/howler-ui/components/app/providers/FieldProvider';
6
6
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
7
+ import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
7
8
  import { has, sortBy, uniq } from 'lodash-es';
8
9
  import { memo, useContext, useEffect, useMemo, useState } from 'react';
9
10
  import { useTranslation } from 'react-i18next';
10
11
  import { useContextSelector } from 'use-context-selector';
11
- const AddColumnModal = ({ open, onClose, anchorEl, addColumn, columns }) => {
12
+ const AddColumnModal = ({ addColumn, columns }) => {
12
13
  const { t } = useTranslation();
13
14
  const { hitFields } = useContext(FieldContext);
14
15
  const response = useContextSelector(HitSearchContext, ctx => ctx.response);
@@ -21,7 +22,7 @@ const AddColumnModal = ({ open, onClose, anchorEl, addColumn, columns }) => {
21
22
  setSuggestions(uniq((await Promise.all((response?.items ?? []).map(async (_hit) => (has(_hit, '__template') ? _hit.__template?.keys : (await getMatchingTemplate(_hit))?.keys) ?? []))).flat()));
22
23
  })();
23
24
  }, [getMatchingTemplate, response?.items]);
24
- return (_jsx(Popover, { open: open, onClose: onClose, anchorEl: anchorEl, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, children: _jsxs(Stack, { spacing: 1, p: 1, width: "500px", children: [_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Autocomplete, { sx: { flex: 1 }, size: "small", options: options, value: columnToAdd, renderInput: params => _jsx(TextField, { fullWidth: true, placeholder: t('hit.fields'), ...params }), onChange: (_ev, value) => setColumnToAdd(value) }), _jsx(IconButton, { disabled: !columnToAdd, onClick: () => {
25
+ return (_jsx(ChipPopper, { icon: _jsx(TableChart, {}), deleteIcon: _jsx(Settings, {}), toggleOnDelete: true, slotProps: { chip: { size: 'small' } }, children: _jsxs(Stack, { spacing: 1, p: 1, width: "500px", children: [_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Autocomplete, { sx: { flex: 1 }, size: "small", options: options, value: columnToAdd, renderInput: params => _jsx(TextField, { fullWidth: true, placeholder: t('hit.fields'), ...params }), onChange: (_ev, value) => setColumnToAdd(value) }), _jsx(IconButton, { disabled: !columnToAdd, onClick: () => {
25
26
  addColumn(columnToAdd);
26
27
  setColumnToAdd(null);
27
28
  }, children: _jsx(Add, {}) })] }), _jsx(Divider, { orientation: "horizontal" }), _jsx(Grid, { container: true, spacing: 1, children: sortBy(suggestions.map(key => ({ key, used: columns.includes(key) })), 'used').map(({ key, used }) => {
@@ -1,18 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { DndContext, KeyboardSensor, PointerSensor, pointerWithin, useSensor, useSensors } from '@dnd-kit/core';
3
3
  import { arrayMove, SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
4
- import { Add, FormatIndentDecrease, FormatIndentIncrease, Info, List, Search, TableChart } from '@mui/icons-material';
4
+ import { FormatIndentDecrease, FormatIndentIncrease, Info, List, Search, TableChart } from '@mui/icons-material';
5
5
  import { IconButton, LinearProgress, Paper, Stack, Table, TableBody, TableCell, TableHead, TableRow, ToggleButton, ToggleButtonGroup, Typography, useTheme } from '@mui/material';
6
6
  import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
7
7
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
8
8
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
9
9
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
10
- import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
11
10
  import SearchTotal from '@cccsaurora/howler-ui/components/elements/addons/search/SearchTotal';
12
11
  import DevelopmentBanner from '@cccsaurora/howler-ui/components/elements/display/features/DevelopmentBanner';
13
- import DevelopmentIcon from '@cccsaurora/howler-ui/components/elements/display/features/DevelopmentIcon';
14
12
  import useHitSelection from '@cccsaurora/howler-ui/components/hooks/useHitSelection';
15
13
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
14
+ import { uniq } from 'lodash-es';
16
15
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
17
16
  import { useTranslation } from 'react-i18next';
18
17
  import { useContextSelector } from 'use-context-selector';
@@ -38,16 +37,14 @@ const HitGrid = () => {
38
37
  const query = useContextSelector(ParameterContext, ctx => ctx.query);
39
38
  const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
40
39
  const [collapseMainColumn, setCollapseMainColumn] = useMyLocalStorageItem(StorageKey.GRID_COLLAPSE_COLUMN, false);
41
- const [analyticIds, setAnalyticIds] = useState({});
42
- const columnModalRef = useRef();
43
- const [columns, setColumns] = useState([
40
+ const [columns, setColumns] = useMyLocalStorageItem(StorageKey.GRID_COLUMNS, [
44
41
  'howler.outline.threat',
45
42
  'howler.outline.target',
46
43
  'howler.outline.indicators',
47
44
  'howler.outline.summary'
48
45
  ]);
49
- const [columnWidths, setColumnWidths] = useState({});
50
- const [showAddColumn, setShowAddColumn] = useState(false);
46
+ const [columnWidths, setColumnWidths] = useMyLocalStorageItem(StorageKey.GRID_COLUMN_WIDTHS, {});
47
+ const [analyticIds, setAnalyticIds] = useState({});
51
48
  const resizingCol = useRef();
52
49
  const showSelectBar = useMemo(() => {
53
50
  if (selectedHits.length > 1) {
@@ -78,10 +75,10 @@ const HitGrid = () => {
78
75
  }, []);
79
76
  const onMouseUp = useCallback(() => {
80
77
  const [col, element] = resizingCol.current;
81
- setColumnWidths(_widths => ({
82
- ..._widths,
78
+ setColumnWidths({
79
+ ...columnWidths,
83
80
  [col]: element.style.width
84
- }));
81
+ });
85
82
  element.style.width = null;
86
83
  element.style.maxWidth = null;
87
84
  document.querySelectorAll(`.col-${col.replaceAll('.', '-')}`).forEach(el => {
@@ -90,7 +87,7 @@ const HitGrid = () => {
90
87
  });
91
88
  window.removeEventListener('mousemove', onMouseMove);
92
89
  window.removeEventListener('mouseup', onMouseUp);
93
- }, [onMouseMove]);
90
+ }, [columnWidths, onMouseMove, setColumnWidths]);
94
91
  const onMouseDown = useCallback((col, event) => {
95
92
  event.stopPropagation();
96
93
  event.preventDefault();
@@ -106,12 +103,12 @@ const HitGrid = () => {
106
103
  }, [query, search]);
107
104
  const handleDragEnd = useCallback((event) => {
108
105
  const { active, over } = event;
109
- if (active.id !== over.id) {
106
+ if (over && active.id !== over.id) {
110
107
  const oldIndex = (columns ?? []).findIndex(entry => entry === active.id);
111
108
  const newIndex = (columns ?? []).findIndex(entry => entry === over.id);
112
109
  setColumns(arrayMove(columns, oldIndex, newIndex));
113
110
  }
114
- }, [columns]);
111
+ }, [columns, setColumns]);
115
112
  const getSelectedId = useCallback((event) => {
116
113
  const target = event.target;
117
114
  const selectedElement = target.closest('[id]');
@@ -120,14 +117,14 @@ const HitGrid = () => {
120
117
  }
121
118
  return selectedElement.id;
122
119
  }, []);
123
- return (_jsxs(Stack, { spacing: 1, p: 2, width: "100%", sx: { overflow: 'hidden', height: `calc(100vh - ${theme.spacing(showSelectBar ? 13 : 8)})` }, children: [_jsx(DevelopmentBanner, {}), _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5, textAlign: 'left' }, variant: "body2", children: t('hit.search.prompt') }), _jsx(DevelopmentIcon, {})] }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Stack, { position: "relative", flex: 1, children: [_jsx(HitQuery, { searching: searching, triggerSearch: search, compact: true }), searching && (_jsx(LinearProgress, { sx: {
120
+ return (_jsxs(Stack, { spacing: 1, p: 2, width: "100%", sx: { overflow: 'hidden', height: `calc(100vh - ${theme.spacing(showSelectBar ? 13 : 8)})` }, children: [_jsx(DevelopmentBanner, {}), _jsxs(Stack, { direction: "row", justifyContent: "space-between", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5, textAlign: 'left' }, variant: "body2", children: t('hit.search.prompt') }), response && (_jsx(SearchTotal, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", offset: response.offset, pageLength: response.rows, total: response.total }))] }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Stack, { position: "relative", flex: 1, children: [_jsx(HitQuery, { searching: searching, triggerSearch: search, compact: true }), searching && (_jsx(LinearProgress, { sx: {
124
121
  position: 'absolute',
125
122
  left: 0,
126
123
  right: 0,
127
124
  bottom: 0,
128
125
  borderBottomLeftRadius: theme.shape.borderRadius,
129
126
  borderBottomRightRadius: theme.shape.borderRadius
130
- } }))] }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", children: [_jsx(ToggleButton, { value: "list", children: _jsx(List, {}) }), _jsx(ToggleButton, { value: "grid", children: _jsx(TableChart, {}) })] })] }), _jsxs(Stack, { direction: "row", spacing: 1, width: "100%", sx: { '& > *': { flex: 1 } }, children: [_jsx(QuerySettings, {}), response && (_jsx(SearchTotal, { sx: { alignSelf: 'center' }, color: "text.secondary", offset: response.offset, pageLength: response.rows, total: response.total })), _jsxs(Stack, { direction: "row", children: [_jsx(FlexOne, {}), _jsx(IconButton, { ref: columnModalRef, onClick: () => setShowAddColumn(true), children: _jsx(Add, { fontSize: "small" }) })] }), _jsx(AddColumnModal, { anchorEl: columnModalRef.current, open: showAddColumn, onClose: () => setShowAddColumn(false), columns: columns, addColumn: key => setColumns(_columns => [..._columns, key]) })] }), _jsxs(Stack, { component: Paper, spacing: 1, width: "100%", height: "100%", sx: { overflow: 'auto', flex: 1 }, onScroll: onScroll, children: [_jsxs(Table, { sx: { '& td,th': { px: 1, py: 0.25, whiteSpace: 'nowrap' } }, children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { sx: {
127
+ } }))] }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", children: [_jsx(ToggleButton, { value: "list", children: _jsx(List, {}) }), _jsx(ToggleButton, { value: "grid", children: _jsx(TableChart, {}) })] })] }), _jsxs(Stack, { direction: "row", spacing: 1, width: "100%", alignItems: "center", children: [_jsx(QuerySettings, { boxSx: { flex: 1 } }), _jsx(AddColumnModal, { columns: columns, addColumn: key => setColumns(uniq([...columns, key])) })] }), _jsxs(Stack, { component: Paper, spacing: 1, width: "100%", height: "100%", sx: { overflow: 'auto', flex: 1 }, onScroll: onScroll, children: [_jsxs(Table, { sx: { '& td,th': { px: 1, py: 0.25, whiteSpace: 'nowrap' } }, children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { sx: {
131
128
  borderRight: 'thin solid',
132
129
  borderRightColor: 'divider'
133
130
  }, children: _jsx(IconButton, { onClick: () => setCollapseMainColumn(!collapseMainColumn), children: collapseMainColumn ? (_jsx(FormatIndentIncrease, { fontSize: "small" })) : (_jsx(FormatIndentDecrease, { fontSize: "small" })) }) }), _jsx(DndContext, { sensors: sensors, collisionDetection: pointerWithin, onDragEnd: handleDragEnd, children: _jsx(SortableContext, { items: columns, children: columns.map(col => (_jsx(ColumnHeader, { col: col, width: columnWidths[col], onMouseDown: onMouseDown, setColumns: setColumns }, col))) }) }), _jsx(TableCell, { sx: { width: '100%' } })] }) }), _jsxs(HitContextMenu, { Component: TableBody, getSelectedId: getSelectedId, children: [response?.items.map(hit => (_jsx(HitRow, { hit: hit, analyticIds: analyticIds, columns: columns, columnWidths: columnWidths, collapseMainColumn: collapseMainColumn, onClick: onClick }, hit.howler.id))), _jsx(TableRow, { children: _jsx(TableCell, { colSpan: columns.length + 2, children: _jsx(Stack, { alignItems: "center", justifyContent: "center", py: 0.5, px: 1, children: _jsx(IconButton, { onClick: () => search(query, true), children: _jsx(Search, {}) }) }) }) })] })] }), (response?.total ?? 0) < 1 && (_jsx(Stack, { direction: "row", spacing: 1, alignItems: "center", p: 1, justifyContent: "center", flex: 1, children: _jsxs(Typography, { variant: "h3", color: "text.secondary", display: "flex", flexDirection: "row", alignItems: "center", children: [_jsx(Info, { fontSize: "inherit", sx: { color: 'text.secondary', mr: 1 } }), _jsx("span", { children: t('app.list.empty') })] }) }))] })] }));
@@ -128,7 +128,7 @@ const HitViewer = () => {
128
128
  display: 'flex',
129
129
  '& > .MuiPaper-root': { flex: 1 },
130
130
  mr: orientation === 'vertical' ? 0 : -2
131
- }, children: [_jsx(HowlerCard, { tabIndex: 0, sx: { position: 'relative' }, children: _jsxs(CardContent, { children: [_jsx(HitBanner, { hit: hit, layout: HitLayout.COMFY, useListener: true }), _jsx(HitOutline, { hit: hit, layout: HitLayout.COMFY }), _jsx(HitLabels, { hit: hit }), _jsx(HitLinks, { hit: hit, analytic: analytic, dossiers: dossiers })] }) }), !isUnderLg && (_jsxs(Stack, { spacing: 1, sx: {
131
+ }, children: [_jsx(HowlerCard, { tabIndex: 0, sx: { position: 'relative' }, children: _jsxs(CardContent, { children: [_jsx(HitBanner, { hit: hit, layout: HitLayout.COMFY, useListener: true }), _jsx(HitOutline, { hit: hit, layout: HitLayout.COMFY, forceAllFields: true }), _jsx(HitLabels, { hit: hit }), _jsx(HitLinks, { hit: hit, analytic: analytic, dossiers: dossiers })] }) }), !isUnderLg && (_jsxs(Stack, { spacing: 1, sx: {
132
132
  position: 'absolute',
133
133
  top: theme.spacing(2),
134
134
  right: theme.spacing(-6)
@@ -0,0 +1,13 @@
1
+ import { type FC } from 'react';
2
+ interface HomeSettingsProps {
3
+ /** Whether the dashboard is currently in edit mode. Disables the "Edit" menu item. */
4
+ isEditing: boolean;
5
+ /** Current auto-refresh interval in seconds. */
6
+ refreshRate: number;
7
+ /** Called when the user selects a new refresh rate. */
8
+ onRefreshRateChange: (rate: number) => void;
9
+ /** Called when the user clicks the "Edit" menu item. */
10
+ onEdit: () => void;
11
+ }
12
+ declare const HomeSettings: FC<HomeSettingsProps>;
13
+ export default HomeSettings;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Edit, Settings } from '@mui/icons-material';
3
+ import { FormControl, FormLabel, IconButton, ListItemIcon, Menu, MenuItem, Slider, Tooltip } from '@mui/material';
4
+ import { useState } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ const REFRESH_RATES = [15, 30, 60, 300];
7
+ const HomeSettings = ({ isEditing, refreshRate, onRefreshRateChange, onEdit }) => {
8
+ const { t } = useTranslation();
9
+ const [anchorEl, setAnchorEl] = useState(null);
10
+ return (_jsxs(_Fragment, { children: [_jsx(Tooltip, { title: t('page.dashboard.settings.edit'), children: _jsx(IconButton, { onClick: e => setAnchorEl(e.currentTarget), size: "small", children: _jsx(Settings, { color: "primary" }) }) }), _jsxs(Menu, { id: "settings-menu", anchorEl: anchorEl, open: !!anchorEl, onClose: () => setAnchorEl(null), children: [_jsxs(MenuItem, { disabled: isEditing, onClick: () => {
11
+ setAnchorEl(null);
12
+ onEdit();
13
+ }, children: [_jsx(ListItemIcon, { children: _jsx(Edit, {}) }), t('page.dashboard.settings.edit')] }), _jsx(MenuItem, { disableRipple: true, disableTouchRipple: true, sx: { '&:hover': { bgcolor: 'transparent' }, cursor: 'default' }, children: _jsxs(FormControl, { sx: { px: 2, py: 1, minWidth: 250, pointerEvents: 'auto' }, children: [_jsx(FormLabel, { id: "refresh-rate-label", sx: { mb: 2 }, children: t('page.dashboard.settings.refreshRate') }), _jsx(Slider, { "aria-labelledby": "refresh-rate-label", value: REFRESH_RATES.indexOf(refreshRate), onChange: (_, value) => onRefreshRateChange(REFRESH_RATES[value]), step: 1, marks: [
14
+ { value: 0, label: '15s' },
15
+ { value: 1, label: '30s' },
16
+ { value: 2, label: '1m' },
17
+ { value: 3, label: '5m' }
18
+ ], min: 0, max: 3, valueLabelDisplay: "auto", valueLabelFormat: value => {
19
+ const rates = ['15s', '30s', '1m', '5m'];
20
+ return rates[value] || '';
21
+ } })] }) })] })] }));
22
+ };
23
+ export default HomeSettings;
@@ -11,8 +11,9 @@ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
11
11
  import HitContextMenu from '@cccsaurora/howler-ui/components/routes/hits/search/HitContextMenu';
12
12
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
13
  import { useTranslation } from 'react-i18next';
14
- import { useNavigate } from 'react-router-dom';
14
+ import { Link, useNavigate } from 'react-router-dom';
15
15
  import { useContextSelector } from 'use-context-selector';
16
+ import { buildViewUrl } from '@cccsaurora/howler-ui/utils/viewUtils';
16
17
  // Custom hook to select hits by IDs with proper memoization
17
18
  const useSelectHitsByIds = (hitIds) => {
18
19
  const hitIdsRef = useRef(hitIds);
@@ -150,6 +151,6 @@ const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
150
151
  }
151
152
  return selectedElement.id;
152
153
  }, []);
153
- return (_jsx(Card, { variant: "outlined", sx: { height: '100%' }, children: _jsxs(Stack, { spacing: 1, sx: { p: 1, minHeight: 100 }, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", children: t(view?.title) || _jsx(Skeleton, { variant: "text", height: "2em", width: "100px" }) }), _jsx(IconButton, { size: "small", onClick: () => onClick(view.query), children: _jsx(OpenInNew, { fontSize: "small" }) })] }), loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { height: 150, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 160, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 140, width: "100%", variant: "rounded" })] })) : hits.length > 0 ? (_jsx(HitContextMenu, { getSelectedId: getSelectedId, children: hits.map(h => (_jsx(Card, { id: h.howler.id, variant: "outlined", sx: { cursor: 'pointer' }, onClick: () => navigate((h.howler.is_bundle ? '/bundles/' : '/hits/') + h.howler.id), children: _jsx(CardContent, { children: _jsx(HitBanner, { layout: HitLayout.DENSE, hit: h }) }) }, h.howler.id))) })) : (_jsx(AppListEmpty, {}))] }) }));
154
+ return (_jsx(Card, { variant: "outlined", sx: { height: '100%' }, children: _jsxs(Stack, { spacing: 1, sx: { p: 1, minHeight: 100 }, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", children: t(view?.title) || _jsx(Skeleton, { variant: "text", height: "2em", width: "100px" }) }), _jsx(IconButton, { size: "small", component: Link, disabled: !view, to: view ? buildViewUrl(view) : '', onClick: () => onClick(view.query), children: _jsx(OpenInNew, { fontSize: "small" }) })] }), loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { height: 150, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 160, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 140, width: "100%", variant: "rounded" })] })) : hits.length > 0 ? (_jsx(HitContextMenu, { getSelectedId: getSelectedId, children: hits.map(h => (_jsx(Card, { id: h.howler.id, variant: "outlined", sx: { cursor: 'pointer' }, onClick: () => navigate((h.howler.is_bundle ? '/bundles/' : '/hits/') + h.howler.id), children: _jsx(CardContent, { children: _jsx(HitBanner, { layout: HitLayout.DENSE, hit: h }) }) }, h.howler.id))) })) : (_jsx(AppListEmpty, {}))] }) }));
154
155
  };
155
156
  export default ViewCard;
@@ -52,7 +52,7 @@ const ViewRefresh = forwardRef(({ refreshRate, viewCardCount, onRefresh }, ref)
52
52
  clearTimeout(timerRef.current);
53
53
  };
54
54
  }, [progress, isRefreshing, refreshRate, triggerRefresh]);
55
- return (_jsxs(Box, { sx: { position: 'relative', display: 'inline-flex' }, children: [isRefreshing ? (_jsx(CircularProgress, { variant: "indeterminate" })) : (_jsx(CircularProgress, { variant: "determinate", value: progress })), _jsx(Box, { sx: {
55
+ return (_jsxs(Box, { sx: { position: 'relative', display: 'inline-flex' }, children: [isRefreshing ? (_jsx(CircularProgress, { variant: "indeterminate", size: 32 })) : (_jsx(CircularProgress, { variant: "determinate", value: progress, size: 32 })), _jsx(Box, { sx: {
56
56
  top: 0,
57
57
  left: 0,
58
58
  bottom: 0,
@@ -61,7 +61,7 @@ const ViewRefresh = forwardRef(({ refreshRate, viewCardCount, onRefresh }, ref)
61
61
  display: 'flex',
62
62
  alignItems: 'center',
63
63
  justifyContent: 'center'
64
- }, children: _jsx(Tooltip, { title: t('refresh'), children: _jsx(IconButton, { onClick: triggerRefresh, disabled: isRefreshing, color: "primary", children: _jsx(Refresh, {}) }) }) })] }));
64
+ }, children: _jsx(Tooltip, { title: t('refresh'), children: _jsx(IconButton, { onClick: triggerRefresh, disabled: isRefreshing, color: "primary", size: "small", children: _jsx(Refresh, {}) }) }) })] }));
65
65
  });
66
66
  ViewRefresh.displayName = 'ViewRefresh';
67
67
  export default ViewRefresh;
@@ -1,18 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors } from '@dnd-kit/core';
3
3
  import { SortableContext, arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
4
- import { Cancel, Check, Close, Edit, MoreVert, OpenInNew } from '@mui/icons-material';
5
- import { Alert, AlertTitle, CircularProgress, FormControl, FormLabel, Grid, IconButton, ListItemIcon, Menu, MenuItem, Slider, Stack, Typography } from '@mui/material';
4
+ import { Cancel, Check, Close, OpenInNew } from '@mui/icons-material';
5
+ import { Alert, AlertTitle, CircularProgress, Grid, IconButton, Stack, Typography } from '@mui/material';
6
6
  import api from '@cccsaurora/howler-ui/api';
7
7
  import { AppBrand } from '@cccsaurora/howler-ui/branding/AppBrand';
8
8
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
9
9
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
10
+ import { AppBarContext } from '@cccsaurora/howler-ui/components/app/providers/AppBarProvider';
10
11
  import CustomButton from '@cccsaurora/howler-ui/components/elements/addons/buttons/CustomButton';
11
12
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
12
13
  import useMyUserFunctions from '@cccsaurora/howler-ui/components/hooks/useMyUserFunctions';
13
14
  import dayjs from 'dayjs';
14
15
  import isEqual from 'lodash-es/isEqual';
15
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
16
+ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
16
17
  import { useTranslation } from 'react-i18next';
17
18
  import { Link } from 'react-router-dom';
18
19
  import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
@@ -20,13 +21,14 @@ import ErrorBoundary from '../ErrorBoundary';
20
21
  import AddNewCard from './AddNewCard';
21
22
  import AnalyticCard, {} from './AnalyticCard';
22
23
  import EntryWrapper from './EntryWrapper';
24
+ import HomeSettings from './HomeSettings';
23
25
  import ViewCard, {} from './ViewCard';
24
26
  import ViewRefresh, {} from './ViewRefresh';
25
27
  const LUCENE_DATE_FMT = 'YYYY-MM-DD[T]HH:mm:ss';
26
- const REFRESH_RATES = [15, 30, 60, 300];
27
28
  const Home = () => {
28
29
  const { t } = useTranslation();
29
30
  const { user, setUser } = useAppUser();
31
+ const { addToAppBar, removeFromAppBar } = useContext(AppBarContext);
30
32
  const { setDashboard, setRefreshRate: setRefreshRateBackend } = useMyUserFunctions();
31
33
  const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
32
34
  const [lastViewed, setLastViewed] = useMyLocalStorageItem(StorageKey.LAST_VIEW, dayjs().utc().format(LUCENE_DATE_FMT));
@@ -34,7 +36,6 @@ const Home = () => {
34
36
  const [isEditing, setIsEditing] = useState(false);
35
37
  const [updatedHitTotal, setUpdatedHitTotal] = useState(0);
36
38
  const [dashboard, setStateDashboard] = useState(user.dashboard ?? []);
37
- const [openSettings, setOpenSettings] = useState(null);
38
39
  const [refreshRate, setRefreshRate] = useState(user.refresh_rate ?? 15);
39
40
  const [refreshTick, setRefreshTick] = useState(null);
40
41
  const viewRefreshRef = useRef(null);
@@ -75,9 +76,6 @@ const Home = () => {
75
76
  const handleRefresh = useCallback(() => {
76
77
  setRefreshTick(Symbol());
77
78
  }, []);
78
- const handleOpenSettings = (event) => {
79
- setOpenSettings(event.currentTarget);
80
- };
81
79
  const saveChanges = useCallback(async () => {
82
80
  setLoading(true);
83
81
  try {
@@ -119,18 +117,15 @@ const Home = () => {
119
117
  };
120
118
  }, []);
121
119
  const viewCardCount = useMemo(() => (dashboard ?? []).filter(e => e.type === 'view').length, [dashboard]);
122
- return (_jsx(PageCenter, { maxWidth: "100%", textAlign: "left", height: "100%", children: _jsx(ErrorBoundary, { children: _jsxs(Stack, { direction: "column", spacing: 1, sx: { height: '100%' }, children: [_jsxs(Stack, { direction: "row", justifyContent: "end", spacing: 1, children: [isEditing && (_jsx(CustomButton, { variant: "outlined", size: "small", color: "error", startIcon: _jsx(Cancel, {}), onClick: discardChanges, children: t('cancel') })), isEditing && (_jsx(CustomButton, { variant: "outlined", size: "small", disabled: isEqual(dashboard, user.dashboard), color: 'success', startIcon: loading ? _jsx(CircularProgress, { size: 20 }) : _jsx(Check, {}), onClick: saveChanges, children: t('save') })), _jsx(ViewRefresh, { ref: viewRefreshRef, refreshRate: refreshRate, viewCardCount: viewCardCount, onRefresh: handleRefresh }), _jsx(IconButton, { onClick: handleOpenSettings, children: _jsx(MoreVert, {}) }), _jsxs(Menu, { id: "settings-menu", anchorEl: openSettings, open: !!openSettings, onClose: () => setOpenSettings(null), children: [_jsxs(MenuItem, { disabled: isEditing, onClick: () => {
123
- setOpenSettings(null);
124
- setIsEditing(true);
125
- }, children: [_jsx(ListItemIcon, { children: _jsx(Edit, {}) }), t('page.dashboard.settings.edit')] }), _jsx(MenuItem, { disableRipple: true, disableTouchRipple: true, sx: { '&:hover': { bgcolor: 'transparent' }, cursor: 'default' }, children: _jsxs(FormControl, { sx: { px: 2, py: 1, minWidth: 250, pointerEvents: 'auto' }, children: [_jsx(FormLabel, { id: "refresh-rate-label", sx: { mb: 2 }, children: t('page.dashboard.settings.refreshRate') }), _jsx(Slider, { "aria-labelledby": "refresh-rate-label", value: REFRESH_RATES.indexOf(refreshRate), onChange: (_, value) => handleRefreshRateChange(REFRESH_RATES[value]), step: 1, marks: [
126
- { value: 0, label: '15s' },
127
- { value: 1, label: '30s' },
128
- { value: 2, label: '1m' },
129
- { value: 3, label: '5m' }
130
- ], min: 0, max: 3, valueLabelDisplay: "auto", valueLabelFormat: value => {
131
- const rates = ['15s', '30s', '1m', '5m'];
132
- return rates[value] || '';
133
- } })] }) })] })] }), updatedHitTotal > 0 && (_jsxs(Alert, { severity: "info", variant: "outlined", action: _jsxs(Stack, { spacing: 1, direction: "row", children: [_jsx(IconButton, { color: "info", component: Link, to: `/hits?query=${encodeURIComponent(updateQuery)}`, onClick: () => setLastViewed(dayjs().utc().format(LUCENE_DATE_FMT)), children: _jsx(OpenInNew, {}) }), _jsx(IconButton, { color: "info", onClick: () => {
120
+ useEffect(() => {
121
+ addToAppBar('left', 'view_refresh', _jsx(ViewRefresh, { ref: viewRefreshRef, refreshRate: refreshRate, viewCardCount: viewCardCount, onRefresh: handleRefresh }));
122
+ addToAppBar('left', 'home_settings', _jsx(HomeSettings, { isEditing: isEditing, refreshRate: refreshRate, onRefreshRateChange: handleRefreshRateChange, onEdit: () => setIsEditing(true) }));
123
+ return () => {
124
+ removeFromAppBar('view_refresh');
125
+ removeFromAppBar('home_settings');
126
+ };
127
+ }, [addToAppBar, handleRefresh, handleRefreshRateChange, isEditing, refreshRate, removeFromAppBar, viewCardCount]);
128
+ return (_jsx(PageCenter, { maxWidth: "100%", textAlign: "left", height: "100%", children: _jsx(ErrorBoundary, { children: _jsxs(Stack, { direction: "column", spacing: 1, sx: { height: '100%' }, children: [_jsxs(Stack, { direction: "row", justifyContent: "end", spacing: 1, children: [isEditing && (_jsx(CustomButton, { variant: "outlined", size: "small", color: "error", startIcon: _jsx(Cancel, {}), onClick: discardChanges, children: t('cancel') })), isEditing && (_jsx(CustomButton, { variant: "outlined", size: "small", disabled: isEqual(dashboard, user.dashboard), color: 'success', startIcon: loading ? _jsx(CircularProgress, { size: 20 }) : _jsx(Check, {}), onClick: saveChanges, children: t('save') }))] }), updatedHitTotal > 0 && (_jsxs(Alert, { severity: "info", variant: "outlined", action: _jsxs(Stack, { spacing: 1, direction: "row", children: [_jsx(IconButton, { color: "info", component: Link, to: `/hits?query=${encodeURIComponent(updateQuery)}`, onClick: () => setLastViewed(dayjs().utc().format(LUCENE_DATE_FMT)), children: _jsx(OpenInNew, {}) }), _jsx(IconButton, { color: "info", onClick: () => {
134
129
  setLastViewed(dayjs().utc().format(LUCENE_DATE_FMT));
135
130
  setUpdatedHitTotal(0);
136
131
  }, children: _jsx(Close, {}) })] }), children: [_jsx(AlertTitle, { children: t('route.home.alert.updated.title') }), t('route.home.alert.updated.description', { count: updatedHitTotal })] })), _jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: _jsx(SortableContext, { items: (dashboard ?? []).map(entry => getIdFromEntry(entry)), children: _jsxs(Grid, { container: true, spacing: 1, alignItems: "stretch", sx: [
@@ -534,8 +534,10 @@
534
534
  "route.dossiers.create": "New Dossier",
535
535
  "route.dossiers.default": "Default",
536
536
  "route.dossiers.edit": "Edit Dossier",
537
+ "route.dossiers.manager.create.success": "Dossier Created.",
537
538
  "route.dossiers.manager.delete": "Delete Dossier",
538
539
  "route.dossiers.manager.delete.success": "Dossier Removed.",
540
+ "route.dossiers.manager.edit.success": "Dossier Updated.",
539
541
  "route.dossiers.manager.field.query": "Query",
540
542
  "route.dossiers.manager.field.title": "Title",
541
543
  "route.dossiers.manager.field.type": "Type",
@@ -539,8 +539,10 @@
539
539
  "route.dossiers.create": "Nouveau dossier",
540
540
  "route.dossiers.default": "Défaut",
541
541
  "route.dossiers.edit": "Modifier le dossier",
542
+ "route.dossiers.manager.create.success": "Dossier crée.",
542
543
  "route.dossiers.manager.delete": "Supprimer un dossier",
543
544
  "route.dossiers.manager.delete.success": "Dossier supprimé.",
545
+ "route.dossiers.manager.edit.success": "Dossier mise à jour.",
544
546
  "route.dossiers.manager.field.query": "Requête",
545
547
  "route.dossiers.manager.field.title": "Titre",
546
548
  "route.dossiers.manager.field.type": "Type",
@@ -635,7 +637,7 @@
635
637
  "route.home.add.visualization.status.description": "Quel est l'état des hits créés au cours des trois derniers mois ?",
636
638
  "route.home.add.visualization.unavailable": "Aucune option de visualisation disponible.",
637
639
  "route.home.alert.updated.description": "Des mises à jour ont été apportées aux alertes auxquelles vous participez. Depuis votre dernière consultation, {{count}} alertes ont été mises à jour",
638
- "route.home.alert.updated.title": "Alert Updates",
640
+ "route.home.alert.updated.title": "Mises à jour d'alert",
639
641
  "route.home.description": "Bienvenue sur Howler. Vous pouvez ajouter des éléments à votre tableau de bord en utilisant les boutons ci-dessus.",
640
642
  "route.home.title": "Howler",
641
643
  "route.integrations": "Intégrations",
package/package.json CHANGED
@@ -101,7 +101,7 @@
101
101
  "internal-slot": "1.0.7"
102
102
  },
103
103
  "type": "module",
104
- "version": "2.17.1",
104
+ "version": "2.17.2-dev.649",
105
105
  "exports": {
106
106
  "./i18n": "./i18n.js",
107
107
  "./index.css": "./index.css",
@@ -55,6 +55,8 @@ export declare enum StorageKey {
55
55
  SEARCH_PANE_WIDTH = "search_pane_width",
56
56
  TEMPLATE_FIELD_COUNT = "template_field_count",
57
57
  GRID_COLLAPSE_COLUMN = "grid_collapse_column",
58
+ GRID_COLUMNS = "grid_columns",
59
+ GRID_COLUMN_WIDTHS = "grid_column_widths",
58
60
  QUERY_HISTORY = "query_history",
59
61
  LOGIN_NONCE = "login_nonce",
60
62
  DISPLAY_TYPE = "display_type"
@@ -60,6 +60,8 @@ export var StorageKey;
60
60
  StorageKey["SEARCH_PANE_WIDTH"] = "search_pane_width";
61
61
  StorageKey["TEMPLATE_FIELD_COUNT"] = "template_field_count";
62
62
  StorageKey["GRID_COLLAPSE_COLUMN"] = "grid_collapse_column";
63
+ StorageKey["GRID_COLUMNS"] = "grid_columns";
64
+ StorageKey["GRID_COLUMN_WIDTHS"] = "grid_column_widths";
63
65
  StorageKey["QUERY_HISTORY"] = "query_history";
64
66
  StorageKey["LOGIN_NONCE"] = "login_nonce";
65
67
  StorageKey["DISPLAY_TYPE"] = "display_type";