@cccsaurora/howler-ui 2.13.0-dev.96 → 2.13.0
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.
- package/api/hit/index.d.ts +1 -1
- package/api/hit/index.js +6 -2
- package/api/search/index.d.ts +1 -0
- package/api/view/index.d.ts +1 -1
- package/api/view/index.js +2 -2
- package/commons/components/notification/elements/item/NotificationItemDate.js +2 -2
- package/commons/components/utils/hooks/useEnv.d.ts +1 -1
- package/components/app/App.js +1 -3
- package/components/app/drawers/ApiKeyDrawer.js +4 -4
- package/components/app/hooks/useMatchers.d.ts +9 -0
- package/components/app/hooks/useMatchers.js +82 -0
- package/components/app/hooks/useMatchers.test.d.ts +1 -0
- package/components/app/hooks/useMatchers.test.js +237 -0
- package/components/app/hooks/useTitle.js +5 -4
- package/components/app/providers/AnalyticProvider.d.ts +0 -4
- package/components/app/providers/AnalyticProvider.js +1 -44
- package/components/app/providers/ApiConfigProvider.js +2 -1
- package/components/app/providers/HitProvider.d.ts +2 -1
- package/components/app/providers/HitProvider.js +8 -2
- package/components/app/providers/HitSearchProvider.d.ts +2 -1
- package/components/app/providers/HitSearchProvider.js +2 -1
- package/components/app/providers/SocketProvider.js +1 -1
- package/components/app/providers/ViewProvider.d.ts +1 -1
- package/components/app/providers/ViewProvider.js +3 -3
- package/components/app/providers/ViewProvider.test.d.ts +1 -0
- package/components/app/providers/ViewProvider.test.js +150 -0
- package/components/elements/display/ActionButton.d.ts +8 -0
- package/components/elements/display/ActionButton.js +18 -0
- package/components/elements/display/handlebars/helpers.js +17 -2
- package/components/elements/display/json/JSONViewer.d.ts +2 -0
- package/components/elements/display/json/JSONViewer.js +6 -13
- package/components/elements/hit/HitActions.js +4 -6
- package/components/elements/hit/HitBanner.js +13 -5
- package/components/elements/hit/HitCard.js +0 -5
- package/components/elements/hit/HitComments.js +5 -4
- package/components/elements/hit/HitOutline.d.ts +2 -2
- package/components/elements/hit/HitOutline.js +11 -21
- package/components/elements/hit/HitOverview.js +7 -4
- package/components/elements/hit/HitSummary.d.ts +2 -1
- package/components/elements/hit/HitSummary.js +8 -7
- package/components/elements/hit/aggregate/HitGraph.d.ts +1 -1
- package/components/elements/hit/aggregate/HitGraph.js +7 -7
- package/components/elements/hit/elements/HitTimestamp.js +8 -8
- package/components/elements/hit/related/PivotLink.js +11 -5
- package/components/elements/hit/related/RelatedIcon.d.ts +8 -0
- package/components/elements/hit/related/RelatedIcon.js +32 -0
- package/components/elements/hit/related/RelatedLink.js +4 -25
- package/components/hooks/useMyChart.d.ts +1 -1
- package/components/hooks/useMyChart.js +1 -1
- package/components/routes/advanced/QueryBuilder.js +47 -11
- package/components/routes/advanced/QueryEditor.js +8 -13
- package/components/routes/analytics/AnalyticOverview.d.ts +1 -1
- package/components/routes/analytics/AnalyticOverview.js +1 -1
- package/components/routes/analytics/AnalyticOverviews.d.ts +1 -1
- package/components/routes/analytics/AnalyticOverviews.js +1 -1
- package/components/routes/analytics/AnalyticSearch.js +14 -2
- package/components/routes/analytics/AnalyticTemplates.d.ts +1 -1
- package/components/routes/analytics/AnalyticTemplates.js +10 -7
- package/components/routes/analytics/RuleView.d.ts +1 -1
- package/components/routes/analytics/RuleView.js +1 -1
- package/components/routes/analytics/TriageSettings.d.ts +1 -1
- package/components/routes/analytics/TriageSettings.js +1 -1
- package/components/routes/analytics/widgets/Assessment.d.ts +1 -1
- package/components/routes/analytics/widgets/Assessment.js +1 -1
- package/components/routes/analytics/widgets/Created.d.ts +1 -1
- package/components/routes/analytics/widgets/Created.js +1 -1
- package/components/routes/analytics/widgets/Detection.d.ts +1 -1
- package/components/routes/analytics/widgets/Detection.js +1 -1
- package/components/routes/analytics/widgets/Escalation.d.ts +1 -1
- package/components/routes/analytics/widgets/Escalation.js +1 -1
- package/components/routes/analytics/widgets/Stacked.d.ts +1 -1
- package/components/routes/analytics/widgets/Stacked.js +1 -1
- package/components/routes/analytics/widgets/Status.d.ts +1 -1
- package/components/routes/analytics/widgets/Status.js +1 -1
- package/components/routes/help/SearchDocumentation.js +2 -1
- package/components/routes/help/TemplateDocumentation.js +5 -5
- package/components/routes/hits/search/HitBrowser.js +2 -2
- package/components/routes/hits/search/HitContextMenu.js +6 -7
- package/components/routes/hits/search/HitQuery.js +2 -1
- package/components/routes/hits/search/InformationPane.js +75 -78
- package/components/routes/hits/search/SearchPane.js +3 -9
- package/components/routes/hits/search/grid/AddColumnModal.js +10 -5
- package/components/routes/hits/search/grid/HitGrid.js +6 -5
- package/components/routes/hits/search/shared/CustomSpan.js +6 -6
- package/components/routes/hits/view/HitViewer.js +18 -26
- package/components/routes/home/index.js +4 -4
- package/components/routes/overviews/OverviewViewer.js +33 -31
- package/components/routes/settings/SecuritySection.js +2 -2
- package/components/routes/templates/TemplateViewer.js +27 -36
- package/components/routes/templates/Templates.js +4 -11
- package/components/routes/views/ViewComposer.js +8 -1
- package/components/routes/views/Views.js +25 -9
- package/index.js +7 -0
- package/locales/en/help/search.json +17 -0
- package/locales/en/translation.json +12 -3
- package/locales/fr/help/search.json +17 -0
- package/locales/fr/translation.json +12 -4
- package/models/WithMetadata.d.ts +10 -0
- package/models/WithMetadata.js +1 -0
- package/models/entities/generated/ApiType.d.ts +7 -0
- package/package.json +112 -111
- package/plugins/borealis/components/BorealisTypography.js +4 -2
- package/setupTests.d.ts +1 -0
- package/setupTests.js +12 -0
- package/tests/MockLocalStorage.d.ts +5 -0
- package/tests/MockLocalStorage.js +44 -0
- package/tests/server-handlers.d.ts +5 -0
- package/tests/server-handlers.js +97 -0
- package/tests/server.d.ts +3 -0
- package/tests/server.js +5 -0
- package/utils/constants.js +2 -2
- package/utils/stringUtils.d.ts +1 -0
- package/utils/stringUtils.js +9 -0
- package/utils/utils.js +3 -3
- package/components/app/providers/DossierProvider.d.ts +0 -16
- package/components/app/providers/DossierProvider.js +0 -82
- package/components/app/providers/TemplateProvider.d.ts +0 -14
- 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
|
|
52
|
-
|
|
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]
|
|
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')
|
|
@@ -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-
|
|
5
|
+
import 'chartjs-adapter-dayjs';
|
|
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
|
|
99
|
+
const createdDate = dayjs(hit.event?.created ?? hit.timestamp);
|
|
100
100
|
return {
|
|
101
|
-
x:
|
|
102
|
-
y:
|
|
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} (${
|
|
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
|
|
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
|
|
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(() =>
|
|
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
|
|
23
|
+
return dayjs(earliest).isBefore(current) ? earliest : current;
|
|
24
24
|
});
|
|
25
25
|
return earliestDate;
|
|
26
26
|
}, [hit]);
|
|
27
27
|
const color = useMemo(() => {
|
|
28
|
-
if (
|
|
28
|
+
if (dayjs(timestamp).isBefore(threshold.clone().add(2, 'weeks'))) {
|
|
29
29
|
return 'error';
|
|
30
30
|
}
|
|
31
|
-
if (
|
|
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 (
|
|
37
|
+
if (dayjs(timestamp).isBefore(threshold)) {
|
|
38
38
|
return t('retention.imminent');
|
|
39
39
|
}
|
|
40
|
-
const diff =
|
|
41
|
-
const _duration =
|
|
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
|
|
34
|
+
return _jsx(RelatedLink, { title: pivot.label[i18n.language], href: href, compact: compact, icon: pivot.icon });
|
|
36
35
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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,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 {
|
|
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, {
|
|
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 ||
|
|
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,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-
|
|
3
|
+
import 'chartjs-adapter-dayjs';
|
|
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
|
|
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: ${
|
|
44
|
-
modified: ${
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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:
|
|
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: {
|
|
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
|
-
|
|
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);
|
|
@@ -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-
|
|
5
|
+
import 'chartjs-adapter-dayjs';
|
|
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,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-
|
|
4
|
+
import 'chartjs-adapter-dayjs';
|
|
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,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 '
|
|
4
|
+
import api from '@cccsaurora/howler-ui/api';
|
|
5
|
+
import 'chartjs-adapter-dayjs';
|
|
5
6
|
import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
|
|
6
|
-
import
|
|
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
|
|
16
|
-
const templates
|
|
15
|
+
const { dispatchApi } = useMyApi();
|
|
16
|
+
const [templates, setTemplates] = useState([]);
|
|
17
17
|
const [loading, setLoading] = useState(false);
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
setLoading(true);
|
|
20
|
-
|
|
21
|
-
|
|
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
|
}
|
|
@@ -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-
|
|
5
|
+
import 'chartjs-adapter-dayjs';
|
|
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';
|