@cccsaurora/howler-ui 2.13.0-dev.96 → 2.13.1-dev.184

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/api/hit/index.d.ts +1 -1
  2. package/api/hit/index.js +6 -2
  3. package/api/search/index.d.ts +1 -0
  4. package/api/view/index.d.ts +1 -1
  5. package/api/view/index.js +2 -2
  6. package/commons/components/notification/elements/item/NotificationItemDate.js +2 -2
  7. package/commons/components/utils/hooks/useEnv.d.ts +1 -1
  8. package/components/app/App.js +1 -3
  9. package/components/app/drawers/ApiKeyDrawer.js +4 -4
  10. package/components/app/hooks/useMatchers.d.ts +9 -0
  11. package/components/app/hooks/useMatchers.js +82 -0
  12. package/components/app/hooks/useMatchers.test.d.ts +1 -0
  13. package/components/app/hooks/useMatchers.test.js +237 -0
  14. package/components/app/hooks/useTitle.js +5 -4
  15. package/components/app/providers/AnalyticProvider.d.ts +0 -4
  16. package/components/app/providers/AnalyticProvider.js +1 -44
  17. package/components/app/providers/ApiConfigProvider.js +2 -1
  18. package/components/app/providers/HitProvider.d.ts +2 -1
  19. package/components/app/providers/HitProvider.js +8 -2
  20. package/components/app/providers/HitSearchProvider.d.ts +2 -1
  21. package/components/app/providers/HitSearchProvider.js +2 -1
  22. package/components/app/providers/SocketProvider.js +1 -1
  23. package/components/app/providers/ViewProvider.d.ts +1 -1
  24. package/components/app/providers/ViewProvider.js +3 -3
  25. package/components/app/providers/ViewProvider.test.d.ts +1 -0
  26. package/components/app/providers/ViewProvider.test.js +150 -0
  27. package/components/elements/display/ActionButton.d.ts +8 -0
  28. package/components/elements/display/ActionButton.js +18 -0
  29. package/components/elements/display/handlebars/helpers.js +17 -2
  30. package/components/elements/display/json/JSONViewer.d.ts +2 -0
  31. package/components/elements/display/json/JSONViewer.js +6 -13
  32. package/components/elements/hit/HitActions.js +4 -6
  33. package/components/elements/hit/HitBanner.js +13 -5
  34. package/components/elements/hit/HitCard.js +0 -5
  35. package/components/elements/hit/HitComments.js +5 -4
  36. package/components/elements/hit/HitOutline.d.ts +2 -2
  37. package/components/elements/hit/HitOutline.js +11 -21
  38. package/components/elements/hit/HitOverview.js +7 -4
  39. package/components/elements/hit/HitSummary.d.ts +2 -1
  40. package/components/elements/hit/HitSummary.js +8 -7
  41. package/components/elements/hit/aggregate/HitGraph.d.ts +1 -1
  42. package/components/elements/hit/aggregate/HitGraph.js +7 -7
  43. package/components/elements/hit/elements/HitTimestamp.js +8 -8
  44. package/components/elements/hit/related/PivotLink.js +11 -5
  45. package/components/elements/hit/related/RelatedIcon.d.ts +8 -0
  46. package/components/elements/hit/related/RelatedIcon.js +32 -0
  47. package/components/elements/hit/related/RelatedLink.js +4 -25
  48. package/components/hooks/useMyChart.d.ts +1 -1
  49. package/components/hooks/useMyChart.js +1 -1
  50. package/components/routes/advanced/QueryBuilder.js +47 -11
  51. package/components/routes/advanced/QueryEditor.js +8 -13
  52. package/components/routes/analytics/AnalyticOverview.d.ts +1 -1
  53. package/components/routes/analytics/AnalyticOverview.js +1 -1
  54. package/components/routes/analytics/AnalyticOverviews.d.ts +1 -1
  55. package/components/routes/analytics/AnalyticOverviews.js +1 -1
  56. package/components/routes/analytics/AnalyticSearch.js +14 -2
  57. package/components/routes/analytics/AnalyticTemplates.d.ts +1 -1
  58. package/components/routes/analytics/AnalyticTemplates.js +10 -7
  59. package/components/routes/analytics/RuleView.d.ts +1 -1
  60. package/components/routes/analytics/RuleView.js +1 -1
  61. package/components/routes/analytics/TriageSettings.d.ts +1 -1
  62. package/components/routes/analytics/TriageSettings.js +1 -1
  63. package/components/routes/analytics/widgets/Assessment.d.ts +1 -1
  64. package/components/routes/analytics/widgets/Assessment.js +1 -1
  65. package/components/routes/analytics/widgets/Created.d.ts +1 -1
  66. package/components/routes/analytics/widgets/Created.js +1 -1
  67. package/components/routes/analytics/widgets/Detection.d.ts +1 -1
  68. package/components/routes/analytics/widgets/Detection.js +1 -1
  69. package/components/routes/analytics/widgets/Escalation.d.ts +1 -1
  70. package/components/routes/analytics/widgets/Escalation.js +1 -1
  71. package/components/routes/analytics/widgets/Stacked.d.ts +1 -1
  72. package/components/routes/analytics/widgets/Stacked.js +1 -1
  73. package/components/routes/analytics/widgets/Status.d.ts +1 -1
  74. package/components/routes/analytics/widgets/Status.js +1 -1
  75. package/components/routes/help/SearchDocumentation.js +2 -1
  76. package/components/routes/help/TemplateDocumentation.js +5 -5
  77. package/components/routes/hits/search/HitBrowser.js +2 -2
  78. package/components/routes/hits/search/HitContextMenu.js +6 -7
  79. package/components/routes/hits/search/HitQuery.js +2 -1
  80. package/components/routes/hits/search/InformationPane.js +75 -78
  81. package/components/routes/hits/search/SearchPane.js +3 -9
  82. package/components/routes/hits/search/grid/AddColumnModal.js +10 -5
  83. package/components/routes/hits/search/grid/HitGrid.js +6 -5
  84. package/components/routes/hits/search/shared/CustomSpan.js +6 -6
  85. package/components/routes/hits/view/HitViewer.js +18 -26
  86. package/components/routes/home/index.js +4 -4
  87. package/components/routes/overviews/OverviewViewer.js +33 -31
  88. package/components/routes/settings/SecuritySection.js +2 -2
  89. package/components/routes/templates/TemplateViewer.js +27 -36
  90. package/components/routes/templates/Templates.js +4 -11
  91. package/components/routes/views/ViewComposer.js +8 -1
  92. package/components/routes/views/Views.js +25 -9
  93. package/index.js +7 -0
  94. package/locales/en/help/search.json +17 -0
  95. package/locales/en/translation.json +12 -3
  96. package/locales/fr/help/search.json +17 -0
  97. package/locales/fr/translation.json +12 -4
  98. package/models/WithMetadata.d.ts +10 -0
  99. package/models/WithMetadata.js +1 -0
  100. package/models/entities/generated/ApiType.d.ts +7 -0
  101. package/package.json +112 -111
  102. package/plugins/borealis/components/BorealisTypography.js +4 -2
  103. package/setupTests.d.ts +1 -0
  104. package/setupTests.js +12 -0
  105. package/tests/MockLocalStorage.d.ts +5 -0
  106. package/tests/MockLocalStorage.js +44 -0
  107. package/tests/server-handlers.d.ts +5 -0
  108. package/tests/server-handlers.js +97 -0
  109. package/tests/server.d.ts +3 -0
  110. package/tests/server.js +5 -0
  111. package/utils/constants.js +2 -2
  112. package/utils/stringUtils.d.ts +1 -0
  113. package/utils/stringUtils.js +9 -0
  114. package/utils/utils.js +3 -3
  115. package/components/app/providers/DossierProvider.d.ts +0 -16
  116. package/components/app/providers/DossierProvider.js +0 -82
  117. package/components/app/providers/TemplateProvider.d.ts +0 -14
  118. package/components/app/providers/TemplateProvider.js +0 -103
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Analytics, InfoOutlined } from '@mui/icons-material';
3
3
  import { Alert, AlertTitle, Autocomplete, Box, Button, Chip, CircularProgress, Divider, Fade, Grid, LinearProgress, Stack, TextField, Tooltip, Typography } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
5
6
  import { FieldContext } from '@cccsaurora/howler-ui/components/app/providers/FieldProvider';
6
7
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
7
8
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
8
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
9
9
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
10
10
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
11
11
  import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
@@ -19,11 +19,11 @@ import PluginChip from '../PluginChip';
19
19
  import HitGraph from './aggregate/HitGraph';
20
20
  const HitSummary = ({ query, response, onStart, onComplete }) => {
21
21
  const { t } = useTranslation();
22
- const getMatchingTemplate = useContextSelector(TemplateContext, ctx => ctx.getMatchingTemplate);
23
22
  const { dispatchApi } = useMyApi();
24
23
  const { hitFields } = useContext(FieldContext);
25
24
  const { showErrorMessage } = useMySnackbar();
26
25
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
26
+ const { getMatchingTemplate } = useMatchers();
27
27
  const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
28
28
  const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
29
29
  const searching = useContextSelector(HitSearchContext, ctx => ctx.searching);
@@ -48,16 +48,17 @@ const HitSummary = ({ query, response, onStart, onComplete }) => {
48
48
  }
49
49
  try {
50
50
  // Get a list of every key in every template of the hits we're searching
51
- const _keyCounts = (response?.items ?? [])
52
- .flatMap(h => {
53
- const matchingTemplate = getMatchingTemplate(h);
51
+ const rawCounts = await Promise.all((response?.items ?? []).map(async (h) => {
52
+ const matchingTemplate = await getMatchingTemplate(h);
54
53
  return (matchingTemplate?.keys ?? [])
55
54
  .filter(key => !['howler.id', 'howler.hash'].includes(key))
56
55
  .map(key => ({
57
56
  key,
58
57
  source: `${matchingTemplate.analytic}: ${matchingTemplate.detection ?? t('any')}`
59
58
  }));
60
- })
59
+ }));
60
+ const _keyCounts = rawCounts
61
+ .flat()
61
62
  .concat(customKeys.map(key => ({ key, source: 'custom' })))
62
63
  // Take that array and reduce it to unique keys and the number of times we see it,
63
64
  // as well as the templates we sourced this key from
@@ -142,7 +143,7 @@ const HitSummary = ({ query, response, onStart, onComplete }) => {
142
143
  return (_jsxs(Stack, { sx: { mx: 2, height: '100%' }, spacing: 1, children: [_jsx(Typography, { variant: "h6", children: t('hit.summary.aggregate.title') }), _jsx(Divider, { flexItem: true }), _jsx(HitGraph, { query: query }), _jsx(Divider, { flexItem: true }), _jsxs(Stack, { sx: { overflow: 'auto', marginTop: '0 !important' }, pt: 1, spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(Autocomplete, { fullWidth: true, multiple: true, sx: { minWidth: '175px' }, size: "small", value: customKeys, options: hitFields.map(_field => _field.key), renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.summary.adhoc') }), onChange: (_, value) => setCustomKeys(value) }), _jsx(Button, { variant: "outlined", startIcon: loading ? _jsx(CircularProgress, { size: 20, sx: { ml: 1 } }) : _jsx(Analytics, { sx: { ml: 1 } }), disabled: loading, onClick: () => performAggregation(), children: t('button.aggregate') })] }), isEmpty(aggregateResults) && (_jsxs(Alert, { severity: "info", variant: "outlined", children: [_jsx(AlertTitle, { children: t('hit.summary.aggregate.nokeys.title') }), t('hit.summary.aggregate.nokeys.description')] })), loading && _jsx(LinearProgress, { sx: { minHeight: '4px' } }), Object.keys(aggregateResults)
143
144
  .filter(key => !isEmpty(aggregateResults[key]))
144
145
  .flatMap(key => [
145
- _jsx(Fade, { in: true, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Typography, { variant: "body1", children: key }, key + '-title'), keyCounts[key]?.count < 0 ? (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", t('hit.summary.adhoc.custom'), ")"] })) : (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", keyCounts[key]?.count, " ", t('references'), ")"] })), _jsx(Tooltip, { title: _jsxs(Stack, { children: [_jsx(Typography, { variant: "caption", children: t('hit.summary.aggregate.sources') }), keyCounts[key].sources.map(source => (_jsx(Typography, { variant: "caption", children: source }, source)))] }), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }) }, key + '-refs'),
146
+ _jsx(Fade, { in: true, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Typography, { variant: "body1", children: key }, key + '-title'), keyCounts[key]?.count < 0 ? (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", t('hit.summary.adhoc.custom'), ")"] })) : (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", keyCounts[key]?.count ?? '?', " ", t('references'), ")"] })), _jsx(Tooltip, { title: _jsxs(Stack, { children: [_jsx(Typography, { variant: "caption", children: t('hit.summary.aggregate.sources') }), keyCounts[key]?.sources.map(source => (_jsx(Typography, { variant: "caption", children: source }, source))) ?? '?'] }), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }) }, key + '-refs'),
146
147
  _jsx(Fade, { in: true, children: hitFields.find(f => f.key === key)?.type !== 'date' ? (_jsx(Box, { sx: theme => ({ ml: `${theme.spacing(1)} !important`, alignSelf: 'start' }), children: _jsx(Grid, { container: true, sx: theme => ({ mr: 1, mt: theme.spacing(-1) }), spacing: 1, children: Object.keys(aggregateResults[key]).map(_key => (_jsx(Grid, { item: true, xs: "auto", children: _jsx(PluginChip, { context: "summary", size: "small", variant: "filled", value: _key, label: `${_key} (${aggregateResults[key][_key]})`, onClick: () => setSearch(key, `"${_key}"`) }) }, _key))) }, key + '-list') })) : (_jsx(Chip, { size: "small", sx: theme => ({ ml: `${theme.spacing(1)} !important`, alignSelf: 'start' }), label: getTimeRange(Object.keys(aggregateResults[key]))
147
148
  .map(d => new Date(d).toLocaleString())
148
149
  .join(' - '), onClick: () => setSearch(key, `[${getTimeRange(Object.keys(aggregateResults[key])).join(' TO ')}]`) })) }, key + '-results')
@@ -1,4 +1,4 @@
1
- import 'chartjs-adapter-moment';
1
+ import 'chartjs-adapter-dayjs-4';
2
2
  import type { FC } from 'react';
3
3
  declare const HitGraph: FC<{
4
4
  query: string;
@@ -2,15 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { CenterFocusWeak, Refresh } from '@mui/icons-material';
3
3
  import { Alert, AlertTitle, Autocomplete, Box, Button, CircularProgress, IconButton, Stack, TextField, Tooltip, alpha, useTheme } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
- import 'chartjs-adapter-moment';
5
+ import 'chartjs-adapter-dayjs-4';
6
6
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
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
10
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
11
11
  import useMyChart from '@cccsaurora/howler-ui/components/hooks/useMyChart';
12
+ import dayjs from 'dayjs';
12
13
  import { capitalize } from 'lodash-es';
13
- import moment from 'moment';
14
14
  import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
15
15
  import { Scatter } from 'react-chartjs-2';
16
16
  import { useTranslation } from 'react-i18next';
@@ -96,10 +96,10 @@ const HitGraph = ({ query }) => {
96
96
  return {
97
97
  label: `${label} (${category.total})`,
98
98
  data: category.items.map(hit => {
99
- const createdMoment = moment(hit.event?.created ?? hit.timestamp);
99
+ const createdDate = dayjs(hit.event?.created ?? hit.timestamp);
100
100
  return {
101
- x: createdMoment.clone().hour(0).minute(0).second(0).toISOString(),
102
- y: createdMoment.hour() + createdMoment.minute() / 60 + createdMoment.second() / 3600,
101
+ x: createdDate.clone().hour(0).minute(0).second(0).toISOString(),
102
+ y: createdDate.hour() + createdDate.minute() / 60 + createdDate.second() / 3600,
103
103
  hit,
104
104
  label
105
105
  };
@@ -157,7 +157,7 @@ const HitGraph = ({ query }) => {
157
157
  tooltip: {
158
158
  callbacks: {
159
159
  title: entries => `${entries.length} ${t('hits')}`,
160
- label: entry => `${entry.raw.hit.howler.analytic}: ${entry.raw.hit.howler.detection} (${moment(entry.raw.hit.event.created).format('MMM D HH:mm:ss')})`,
160
+ label: entry => `${entry.raw.hit.howler.analytic}: ${entry.raw.hit.howler.detection} (${dayjs(entry.raw.hit.event.created).format('MMM D HH:mm:ss')})`,
161
161
  afterLabel: entry => `${entry.raw.hit.howler.outline.threat} ${entry.raw.hit.howler.outline.target}`
162
162
  }
163
163
  },
@@ -183,7 +183,7 @@ const HitGraph = ({ query }) => {
183
183
  ticks: {
184
184
  callback: (value) => {
185
185
  const [hour, minute] = [Math.floor(value), Math.floor((value - Math.floor(value)) * 60)];
186
- return moment().hour(hour).minute(minute).format('HH:mm');
186
+ return dayjs().hour(hour).minute(minute).format('HH:mm');
187
187
  }
188
188
  }
189
189
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Chip, Tooltip } from '@mui/material';
3
3
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
4
- import moment from 'moment';
4
+ import dayjs from 'dayjs';
5
5
  import { useContext, useMemo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { formatDate } from '@cccsaurora/howler-ui/utils/utils';
@@ -14,31 +14,31 @@ const TIMESTAMP_MESSAGES = {
14
14
  const HitTimestamp = ({ hit, layout }) => {
15
15
  const { t } = useTranslation();
16
16
  const { config } = useContext(ApiConfigContext);
17
- const threshold = useMemo(() => moment().subtract(config.configuration.system.retention?.limit_amount ?? 350, config.configuration.system.retention?.limit_unit ?? 'days'), [config.configuration.system.retention?.limit_amount, config.configuration.system.retention?.limit_unit]);
17
+ const threshold = useMemo(() => dayjs().subtract(config.configuration.system.retention?.limit_amount ?? 350, config.configuration.system.retention?.limit_unit ?? 'days'), [config.configuration.system.retention?.limit_amount, config.configuration.system.retention?.limit_unit]);
18
18
  const timestamp = useMemo(() => {
19
19
  const validFieldValues = [hit.howler?.expiry, hit.event?.created, hit.timestamp];
20
20
  const earliestDate = validFieldValues
21
21
  .filter(entry => !!entry)
22
22
  .reduce((earliest, current) => {
23
- return moment(earliest).isBefore(current) ? earliest : current;
23
+ return dayjs(earliest).isBefore(current) ? earliest : current;
24
24
  });
25
25
  return earliestDate;
26
26
  }, [hit]);
27
27
  const color = useMemo(() => {
28
- if (moment(timestamp).isBefore(threshold.clone().add(2, 'weeks'))) {
28
+ if (dayjs(timestamp).isBefore(threshold.clone().add(2, 'weeks'))) {
29
29
  return 'error';
30
30
  }
31
- if (moment(timestamp).isBefore(threshold.clone().add(1, 'months'))) {
31
+ if (dayjs(timestamp).isBefore(threshold.clone().add(1, 'months'))) {
32
32
  return 'warning';
33
33
  }
34
34
  return 'default';
35
35
  }, [threshold, timestamp]);
36
36
  const duration = useMemo(() => {
37
- if (moment(timestamp).isBefore(threshold)) {
37
+ if (dayjs(timestamp).isBefore(threshold)) {
38
38
  return t('retention.imminent');
39
39
  }
40
- const diff = moment(timestamp).diff(threshold, 'seconds');
41
- const _duration = moment.duration(diff, 'seconds');
40
+ const diff = dayjs(timestamp).diff(threshold, 'seconds');
41
+ const _duration = dayjs.duration(diff, 'seconds');
42
42
  return _duration.humanize();
43
43
  }, [t, threshold, timestamp]);
44
44
  return (_jsx(Tooltip, { title: t(TIMESTAMP_MESSAGES[color], {
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Icon } from '@iconify/react/dist/iconify.js';
3
2
  import { Skeleton } from '@mui/material';
4
3
  import Handlebars from 'handlebars';
5
4
  import { isEmpty } from 'lodash-es';
@@ -32,11 +31,18 @@ const PivotLink = ({ pivot, hit, compact = false }) => {
32
31
  return Handlebars.compile(pivot.value)(templateObject);
33
32
  }, [flatHit, pivot]);
34
33
  if (href) {
35
- return (_jsx(RelatedLink, { title: pivot.label[i18n.language], href: href, compact: compact, children: _jsx(Icon, { fontSize: "1.5rem", icon: pivot.icon }) }));
34
+ return _jsx(RelatedLink, { title: pivot.label[i18n.language], href: href, compact: compact, icon: pivot.icon });
36
35
  }
37
- const pluginPivot = pluginStore.executeFunction(`pivot.${pivot.format}`, { pivot, hit, compact });
38
- if (pluginPivot) {
39
- return pluginPivot;
36
+ try {
37
+ const pluginPivot = pluginStore.executeFunction(`pivot.${pivot.format}`, { pivot, hit, compact });
38
+ if (pluginPivot) {
39
+ return pluginPivot;
40
+ }
41
+ }
42
+ catch (e) {
43
+ // eslint-disable-next-line no-console
44
+ console.warn(`Pivot plugin for format ${pivot.format} does not exist, not rendering`);
45
+ return null;
40
46
  }
41
47
  return _jsx(Skeleton, { variant: "rounded" });
42
48
  };
@@ -0,0 +1,8 @@
1
+ import { type FC } from 'react';
2
+ declare const RelatedIcon: FC<{
3
+ icon?: string;
4
+ title?: string;
5
+ href?: string;
6
+ compact?: boolean;
7
+ }>;
8
+ export default RelatedIcon;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Icon, iconExists } from '@iconify/react/dist/iconify.js';
3
+ import Avatar from '@mui/material/Avatar';
4
+ import { useAppTheme } from '@cccsaurora/howler-ui/commons/components/app/hooks';
5
+ import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
6
+ import { useContext } from 'react';
7
+ const RelatedIcon = ({ icon, title, href, compact = false }) => {
8
+ const { config } = useContext(ApiConfigContext);
9
+ const { isDark } = useAppTheme();
10
+ if (!icon) {
11
+ return null;
12
+ }
13
+ if (iconExists(icon)) {
14
+ return _jsx(Icon, { fontSize: "1.5rem", icon: icon });
15
+ }
16
+ const app = config.configuration.ui.apps.find(a => a.name.toLowerCase() === icon?.toLowerCase());
17
+ if (app) {
18
+ // use the image link for the configured related application instead
19
+ icon = app[`img_${isDark ? 'd' : 'l'}`];
20
+ }
21
+ return (_jsx(Avatar, { variant: "rounded", alt: title ?? href, src: icon, sx: [
22
+ theme => ({
23
+ width: theme.spacing(compact ? 4 : 6),
24
+ height: theme.spacing(compact ? 4 : 6),
25
+ '& img': {
26
+ objectFit: 'contain'
27
+ }
28
+ }),
29
+ !icon && { backgroundColor: 'transparent' }
30
+ ], children: icon }));
31
+ };
32
+ export default RelatedIcon;
@@ -1,22 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Avatar, Stack, Typography } from '@mui/material';
3
- import { useAppTheme } from '@cccsaurora/howler-ui/commons/components/app/hooks';
4
- import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
2
+ import { Stack, Typography } from '@mui/material';
5
3
  import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
6
- import React, { useContext, useMemo } from 'react';
4
+ import React, {} from 'react';
7
5
  import { Link } from 'react-router-dom';
6
+ import RelatedIcon from './RelatedIcon';
8
7
  const RelatedLink = ({ icon, title, href, compact = false, children }) => {
9
- const { config } = useContext(ApiConfigContext);
10
- const { isDark } = useAppTheme();
11
- const _icon = useMemo(() => {
12
- if (icon) {
13
- const app = config.configuration.ui.apps.find(a => a.name.toLowerCase() === icon?.toLowerCase());
14
- if (app) {
15
- return app[`img_${isDark ? 'd' : 'l'}`];
16
- }
17
- }
18
- return icon;
19
- }, [config.configuration.ui.apps, icon, isDark]);
20
8
  return (_jsx(HowlerCard, { variant: compact ? 'outlined' : 'elevation', onClick: () => window.open(href), sx: [
21
9
  theme => ({
22
10
  cursor: 'pointer',
@@ -29,15 +17,6 @@ const RelatedLink = ({ icon, title, href, compact = false, children }) => {
29
17
  '& a': { textDecoration: 'none', color: 'text.primary' }
30
18
  }),
31
19
  !compact && { border: 'thin solid', borderColor: 'transparent' }
32
- ], children: _jsxs(Stack, { direction: "row", p: compact ? 0.5 : 1, spacing: 1, alignItems: "center", children: [children || (_jsx(Avatar, { variant: "rounded", alt: title ?? href, src: _icon, sx: [
33
- theme => ({
34
- width: theme.spacing(compact ? 4 : 6),
35
- height: theme.spacing(compact ? 4 : 6),
36
- '& img': {
37
- objectFit: 'contain'
38
- }
39
- }),
40
- !_icon && { backgroundColor: 'transparent' }
41
- ], children: _icon })), _jsx(Typography, { component: Link, to: href, onClick: e => e.stopPropagation(), children: title ?? href })] }) }, href));
20
+ ], children: _jsxs(Stack, { direction: "row", p: compact ? 0.5 : 1, spacing: 1, alignItems: "center", children: [children || _jsx(RelatedIcon, { icon: icon, title: title, href: href, compact: compact }), _jsx(Typography, { component: Link, to: href, onClick: e => e.stopPropagation(), children: title ?? href })] }) }, href));
42
21
  };
43
22
  export default RelatedLink;
@@ -1,4 +1,4 @@
1
- import 'chartjs-adapter-moment';
1
+ import 'chartjs-adapter-dayjs-4';
2
2
  declare const useMyChart: () => {
3
3
  line: (titleKey: string, subtitleKey?: string) => {
4
4
  responsive: boolean;
@@ -1,6 +1,6 @@
1
1
  import { useTheme } from '@mui/material';
2
2
  import { ArcElement, BarElement, CategoryScale, Chart as ChartJS, Filler, Legend, LinearScale, LineElement, PointElement, SubTitle, TimeScale, Title, Tooltip } from 'chart.js';
3
- import 'chartjs-adapter-moment';
3
+ import 'chartjs-adapter-dayjs-4';
4
4
  import zoomPlugin from 'chartjs-plugin-zoom';
5
5
  import { useCallback } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useMonaco } from '@monaco-editor/react';
4
4
  import { OpenInNew, PlayArrowOutlined, SsidChart } from '@mui/icons-material';
5
5
  import { Alert, AlertTitle, Autocomplete, Box, Card, Checkbox, Chip, CircularProgress, FormControlLabel, IconButton, ListItemText, Slider, Stack, TextField, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material';
6
+ import Popper, {} from '@mui/material/Popper';
6
7
  import api from '@cccsaurora/howler-ui/api';
7
8
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
8
9
  import { parseEvent } from '@cccsaurora/howler-ui/commons/components/utils/keyboard';
@@ -12,7 +13,7 @@ import CustomButton from '@cccsaurora/howler-ui/components/elements/addons/butto
12
13
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
13
14
  import JSONViewer from '@cccsaurora/howler-ui/components/elements/display/json/JSONViewer';
14
15
  import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
15
- import moment from 'moment';
16
+ import dayjs from 'dayjs';
16
17
  import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
17
18
  import { useTranslation } from 'react-i18next';
18
19
  import { Link } from 'react-router-dom';
@@ -40,8 +41,8 @@ description: A basic example of using sigma rule notation to query howler
40
41
  references:
41
42
  - https://github.com/SigmaHQ/sigma
42
43
  author: You
43
- date: ${moment().format('YYYY/MM/DD')}
44
- modified: ${moment().format('YYYY/MM/DD')}
44
+ date: ${dayjs().format('YYYY/MM/DD')}
45
+ modified: ${dayjs().format('YYYY/MM/DD')}
45
46
  tags:
46
47
  - attack.command_and_control
47
48
  logsource:
@@ -61,6 +62,10 @@ falsepositives:
61
62
  level: informational
62
63
  `
63
64
  };
65
+ const LUCENE_QUERY_OPTIONS = ['default', 'facet', 'groupby'];
66
+ const CustomPopper = (props) => {
67
+ return _jsx(Popper, { ...props, style: { width: 'fit-content' }, placement: "bottom-start" });
68
+ };
64
69
  const QueryBuilder = () => {
65
70
  const { t } = useTranslation();
66
71
  const theme = useTheme();
@@ -73,6 +78,8 @@ const QueryBuilder = () => {
73
78
  const [type, setType] = useState('lucene');
74
79
  const [loading, setLoading] = useState(false);
75
80
  const [query, setQuery] = useState(DEFAULT_VALUES.lucene);
81
+ const [queryType, setQueryType] = useState(LUCENE_QUERY_OPTIONS[0]);
82
+ const [groupByField, setGroupByField] = useState(null);
76
83
  const [allFields, setAllFields] = useState(true);
77
84
  const [fields, setFields] = useState(['howler.id']);
78
85
  const [response, setResponse] = useState(null);
@@ -90,10 +97,25 @@ const QueryBuilder = () => {
90
97
  };
91
98
  let result;
92
99
  if (type === 'lucene') {
93
- result = await api.search.hit.post({
94
- query: sanitizeMultilineLucene(query),
95
- ...searchProperties
96
- });
100
+ if (queryType === 'facet') {
101
+ result = await api.search.facet.hit.post({
102
+ query: sanitizeMultilineLucene(query),
103
+ rows: STEPS[rows],
104
+ fields
105
+ });
106
+ }
107
+ else if (queryType === 'groupby') {
108
+ result = await api.search.grouped.hit.post(groupByField, {
109
+ query: sanitizeMultilineLucene(query),
110
+ ...searchProperties
111
+ });
112
+ }
113
+ else {
114
+ result = await api.search.hit.post({
115
+ query: sanitizeMultilineLucene(query),
116
+ ...searchProperties
117
+ });
118
+ }
97
119
  }
98
120
  else if (type === 'eql') {
99
121
  result = await api.search.hit.eql.post({
@@ -116,7 +138,7 @@ const QueryBuilder = () => {
116
138
  finally {
117
139
  setLoading(false);
118
140
  }
119
- }, [allFields, fields, query, rows, type]);
141
+ }, [allFields, fields, groupByField, query, queryType, rows, type]);
120
142
  const onKeyDown = useCallback(event => {
121
143
  const parsedEvent = parseEvent(event);
122
144
  if (parsedEvent.isCtrl && parsedEvent.isEnter) {
@@ -146,6 +168,12 @@ const QueryBuilder = () => {
146
168
  maxHeight: '85vh'
147
169
  }));
148
170
  }, [query, response, showModal, showWarningMessage, t, type]);
171
+ const searchDisabled = useMemo(() => type === 'lucene' && queryType === 'groupby' && !groupByField, [groupByField, queryType, type]);
172
+ useEffect(() => {
173
+ if (type !== 'lucene' && queryType !== 'default') {
174
+ setQueryType('default');
175
+ }
176
+ }, [queryType, type]);
149
177
  useEffect(() => {
150
178
  if (!monaco) {
151
179
  return;
@@ -190,13 +218,21 @@ const QueryBuilder = () => {
190
218
  display: 'flex',
191
219
  alignSelf: 'stretch'
192
220
  }
193
- }, children: [smallButtons ? (_jsx(Tooltip, { title: t('route.actions.execute'), children: _jsx(IconButton, { size: "small", sx: { alignSelf: 'start' }, color: "success", onClick: execute, children: loading ? _jsx(CircularProgress, { size: 18, color: "success" }) : _jsx(PlayArrowOutlined, { color: "success" }) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", startIcon: loading ? (_jsx(CircularProgress, { size: 18, color: "success" })) : (_jsx(PlayArrowOutlined, { color: "success", sx: { '& path': { stroke: 'currentcolor', strokeWidth: '1px' } } })), color: "success", onClick: execute, children: t('route.actions.execute') })), _jsxs(Stack, { direction: compactLayout ? 'column' : 'row', spacing: 1, children: [_jsx(Autocomplete, { value: type, onChange: (__, value) => setType(value), options: QUERY_TYPES, getOptionLabel: option => t(`route.advanced.query.${option}`), renderOption: (props, option) => (_jsx(ListItemText, { ...props, sx: { flexDirection: 'column', alignItems: 'start !important' }, primary: t(`route.advanced.query.${option}`), secondary: t(`route.advanced.query.${option}.description`) })), renderInput: params => (_jsx(TextField, { ...params, size: "small", label: t('route.advanced.type'), sx: { minWidth: '250px' } })), sx: [
221
+ }, children: [smallButtons ? (_jsx(Tooltip, { title: t('route.actions.execute'), children: _jsx(IconButton, { size: "small", sx: { alignSelf: 'start' }, color: "success", onClick: execute, disabled: searchDisabled, children: loading ? (_jsx(CircularProgress, { size: 18, color: "success" })) : (_jsx(PlayArrowOutlined, { color: searchDisabled ? 'disabled' : 'success' })) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", startIcon: loading ? (_jsx(CircularProgress, { size: 18, color: "success" })) : (_jsx(PlayArrowOutlined, { color: searchDisabled ? 'disabled' : 'success', sx: { '& path': { stroke: 'currentcolor', strokeWidth: '1px' } } })), color: "success", onClick: execute, disabled: searchDisabled, children: t('route.actions.execute') })), _jsxs(Stack, { direction: compactLayout ? 'column' : 'row', spacing: 1, children: [_jsx(Autocomplete, { value: type, onChange: (__, value) => setType(value), options: QUERY_TYPES, getOptionLabel: option => t(`route.advanced.query.${option}`), renderOption: (props, option) => (_jsx(ListItemText, { ...props, sx: { flexDirection: 'column', alignItems: 'start !important' }, primary: t(`route.advanced.query.${option}`), secondary: t(`route.advanced.query.${option}.description`) })), renderInput: params => (_jsx(TextField, { ...params, size: "small", label: t('route.advanced.type'), sx: { minWidth: '250px' } })), sx: [
194
222
  !compactLayout && {
195
223
  height: '100%',
196
224
  '& .MuiFormControl-root': { height: '100%', '& > div': { height: '100%' } }
197
225
  }
198
- ], slotProps: { paper: { sx: { minWidth: '600px' } } } }), _jsx(Card, { variant: "outlined", sx: { flex: 1, maxWidth: '350px', minWidth: '210px' }, children: _jsxs(Stack, { spacing: 0.5, sx: { px: 1, alignItems: 'start' }, children: [_jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: [t('route.advanced.rows'), ": ", STEPS[rows]] }), _jsx(Slider, { size: "small", valueLabelDisplay: "off", value: rows, onChange: (_, value) => setRows(value), min: 0, max: 9, step: 1, marks: true, track: false, sx: { py: 0.5 } })] }) })] }), allFields ? (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: allFields, onChange: (__, checked) => setAllFields(checked) }), label: t('route.advanced.fields.all'), sx: { '& > span': { color: 'text.secondary' }, alignSelf: 'start' } })) : (_jsx(Autocomplete, { fullWidth: true, renderTags: values => values.length <= 3 ? (_jsx(Stack, { direction: "row", spacing: 0.5, children: values.map(_value => (_jsx(Chip, { size: "small", label: _value }, _value))) })) : (_jsx(Tooltip, { title: _jsx(Stack, { spacing: 1, children: values.map(_value => (_jsx("span", { children: _value }, _value))) }), children: _jsx(Chip, { size: "small", label: values.length }) })), multiple: true, size: "small", options: fieldOptions, value: fields, onChange: (__, values) => (values.length > 0 ? setFields(values) : setAllFields(true)), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.fields') }), sx: { maxWidth: '500px', width: '20vw', minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown })), _jsx(FlexOne, {}), type === 'lucene' &&
199
- (smallButtons ? (_jsx(Tooltip, { title: t('route.advanced.open'), children: _jsx(IconButton, { color: "primary", sx: { alignSelf: 'center' }, component: Link, disabled: !response, to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}`, children: _jsx(OpenInNew, { fontSize: "small" }) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", startIcon: _jsx(OpenInNew, {}), component: Link, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, children: t('route.advanced.open') }))), smallButtons ? (_jsx(Tooltip, { title: response ? t('route.advanced.create.rule') : t('route.advanced.create.rule.disabled'), children: _jsx(IconButton, { size: "small", sx: { alignSelf: 'center' }, color: "info", onClick: onCreateRule, disabled: !response, children: _jsx(SsidChart, {}) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", color: "info", startIcon: _jsx(SsidChart, {}), onClick: onCreateRule, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, tooltip: !response && t('route.advanced.create.rule.disabled'), children: t('route.advanced.create.rule') }))] }), _jsxs(Box, { width: "100%", height: "calc(100vh - 112px)", sx: { position: 'relative', overflow: 'hidden', borderTop: `thin solid ${theme.palette.divider}` }, children: [_jsx(Box, { sx: { position: 'absolute', top: 0, left: 0, bottom: 0, right: `calc(50% + 7px - ${x}px)`, pt: 1 }, children: _jsx(QueryEditor, { query: query, setQuery: setQuery, language: type }) }), _jsx(Box, { sx: {
226
+ ], slotProps: { paper: { sx: { minWidth: '600px' } } } }), _jsx(Card, { variant: "outlined", sx: { flex: 1, maxWidth: '350px', minWidth: '210px' }, children: _jsxs(Stack, { spacing: 0.5, sx: { px: 1, alignItems: 'start' }, children: [_jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: [t('route.advanced.rows'), ": ", STEPS[rows]] }), _jsx(Slider, { size: "small", valueLabelDisplay: "off", value: rows, onChange: (_, value) => setRows(value), min: 0, max: 9, step: 1, marks: true, track: false, sx: { py: 0.5 } })] }) })] }), type === 'lucene' && (_jsx(Autocomplete, { size: "small", getOptionLabel: opt => t(`route.advanced.query.type.${opt}`), options: LUCENE_QUERY_OPTIONS, value: queryType, onChange: (_event, value) => setQueryType(value), renderInput: params => (_jsx(TextField, { ...params, label: t('route.advanced.query.type'), sx: { minWidth: '200px' } })) })), queryType === 'groupby' && (_jsx(Autocomplete, { size: "small", options: fieldOptions, value: groupByField, onChange: (__, value) => setGroupByField(value), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.pivot.field') }), sx: { minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), allFields && queryType !== 'facet' ? (_jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: allFields, onChange: (__, checked) => setAllFields(checked) }), label: t('route.advanced.fields.all'), sx: { '& > span': { color: 'text.secondary' }, alignSelf: 'start' } })) : (_jsx(Autocomplete, { fullWidth: true, renderTags: values => values.length <= 3 ? (_jsx(Stack, { direction: "row", spacing: 0.5, children: values.map(_value => (_jsx(Chip, { size: "small", label: _value }, _value))) })) : (_jsx(Tooltip, { title: _jsx(Stack, { spacing: 1, children: values.map(_value => (_jsx("span", { children: _value }, _value))) }), children: _jsx(Chip, { size: "small", label: values.length }) })), multiple: true, size: "small", options: fieldOptions, value: fields, onChange: (__, values) => (values.length > 0 ? setFields(values) : setAllFields(true)), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.fields') }), sx: { maxWidth: '500px', width: '20vw', minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), _jsx(FlexOne, {}), type === 'lucene' &&
227
+ (smallButtons ? (_jsx(Tooltip, { title: t('route.advanced.open'), children: _jsx(IconButton, { color: "primary", sx: { alignSelf: 'center' }, component: Link, disabled: !response, to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}`, children: _jsx(OpenInNew, { fontSize: "small" }) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", startIcon: _jsx(OpenInNew, {}), component: Link, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, children: t('route.advanced.open') }))), smallButtons ? (_jsx(Tooltip, { title: response ? t('route.advanced.create.rule') : t('route.advanced.create.rule.disabled'), children: _jsx(IconButton, { size: "small", sx: { alignSelf: 'center' }, color: "info", onClick: onCreateRule, disabled: !response, children: _jsx(SsidChart, {}) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", color: "info", startIcon: _jsx(SsidChart, {}), onClick: onCreateRule, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, tooltip: !response && t('route.advanced.create.rule.disabled'), children: t('route.advanced.create.rule') }))] }), _jsxs(Box, { width: "100%", height: "calc(100vh - 112px)", sx: { position: 'relative', overflow: 'hidden', borderTop: `thin solid ${theme.palette.divider}` }, children: [_jsx(Box, { sx: {
228
+ position: 'absolute',
229
+ top: 0,
230
+ left: 0,
231
+ bottom: 0,
232
+ right: `calc(50% + 7px - ${x}px)`,
233
+ pt: 1,
234
+ display: 'flex'
235
+ }, children: _jsx(QueryEditor, { query: query, setQuery: setQuery, language: type, height: "100%" }) }), _jsx(Box, { sx: {
200
236
  position: 'absolute',
201
237
  top: 0,
202
238
  bottom: 0,
@@ -26,18 +26,6 @@ const QueryEditor = ({ query, setQuery, onMount, language = 'lucene', fontSize =
26
26
  _monaco.languages.register({ id: 'lucene' });
27
27
  _monaco.languages.register({ id: 'eql' });
28
28
  }, []);
29
- useEffect(() => {
30
- const handleKeyPress = event => {
31
- if (setFzfSearch && event.ctrlKey && event.key == 'r') {
32
- event.preventDefault();
33
- setFzfSearch(!fzfSearch);
34
- }
35
- };
36
- window.addEventListener('keydown', handleKeyPress);
37
- return () => {
38
- window.removeEventListener('keydown', handleKeyPress);
39
- };
40
- }, [fzfSearch, setFzfSearch]);
41
29
  useEffect(() => {
42
30
  if (!monaco) {
43
31
  return;
@@ -90,6 +78,13 @@ const QueryEditor = ({ query, setQuery, onMount, language = 'lucene', fontSize =
90
78
  fontSize,
91
79
  ...editorOptions
92
80
  }), [setQuery, fontSize, editorOptions]);
93
- return (_jsx(Box, { sx: { flex: 1 }, children: _jsx(ThemedEditor, { height: height, width: width, theme: theme.palette.mode === 'light' ? 'howler' : 'howler-dark', value: query, onChange: value => setQuery(value), beforeMount: beforeEditorMount, onMount: onMount, options: options }) }));
81
+ const handleKeyPress = useCallback((event) => {
82
+ if (setFzfSearch && event.ctrlKey && event.key == 'r') {
83
+ event.preventDefault();
84
+ event.stopPropagation();
85
+ setFzfSearch(!fzfSearch);
86
+ }
87
+ }, [fzfSearch, setFzfSearch]);
88
+ return (_jsx(Box, { sx: { flex: 1 }, onKeyDownCapture: handleKeyPress, children: _jsx(ThemedEditor, { height: height, width: width, theme: theme.palette.mode === 'light' ? 'howler' : 'howler-dark', value: query, onChange: value => setQuery(value), beforeMount: beforeEditorMount, onMount: onMount, options: options }) }));
94
89
  };
95
90
  export default memo(QueryEditor);
@@ -1,4 +1,4 @@
1
- import 'chartjs-adapter-moment';
1
+ import 'chartjs-adapter-dayjs-4';
2
2
  import type { Analytic } from '@cccsaurora/howler-ui/models/entities/generated/Analytic';
3
3
  import type { FC } from 'react';
4
4
  declare const AnalyticOverview: FC<{
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Check, Edit } from '@mui/icons-material';
3
3
  import { Alert, AlertTitle, Box, Card, CardContent, CircularProgress, Divider, IconButton, Skeleton, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
- import 'chartjs-adapter-moment';
5
+ import 'chartjs-adapter-dayjs-4';
6
6
  import Markdown from '@cccsaurora/howler-ui/components/elements/display/Markdown';
7
7
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
8
8
  import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
@@ -1,4 +1,4 @@
1
- import 'chartjs-adapter-moment';
1
+ import 'chartjs-adapter-dayjs-4';
2
2
  import type { Analytic } from '@cccsaurora/howler-ui/models/entities/generated/Analytic';
3
3
  import { type FC } from 'react';
4
4
  declare const AnalyticOverviews: FC<{
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Article } from '@mui/icons-material';
3
3
  import { Box, Fab, Skeleton, Stack, Typography, useMediaQuery } from '@mui/material';
4
- import 'chartjs-adapter-moment';
4
+ import 'chartjs-adapter-dayjs-4';
5
5
  import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
6
6
  import { OverviewContext } from '@cccsaurora/howler-ui/components/app/providers/OverviewProvider';
7
7
  import { useContext, useEffect, useMemo, useState } from 'react';
@@ -4,7 +4,6 @@ import { AvatarGroup, Card, CardContent, CardHeader, Chip, Divider, Grid, IconBu
4
4
  import api from '@cccsaurora/howler-ui/api';
5
5
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
6
6
  import useLocalStorageItem from '@cccsaurora/howler-ui/commons/components/utils/hooks/useLocalStorageItem';
7
- import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
8
7
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
9
8
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
10
9
  import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
@@ -26,13 +25,26 @@ const AnalyticSearchBase = () => {
26
25
  const [searchParams, setSearchParams] = useSearchParams();
27
26
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
28
27
  const appUser = useAppUser();
29
- const { addFavourite, removeFavourite } = useContext(AnalyticContext);
30
28
  const [onlyRules, setOnlyRules] = useLocalStorageItem(StorageKey.ONLY_RULES, 0);
31
29
  const [searching, setSearching] = useState(false);
32
30
  const [hasError, setHasError] = useState(false);
33
31
  const [phrase, setPhrase] = useState(searchParams.get('phrase') || '');
34
32
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
35
33
  const [response, setResponse] = useState(null);
34
+ const addFavourite = useCallback(async (analytic) => {
35
+ await dispatchApi(api.analytic.favourite.post(analytic.analytic_id));
36
+ appUser.setUser({
37
+ ...appUser.user,
38
+ favourite_analytics: [...appUser.user.favourite_analytics, analytic.analytic_id]
39
+ });
40
+ }, [appUser, dispatchApi]);
41
+ const removeFavourite = useCallback(async (analytic) => {
42
+ await dispatchApi(api.analytic.favourite.del(analytic.analytic_id));
43
+ appUser.setUser({
44
+ ...appUser.user,
45
+ favourite_analytics: appUser.user.favourite_analytics.filter(v => v !== analytic.analytic_id)
46
+ });
47
+ }, [appUser, dispatchApi]);
36
48
  // Search Handler.
37
49
  const onSearch = useCallback(async () => {
38
50
  setSearching(true);
@@ -1,4 +1,4 @@
1
- import 'chartjs-adapter-moment';
1
+ import 'chartjs-adapter-dayjs-4';
2
2
  import type { Analytic } from '@cccsaurora/howler-ui/models/entities/generated/Analytic';
3
3
  import { type FC } from 'react';
4
4
  declare const AnalyticTemplates: FC<{
@@ -1,24 +1,27 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Article } from '@mui/icons-material';
3
3
  import { Box, Fab, Skeleton, Stack, Typography, useMediaQuery } from '@mui/material';
4
- import 'chartjs-adapter-moment';
4
+ import api from '@cccsaurora/howler-ui/api';
5
+ import 'chartjs-adapter-dayjs-4';
5
6
  import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
6
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
7
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
7
8
  import { useEffect, useState } from 'react';
8
9
  import { useTranslation } from 'react-i18next';
9
10
  import { Link } from 'react-router-dom';
10
- import { useContextSelector } from 'use-context-selector';
11
11
  import TemplateCard from '../templates/TemplateCard';
12
12
  const AnalyticTemplates = ({ analytic }) => {
13
13
  const { t } = useTranslation();
14
14
  const isNarrow = useMediaQuery('(max-width: 1800px)');
15
- const getTemplates = useContextSelector(TemplateContext, ctx => ctx.getTemplates);
16
- const templates = useContextSelector(TemplateContext, ctx => ctx.templates.filter(_template => _template.analytic === analytic?.name));
15
+ const { dispatchApi } = useMyApi();
16
+ const [templates, setTemplates] = useState([]);
17
17
  const [loading, setLoading] = useState(false);
18
18
  useEffect(() => {
19
19
  setLoading(true);
20
- getTemplates().finally(() => setLoading(false));
21
- }, [getTemplates]);
20
+ dispatchApi(api.template.get())
21
+ .then(_templates => _templates.filter(_template => _template.analytic === analytic?.name))
22
+ .then(setTemplates)
23
+ .finally(() => setLoading(false));
24
+ }, [analytic?.name, dispatchApi]);
22
25
  if (!analytic) {
23
26
  return _jsx(Skeleton, { variant: "rounded", width: "100%", sx: { minHeight: '300px', mt: 2 } });
24
27
  }
@@ -1,4 +1,4 @@
1
- import 'chartjs-adapter-moment';
1
+ import 'chartjs-adapter-dayjs-4';
2
2
  import type { Analytic } from '@cccsaurora/howler-ui/models/entities/generated/Analytic';
3
3
  import type { FC } from 'react';
4
4
  declare const RuleView: FC<{
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Check, Edit } from '@mui/icons-material';
3
3
  import { Box, CircularProgress, IconButton, Stack, Typography, useTheme } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
- import 'chartjs-adapter-moment';
5
+ import 'chartjs-adapter-dayjs-4';
6
6
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
7
7
  import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
8
8
  import { useCallback, useState } from 'react';
@@ -1,4 +1,4 @@
1
- import 'chartjs-adapter-moment';
1
+ import 'chartjs-adapter-dayjs-4';
2
2
  import type { Analytic } from '@cccsaurora/howler-ui/models/entities/generated/Analytic';
3
3
  import type { FC } from 'react';
4
4
  declare const TriageSettings: FC<{