@cccsaurora/howler-ui 2.19.0-dev.922 → 2.19.0-dev.938

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.
@@ -1,3 +1,4 @@
1
+ import 'dayjs/locale/fr-ca';
1
2
  import { type FC } from 'react';
2
3
  declare const App: FC;
3
4
  export default App;
@@ -51,14 +51,16 @@ import Templates from '@cccsaurora/howler-ui/components/routes/templates/Templat
51
51
  import ViewComposer from '@cccsaurora/howler-ui/components/routes/views/ViewComposer';
52
52
  import Views from '@cccsaurora/howler-ui/components/routes/views/Views';
53
53
  import dayjs from 'dayjs';
54
+ import 'dayjs/locale/fr-ca';
54
55
  import duration from 'dayjs/plugin/duration';
55
56
  import localizedFormat from 'dayjs/plugin/localizedFormat';
57
+ import minMax from 'dayjs/plugin/minMax';
56
58
  import relativeTime from 'dayjs/plugin/relativeTime';
57
59
  import utc from 'dayjs/plugin/utc';
58
60
  import i18n from '@cccsaurora/howler-ui/i18n';
59
61
  import * as monaco from 'monaco-editor';
60
62
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
61
- import { useContext, useEffect, useMemo } from 'react';
63
+ import { useCallback, useContext, useEffect, useMemo } from 'react';
62
64
  import { I18nextProvider } from 'react-i18next';
63
65
  import { PluginProvider, usePluginStore } from 'react-pluggable';
64
66
  import { createBrowserRouter, Outlet, RouterProvider, useLocation, useNavigate } from 'react-router-dom';
@@ -84,6 +86,8 @@ dayjs.extend(utc);
84
86
  dayjs.extend(duration);
85
87
  dayjs.extend(relativeTime);
86
88
  dayjs.extend(localizedFormat);
89
+ dayjs.extend(minMax);
90
+ dayjs.locale(i18n.language === 'en' ? 'en' : 'fr-ca');
87
91
  loader.config({ monaco });
88
92
  const RoleRoute = ({ roles }) => {
89
93
  const appUser = useAppUser();
@@ -105,6 +109,13 @@ const MyApp = () => {
105
109
  const { setItems } = useAppSwitcher();
106
110
  const { get, set, remove } = useMyLocalStorage();
107
111
  const pluginStore = usePluginStore();
112
+ const onLanguageChange = useCallback((language) => dayjs.locale(language === 'en' ? 'en' : 'fr-ca'), []);
113
+ useEffect(() => {
114
+ i18n.on('languageChanged', onLanguageChange);
115
+ return () => {
116
+ i18n.off('languageChanged', onLanguageChange);
117
+ };
118
+ }, [onLanguageChange]);
108
119
  // Simulate app loading time...
109
120
  // e.g. fetching initial app data, etc.
110
121
  useEffect(() => {
@@ -26,10 +26,6 @@ const FavouriteProvider = ({ children }) => {
26
26
  if (favourites.length < 1) {
27
27
  return null;
28
28
  }
29
- // The favourite list is fully represented, skip
30
- if (favourites.length === viewElement?.element?.items?.length) {
31
- return viewElement;
32
- }
33
29
  const savedViews = await fetchViews(favourites);
34
30
  const items = sortBy(savedViews, 'title')
35
31
  .filter(view => !!view)
@@ -10,6 +10,7 @@ const HowlerAvatar = ({ userId, ...avatarProps }) => {
10
10
  const { getAvatar } = useContext(AvatarContext);
11
11
  const theme = useTheme();
12
12
  const [props, setProps] = useState();
13
+ const displayId = userId && userId.toLowerCase() !== 'unassigned' ? userId : t('app.drawer.hit.assignment.unassigned.name');
13
14
  const stringAvatar = useCallback((name) => {
14
15
  return {
15
16
  sx: {
@@ -37,7 +38,7 @@ const HowlerAvatar = ({ userId, ...avatarProps }) => {
37
38
  // eslint-disable-next-line react-hooks/exhaustive-deps
38
39
  }, [userId]);
39
40
  if (userId) {
40
- return (_jsx(Tooltip, { title: userId, children: _jsx(Avatar, { "aria-label": userId, ...avatarProps, ...props, sx: { ...(avatarProps?.sx || {}), ...(props?.sx || {}) } }) }));
41
+ return (_jsx(Tooltip, { title: displayId, children: _jsx(Avatar, { "aria-label": displayId, ...avatarProps, ...props, sx: { ...(avatarProps?.sx || {}), ...(props?.sx || {}) } }) }));
41
42
  }
42
43
  else {
43
44
  return (_jsx(Avatar, { "aria-label": t('unknown'), ...avatarProps, ...props, sx: { ...(avatarProps?.sx || {}), ...(props?.sx || {}) } }));
@@ -276,7 +276,8 @@ const useMyPreferences = () => {
276
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, {})] }))
277
277
  },
278
278
  leftnav: {
279
- elements: MENU_ITEMS
279
+ elements: MENU_ITEMS,
280
+ width: 280
280
281
  }
281
282
  }), [USER_MENU_ITEMS, ADMIN_MENU_ITEMS, MENU_ITEMS, leftItems, rightItems]);
282
283
  };
@@ -63,7 +63,7 @@ const ActionDetails = () => {
63
63
  user.roles.includes('admin') ||
64
64
  user.roles.includes('actionrunner_basic') ||
65
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
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('button.delete') })), execRoles && (_jsx(Button, { startIcon: _jsx(PlayCircleOutline, {}), size: "small", variant: "outlined", color: "success", onClick: () => executeAction(action?.action_id), children: t('route.actions.execute') })), ((action?.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Edit, {}), size: "small", variant: "outlined", component: Link, to: `/action/${params.id}/edit`, children: t('route.actions.edit') }))] }), user.roles.includes('automation_advanced') && (_jsx(FormGroup, { children: _jsx(Stack, { direction: "row", spacing: 1, children: action?.operations
67
67
  ?.map(a => (operations ?? []).find(_action => _action.id === a.operation_id)?.triggers ?? [])
68
68
  .reduce((acc, triggers) => acc.filter(_t => triggers.includes(_t)))
69
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 &&
@@ -115,7 +115,7 @@ const AnalyticDetails = () => {
115
115
  borderRadius: theme.shape.borderRadius,
116
116
  border: `thin solid ${theme.palette.divider}`,
117
117
  width: '100%'
118
- }, children: analytic.rule_crontab }))] }), (analytic?.owner === user.username || user.roles.includes('admin')) && (_jsx(Tooltip, { title: editingInterval ? t('rule.interval.save') : t('rule.interval.edit'), children: _jsx(IconButton, { disabled: intervalLoading, sx: { alignSelf: 'end' }, onClick: onEdit, children: intervalLoading ? _jsx(CircularProgress, { size: 20 }) : editingInterval ? _jsx(Check, {}) : _jsx(Edit, {}) }) }))] }))] }), _jsxs(Grid, { container: true, children: [_jsx(Grid, { item: true, xs: 12, md: 9, children: _jsxs(Tabs, { value: tab, onChange: (_, _tab) => setTab(_tab), children: [_jsx(Tab, { label: t('route.analytics.tab.overview'), value: "overview" }), _jsx(Tab, { label: t('route.analytics.tab.comments'), value: "comments" }), _jsx(Tab, { label: t('route.analytics.tab.hit_comments'), value: "hit_comments" }), _jsx(Tab, { label: t('route.analytics.tab.templates'), value: "templates" }), _jsx(Tab, { label: t('route.analytics.tab.overviews'), value: "overviews" }), analytic?.rule && _jsx(Tab, { label: t('route.analytics.tab.rule'), value: "rule" }), config?.configuration.features.notebook && (_jsx(Tab, { label: t('route.analytics.tab.notebooks'), value: "notebooks" })), _jsx(Tab, { label: t('route.analytics.tab.triage'), value: "triage" })] }) }), ['comments', 'hit_comments'].includes(tab) && (_jsx(Grid, { item: true, xs: 12, md: 3, children: _jsx(Autocomplete, { options: analytic?.detections ?? [], renderInput: param => _jsx(TextField, { ...param, label: "Detection" }), value: filter, onChange: (_, v) => setFilter(v) }) }))] }), {
118
+ }, children: analytic.rule_crontab }))] }), (analytic?.owner === user.username || user.roles.includes('admin')) && (_jsx(Tooltip, { title: editingInterval ? t('rule.interval.save') : t('rule.interval.edit'), children: _jsx(IconButton, { disabled: intervalLoading, sx: { alignSelf: 'end' }, onClick: onEdit, children: intervalLoading ? _jsx(CircularProgress, { size: 20 }) : editingInterval ? _jsx(Check, {}) : _jsx(Edit, {}) }) }))] }))] }), _jsxs(Grid, { container: true, children: [_jsx(Grid, { item: true, xs: 12, md: 9, children: _jsxs(Tabs, { value: tab, onChange: (_, _tab) => setTab(_tab), children: [_jsx(Tab, { label: t('route.analytics.tab.overview'), value: "overview" }), _jsx(Tab, { label: t('route.analytics.tab.comments'), value: "comments" }), _jsx(Tab, { label: t('route.analytics.tab.hit_comments'), value: "hit_comments" }), _jsx(Tab, { label: t('route.analytics.tab.templates'), value: "templates" }), _jsx(Tab, { label: t('route.analytics.tab.overviews'), value: "overviews" }), analytic?.rule && _jsx(Tab, { label: t('route.analytics.tab.rule'), value: "rule" }), config?.configuration.features.notebook && (_jsx(Tab, { label: t('route.analytics.tab.notebooks'), value: "notebooks" })), _jsx(Tab, { label: t('route.analytics.tab.triage'), value: "triage" })] }) }), ['comments', 'hit_comments'].includes(tab) && (_jsx(Grid, { item: true, xs: 12, md: 3, children: _jsx(Autocomplete, { options: analytic?.detections ?? [], renderInput: param => _jsx(TextField, { ...param, label: t('route.analytics.dropdown.detection') }), value: filter, onChange: (_, v) => setFilter(v) }) }))] }), {
119
119
  comments: _jsx(AnalyticComments, { analytic: analytic, setAnalytic: setAnalytic }),
120
120
  hit_comments: _jsx(AnalyticHitComments, { analytic: analytic }),
121
121
  overview: _jsx(AnalyticOverview, { analytic: analytic, setAnalytic: setAnalytic }),
@@ -11,16 +11,16 @@ import { useContextSelector } from 'use-context-selector';
11
11
  const CustomSpan = () => {
12
12
  const { t } = useTranslation();
13
13
  const span = useContextSelector(ParameterContext, ctx => ctx.span);
14
- const defaultStartDate = dayjs().subtract(2, 'days');
15
- const defaultEndDate = dayjs().subtract(1, 'day');
16
- const startDate = useContextSelector(ParameterContext, ctx => ctx.startDate ? dayjs(ctx.startDate) : defaultStartDate);
17
14
  const setCustomSpan = useContextSelector(ParameterContext, ctx => ctx.setCustomSpan);
18
- const endDate = useContextSelector(ParameterContext, ctx => (ctx.endDate ? dayjs(ctx.endDate) : defaultEndDate));
15
+ const startDate = useContextSelector(ParameterContext, ctx => (ctx.startDate ? dayjs(ctx.startDate) : null));
16
+ const endDate = useContextSelector(ParameterContext, ctx => (ctx.endDate ? dayjs(ctx.endDate) : null));
19
17
  useEffect(() => {
20
- if (span?.endsWith('custom')) {
21
- setCustomSpan(startDate.toISOString(), endDate.toISOString());
18
+ if (span?.endsWith('custom') && (!startDate || !endDate)) {
19
+ const _startDate = startDate ?? dayjs().subtract(2, 'day');
20
+ const _endDate = endDate ?? dayjs().subtract(1, 'day');
21
+ setCustomSpan(dayjs.min(_startDate, _endDate).toISOString(), dayjs.max(_startDate, _endDate).toISOString());
22
22
  }
23
23
  }, [endDate, setCustomSpan, span, startDate]);
24
- return span?.endsWith('custom') ? (_jsx(LocalizationProvider, { dateAdapter: AdapterDayjs, children: _jsxs(Stack, { direction: "row", spacing: 1, useFlexGap: true, flexWrap: "wrap", children: [_jsx(DateTimePicker, { sx: { minWidth: '175px', flexGrow: 1, marginTop: 1 }, slotProps: { textField: { size: 'small' } }, label: t('date.select.start'), value: startDate ? dayjs(startDate) : dayjs().subtract(1, 'days'), maxDate: endDate, onChange: (newStartDate) => setCustomSpan(newStartDate.toISOString(), endDate.toISOString()), ampm: false, disableFuture: true }), _jsx(DateTimePicker, { sx: { minWidth: '175px', flexGrow: 1, marginTop: 1 }, slotProps: { textField: { size: 'small' } }, label: t('date.select.end'), value: endDate, minDate: startDate, onChange: (newEndDate) => setCustomSpan(startDate.toISOString(), newEndDate.toISOString()), ampm: false, disableFuture: true })] }) })) : null;
24
+ return span?.endsWith('custom') ? (_jsx(LocalizationProvider, { dateAdapter: AdapterDayjs, children: _jsxs(Stack, { direction: "row", spacing: 1, useFlexGap: true, flexWrap: "wrap", children: [_jsx(DateTimePicker, { sx: { minWidth: '175px', flexGrow: 1, marginTop: 1 }, slotProps: { textField: { size: 'small' } }, label: t('date.select.start'), value: startDate ? dayjs(startDate) : dayjs().subtract(1, 'days'), maxDateTime: endDate, onChange: (newStartDate) => setCustomSpan(newStartDate.toISOString(), endDate.toISOString()), ampm: false, disableFuture: true }), _jsx(DateTimePicker, { sx: { minWidth: '175px', flexGrow: 1, marginTop: 1 }, slotProps: { textField: { size: 'small' } }, label: t('date.select.end'), value: endDate, minDateTime: startDate, onChange: (newEndDate) => setCustomSpan(startDate.toISOString(), newEndDate.toISOString()), ampm: false, disableFuture: true })] }) })) : null;
25
25
  };
26
26
  export default memo(CustomSpan);
@@ -0,0 +1,170 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from 'react';
3
+ vi.mock('use-context-selector', async () => {
4
+ const actual = await vi.importActual('use-context-selector');
5
+ return {
6
+ ...actual,
7
+ createContext,
8
+ useContextSelector: (_context, selector) => selector(useContext(_context))
9
+ };
10
+ });
11
+ vi.mock('react-i18next', () => ({
12
+ useTranslation: () => ({ t: (key) => key })
13
+ }));
14
+ import { render, screen, waitFor } from '@testing-library/react';
15
+ import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
16
+ import dayjs from 'dayjs';
17
+ import minMax from 'dayjs/plugin/minMax';
18
+ import {} from 'react';
19
+ dayjs.extend(minMax);
20
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
21
+ // Import after mocks
22
+ import CustomSpan from './CustomSpan';
23
+ const mockSetCustomSpan = vi.fn();
24
+ const defaultCtx = {
25
+ span: 'date.range.custom',
26
+ startDate: undefined,
27
+ endDate: undefined,
28
+ setCustomSpan: mockSetCustomSpan
29
+ };
30
+ const makeWrapper = (ctx) => function ({ children }) {
31
+ return (_jsx(ParameterContext.Provider, { value: { ...defaultCtx, ...ctx }, children: children }));
32
+ };
33
+ describe('CustomSpan', () => {
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+ });
37
+ describe('useEffect – date initialisation', () => {
38
+ it('should set both dates to defaults when both are null', async () => {
39
+ render(_jsx(CustomSpan, {}), {
40
+ wrapper: makeWrapper({ span: 'date.range.custom', startDate: undefined, endDate: undefined })
41
+ });
42
+ await waitFor(() => expect(mockSetCustomSpan).toHaveBeenCalledOnce());
43
+ const [start, end] = mockSetCustomSpan.mock.calls[0];
44
+ const startDate = dayjs(start);
45
+ const endDate = dayjs(end);
46
+ expect(startDate.isBefore(endDate)).toBe(true);
47
+ expect(dayjs().diff(startDate, 'hour')).toBeGreaterThanOrEqual(47);
48
+ expect(dayjs().diff(startDate, 'hour')).toBeLessThanOrEqual(49);
49
+ expect(dayjs().diff(endDate, 'hour')).toBeGreaterThanOrEqual(23);
50
+ expect(dayjs().diff(endDate, 'hour')).toBeLessThanOrEqual(25);
51
+ });
52
+ it('should set default start date when only endDate is provided', async () => {
53
+ // Use a recent end date that's after the default start (now - 2 days)
54
+ const existingEnd = dayjs().subtract(1, 'hour').toISOString();
55
+ render(_jsx(CustomSpan, {}), {
56
+ wrapper: makeWrapper({ span: 'date.range.custom', startDate: undefined, endDate: existingEnd })
57
+ });
58
+ await waitFor(() => expect(mockSetCustomSpan).toHaveBeenCalledOnce());
59
+ const [start, end] = mockSetCustomSpan.mock.calls[0];
60
+ expect(end).toBe(existingEnd);
61
+ expect(dayjs(start).isBefore(dayjs(end))).toBe(true);
62
+ });
63
+ it('should set default end date when only startDate is provided', async () => {
64
+ // Use a recent start date that's before the default end (now - 1 day)
65
+ const existingStart = dayjs().subtract(3, 'day').toISOString();
66
+ render(_jsx(CustomSpan, {}), {
67
+ wrapper: makeWrapper({ span: 'date.range.custom', startDate: existingStart, endDate: undefined })
68
+ });
69
+ await waitFor(() => expect(mockSetCustomSpan).toHaveBeenCalledOnce());
70
+ const [start, end] = mockSetCustomSpan.mock.calls[0];
71
+ expect(start).toBe(existingStart);
72
+ expect(dayjs(end).isAfter(dayjs(start))).toBe(true);
73
+ });
74
+ it('should NOT call setCustomSpan when both dates are already set', () => {
75
+ render(_jsx(CustomSpan, {}), {
76
+ wrapper: makeWrapper({
77
+ span: 'date.range.custom',
78
+ startDate: dayjs().subtract(3, 'day').toISOString(),
79
+ endDate: dayjs().subtract(1, 'hour').toISOString()
80
+ })
81
+ });
82
+ expect(mockSetCustomSpan).not.toHaveBeenCalled();
83
+ });
84
+ it('should NOT call setCustomSpan when span is not custom', () => {
85
+ render(_jsx(CustomSpan, {}), {
86
+ wrapper: makeWrapper({ span: 'date.range.1.month', startDate: undefined, endDate: undefined })
87
+ });
88
+ expect(mockSetCustomSpan).not.toHaveBeenCalled();
89
+ });
90
+ it('should NOT call setCustomSpan when span is undefined', () => {
91
+ render(_jsx(CustomSpan, {}), {
92
+ wrapper: makeWrapper({ span: undefined, startDate: undefined, endDate: undefined })
93
+ });
94
+ expect(mockSetCustomSpan).not.toHaveBeenCalled();
95
+ });
96
+ it('should ensure startDate is before endDate even if existing startDate is after default endDate', async () => {
97
+ const futureStart = dayjs().add(1, 'hour').toISOString();
98
+ render(_jsx(CustomSpan, {}), {
99
+ wrapper: makeWrapper({ span: 'date.range.custom', startDate: futureStart, endDate: undefined })
100
+ });
101
+ await waitFor(() => expect(mockSetCustomSpan).toHaveBeenCalledOnce());
102
+ const [start, end] = mockSetCustomSpan.mock.calls[0];
103
+ expect(dayjs(start).isBefore(dayjs(end))).toBe(true);
104
+ });
105
+ });
106
+ describe('rendering', () => {
107
+ it('should render nothing when span is not custom', () => {
108
+ const { container } = render(_jsx(CustomSpan, {}), {
109
+ wrapper: makeWrapper({ span: 'date.range.1.month' })
110
+ });
111
+ expect(container.innerHTML).toBe('');
112
+ });
113
+ it('should render nothing when span is undefined', () => {
114
+ const { container } = render(_jsx(CustomSpan, {}), {
115
+ wrapper: makeWrapper({ span: undefined })
116
+ });
117
+ expect(container.innerHTML).toBe('');
118
+ });
119
+ it('should render two date pickers when span is custom', () => {
120
+ render(_jsx(CustomSpan, {}), {
121
+ wrapper: makeWrapper({
122
+ span: 'date.range.custom',
123
+ startDate: dayjs().subtract(3, 'day').toISOString(),
124
+ endDate: dayjs().subtract(1, 'hour').toISOString()
125
+ })
126
+ });
127
+ expect(screen.getByLabelText('date.select.start')).toBeInTheDocument();
128
+ expect(screen.getByLabelText('date.select.end')).toBeInTheDocument();
129
+ });
130
+ it('should render pickers for any span ending with "custom"', () => {
131
+ render(_jsx(CustomSpan, {}), {
132
+ wrapper: makeWrapper({
133
+ span: 'my.prefix.custom',
134
+ startDate: dayjs().subtract(3, 'day').toISOString(),
135
+ endDate: dayjs().subtract(1, 'hour').toISOString()
136
+ })
137
+ });
138
+ expect(screen.getByLabelText('date.select.start')).toBeInTheDocument();
139
+ expect(screen.getByLabelText('date.select.end')).toBeInTheDocument();
140
+ });
141
+ });
142
+ describe('onChange handlers', () => {
143
+ it('should not call setCustomSpan when both dates are present on render', () => {
144
+ render(_jsx(CustomSpan, {}), {
145
+ wrapper: makeWrapper({
146
+ span: 'date.range.custom',
147
+ startDate: dayjs().subtract(3, 'day').toISOString(),
148
+ endDate: dayjs().subtract(1, 'hour').toISOString()
149
+ })
150
+ });
151
+ const startPicker = screen.getByLabelText('date.select.start');
152
+ expect(startPicker).toBeInTheDocument();
153
+ expect(mockSetCustomSpan).not.toHaveBeenCalled();
154
+ });
155
+ it('should render start picker with fallback value when startDate is null', async () => {
156
+ render(_jsx(CustomSpan, {}), {
157
+ wrapper: makeWrapper({
158
+ span: 'date.range.custom',
159
+ startDate: undefined,
160
+ endDate: dayjs().subtract(1, 'hour').toISOString()
161
+ })
162
+ });
163
+ // When startDate is null, the picker's value falls back to dayjs().subtract(1, 'days')
164
+ const startInput = screen.getByLabelText('date.select.start');
165
+ expect(startInput).toBeInTheDocument();
166
+ // The input should have a value (the fallback), not be empty
167
+ expect(startInput.value).not.toBe('');
168
+ });
169
+ });
170
+ });
@@ -1,5 +1,7 @@
1
1
  {
2
2
  "*": "All values",
3
+ "Protected B": "Protected B",
4
+ "Unclassified//Official Use Only": "Unclassified//Official Use Only",
3
5
  "actions.error": "Action \"{{action}}\" had error(s): {{messages}}",
4
6
  "actions.running": "Action \"{{action}}\" is executing.",
5
7
  "actions.skipped": "Action \"{{action}}\" was skipped: {{messages}}",
@@ -190,6 +192,7 @@
190
192
  "hit.label.edit.add.error.duplicate": "Duplicated label not allowed",
191
193
  "hit.label.edit.add.error.empty": "Can't add an empty label",
192
194
  "hit.label.edit.add.label": "New label value",
195
+ "hit.label.edit.desc": "Add or remove labels",
193
196
  "hit.notebook.confirm.dialog": "A notebook with that name already exists in your environment, do you wish to overwrite it?",
194
197
  "hit.notebook.confirm.title": "Overwrite existing notebook?",
195
198
  "hit.notebook.error.failToPost": "Failed to send notebook to Jupyterhub, make sure your user environment is running.",
@@ -259,6 +262,7 @@
259
262
  "hit.summary.subtitle": "Limited to a maximum of 10 000 hits.",
260
263
  "hit.summary.title": "Summary of Hits Over Time",
261
264
  "hit.summary.zoom.reset": "Reset Zoom",
265
+ "hit.view.overview": "Overview",
262
266
  "hit.viewer.aggregate": "Summary",
263
267
  "hit.viewer.comments": "Comments",
264
268
  "hit.viewer.data": "Raw Data",
@@ -421,6 +425,7 @@
421
425
  "personalization.showbreadcrumbs": "Show Breadcrumbs",
422
426
  "personalization.sticky": "Sticky Topbar",
423
427
  "query": "Query",
428
+ "query.invalid": "Invalid query",
424
429
  "quicksearch.aria": "search",
425
430
  "quicksearch.placeholder": "Search ...",
426
431
  "rationale.default": "Hit assessed as {{assessment}}",
@@ -500,9 +505,11 @@
500
505
  "route.analytics.deleted": "Deleted Rule!",
501
506
  "route.analytics.detection.title": "Hits Ingested in the Last Three Months by Detection",
502
507
  "route.analytics.detections": "Detections:",
508
+ "route.analytics.dropdown.detection": "Detection",
503
509
  "route.analytics.escalation.title": "Escalation of Created Hits",
504
510
  "route.analytics.filter.rule": "Filter on Rules",
505
511
  "route.analytics.ingestion.title": "Number of Hits Created",
512
+ "route.analytics.manager.search": "Search Analytics",
506
513
  "route.analytics.overview.description": "Description",
507
514
  "route.analytics.overview.empty.description": "There are no existing hits for this analytic. Statistics cannot be shown.",
508
515
  "route.analytics.overview.empty.title": "No Statistics to Show",
@@ -598,8 +605,10 @@
598
605
  "route.help.actions": "Action Documentation",
599
606
  "route.help.api": "API Documentation",
600
607
  "route.help.auth": "Authentication",
608
+ "route.help.bundles": "Hit Bundles",
601
609
  "route.help.client": "Howler Client",
602
610
  "route.help.hit": "Hit Documentation",
611
+ "route.help.hit.banner": "Hit Banner Documentation",
603
612
  "route.help.main": "Dashboard",
604
613
  "route.help.notebook": "Notebook Documentation",
605
614
  "route.help.overviews": "Overviews",
@@ -667,15 +676,15 @@
667
676
  "route.templates.default": "Default",
668
677
  "route.templates.detection": "Choose Detection",
669
678
  "route.templates.global": "Global",
679
+ "route.templates.manager.error.action": "Click to open quick fix options",
680
+ "route.templates.manager.error.message": "Invalid Detection",
681
+ "route.templates.manager.error.modal.description": "The template fields are read only and will not be used. Do you want to remove the template?",
682
+ "route.templates.manager.error.modal.title": "Template detection no longer exists",
670
683
  "route.templates.manager.global": "Global",
671
684
  "route.templates.manager.open": "Open View",
672
685
  "route.templates.manager.personal": "Personal",
673
686
  "route.templates.manager.readonly": "Built-in",
674
687
  "route.templates.manager.search": "Search Templates",
675
- "route.templates.manager.error.message": "Invalid Detection",
676
- "route.templates.manager.error.action": "Click to open quick fix options",
677
- "route.templates.manager.error.modal.title": "Template detection no longer exists",
678
- "route.templates.manager.error.modal.description": "The template fields are read only and will not be used. Do you want to remove the template?",
679
688
  "route.templates.personal": "Personal",
680
689
  "route.templates.prompt": "Activate autocomplete using [ctrl + space].",
681
690
  "route.templates.readonly.warning": "This is a built-in template, and cannot be edited. To make changes to it, contact",
@@ -712,6 +721,7 @@
712
721
  "rule.interval.thirty.minutes": "Every thirty minutes",
713
722
  "rule.interval.three.hours": "Every three hours",
714
723
  "save": "Save",
724
+ "search.layout.settings": "Edit search result layout",
715
725
  "search.open": "Open Search",
716
726
  "search.result.showing": "Showing {{offset}} to {{length}} of {{total}} results",
717
727
  "search.result.showing.single": "No results",
@@ -364,7 +364,6 @@
364
364
  "page.help": "Aide",
365
365
  "page.help.title": "Tableau de bord de l'aide",
366
366
  "page.login.button": "Se connecter",
367
- "page.login.error": "Tapez simplement n'importe quoi dans les champs du nom d'utilisateur et du mot de passe ...",
368
367
  "page.login.password": "Mot de passe",
369
368
  "page.login.username": "Nom d'utilisateur",
370
369
  "page.logout": "Déconnexion de l'utilisateur actuel ... ",
@@ -445,6 +444,7 @@
445
444
  "route.actions.create": "Nouveau action",
446
445
  "route.actions.edit": "Modifier",
447
446
  "route.actions.execute": "Exécuter",
447
+ "route.actions.manager": "Gestion des actions",
448
448
  "route.actions.name": "Nom de l'action",
449
449
  "route.actions.open": "Ouvrir la requête",
450
450
  "route.actions.operation.add": "Ajouter une nouvelle opération",
@@ -505,9 +505,11 @@
505
505
  "route.analytics.deleted": "Règle supprimée!",
506
506
  "route.analytics.detection.title": "Hits ingérés au cours des trois derniers mois par détection",
507
507
  "route.analytics.detections": "Détections:",
508
+ "route.analytics.dropdown.detection": "Détection",
508
509
  "route.analytics.escalation.title": "Escalade des hits créés",
509
510
  "route.analytics.filter.rule": "Filtre sur les règles",
510
511
  "route.analytics.ingestion.title": "Nombre de hits créés",
512
+ "route.analytics.manager.search": "Rechercher les analyses",
511
513
  "route.analytics.overview.description": "Description",
512
514
  "route.analytics.overview.empty.description": "Il n'y a pas de résultats existants pour cette analyse. Les statistiques ne peuvent pas être affichées.",
513
515
  "route.analytics.overview.empty.title": "Pas de statistiques à montrer",
@@ -600,6 +602,7 @@
600
602
  "route.dossiers.search.prompt": "Recherche par titre, requête ou propriétaire.",
601
603
  "route.dossiers.view": "Voir le dossier",
602
604
  "route.help": "Aide",
605
+ "route.help.actions": "Documentation sur les actions",
603
606
  "route.help.api": "Documentation de l'API",
604
607
  "route.help.auth": "Authentification",
605
608
  "route.help.bundles": "Groupes des hits",
@@ -663,6 +666,7 @@
663
666
  "route.overviews.theme.dark": "Prévoyez en mode sombre",
664
667
  "route.overviews.theme.light": "Prévoyez en mode clair",
665
668
  "route.overviews.view": "Voir la vue d'ensemble",
669
+ "route.search": "Rechercher",
666
670
  "route.templates": "Modèles",
667
671
  "route.templates.analytic": "Choisir une analyse",
668
672
  "route.templates.builtin": "Intégré",
@@ -671,15 +675,16 @@
671
675
  "route.templates.create": "Nouvelle modèle",
672
676
  "route.templates.default": "Défaut",
673
677
  "route.templates.detection": "Choisir une détection",
674
- "route.templates.global": "Général",
678
+ "route.templates.global": "Global",
679
+ "route.templates.manager.error.action": "Cliquez ici pour afficher les options de correction rapide",
680
+ "route.templates.manager.error.message": "Détection non valide",
681
+ "route.templates.manager.error.modal.description": "Les clés du modèle sont en lecture seule et ne seront pas utilisées. Voulez-vous supprimer le modèle ?",
682
+ "route.templates.manager.error.modal.title": "La détection du modèle n'existe plus",
675
683
  "route.templates.manager.global": "Global",
684
+ "route.templates.manager.open": "Ouvrir la vue",
676
685
  "route.templates.manager.personal": "Personnel",
677
686
  "route.templates.manager.readonly": "Intégré",
678
687
  "route.templates.manager.search": "Rechercher les modèles",
679
- "route.templates.manager.error.message": "Détection non valide",
680
- "route.templates.manager.error.action": "Cliquez ici pour afficher les options de correction rapide",
681
- "route.templates.manager.error.modal.title": "La détection du modèle n'existe plus",
682
- "route.templates.manager.error.modal.description": "Les clés du modèle sont en lecture seule et ne seront pas utilisées. Voulez-vous supprimer le modèle ?",
683
688
  "route.templates.personal": "Personnel",
684
689
  "route.templates.prompt": "Activer l'autocomplétion en utilisant [ctrl + espace].",
685
690
  "route.templates.readonly.warning": "Il s'agit d'un modèle intégré qui ne peut pas être modifié. Pour le modifier, veuillez contacter",
@@ -700,8 +705,10 @@
700
705
  "route.views.manager.personal": "Personnel",
701
706
  "route.views.manager.readonly": "Intégré",
702
707
  "route.views.manager.search": "Rechercher les vues",
708
+ "route.views.name": "Nom de la vue",
703
709
  "route.views.save": "Enregistrer cette requête comme vue",
704
710
  "route.views.saved": "Vues épinglées",
711
+ "route.views.search.prompt": "Rechercher par nom, requête ou propriétaire.",
705
712
  "route.views.show": "Voir les vues",
706
713
  "route.views.update.success": "Vue actualisée.",
707
714
  "rule.interval": "Intervalle d'exécution de la règle",
@@ -714,6 +721,7 @@
714
721
  "rule.interval.thirty.minutes": "Toutes les trente minutes",
715
722
  "rule.interval.three.hours": "Toutes les trois heures",
716
723
  "save": "Sauvegarder",
724
+ "search.layout.settings": "Modifier la présentation des résultats de recherche",
717
725
  "search.open": "Ouvrir la recherche",
718
726
  "search.result.showing": "Affichage de {{offset}} à {{length}} sur {{total}} articles",
719
727
  "search.result.showing.single": "Aucun articles",
package/package.json CHANGED
@@ -96,7 +96,7 @@
96
96
  "internal-slot": "1.0.7"
97
97
  },
98
98
  "type": "module",
99
- "version": "2.19.0-dev.922",
99
+ "version": "2.19.0-dev.938",
100
100
  "exports": {
101
101
  "./i18n": "./i18n.js",
102
102
  "./index.css": "./index.css",