@cccsaurora/howler-ui 2.13.0-dev.107 → 2.13.0-dev.110
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/components/app/App.js +1 -3
- package/components/app/hooks/useMatchers.d.ts +8 -0
- package/components/app/hooks/useMatchers.js +37 -0
- package/components/app/hooks/useTitle.js +5 -4
- 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/elements/display/handlebars/helpers.js +5 -2
- package/components/elements/hit/HitCard.js +0 -5
- 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 +7 -6
- package/components/elements/hit/related/PivotLink.js +10 -3
- package/components/routes/analytics/AnalyticTemplates.js +9 -6
- package/components/routes/hits/search/InformationPane.js +23 -40
- package/components/routes/hits/search/SearchPane.js +0 -6
- package/components/routes/hits/search/grid/AddColumnModal.js +10 -5
- package/components/routes/hits/view/HitViewer.js +17 -23
- package/components/routes/templates/TemplateViewer.js +27 -36
- package/components/routes/templates/Templates.js +4 -11
- package/models/WithMetadata.d.ts +8 -0
- package/models/WithMetadata.js +1 -0
- package/package.json +1 -1
- 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
package/api/hit/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type HitActionResponse = {
|
|
|
20
20
|
success: boolean;
|
|
21
21
|
};
|
|
22
22
|
export declare const uri: (id?: string) => string;
|
|
23
|
-
export declare const get: (id: string) => Promise<
|
|
23
|
+
export declare const get: <T extends Hit>(id: string, metadata?: string[]) => Promise<T>;
|
|
24
24
|
interface PostResponse {
|
|
25
25
|
valid: Hit[];
|
|
26
26
|
invalid: {
|
package/api/hit/index.js
CHANGED
|
@@ -7,8 +7,12 @@ import * as transition from '@cccsaurora/howler-ui/api/hit/transition';
|
|
|
7
7
|
export const uri = (id) => {
|
|
8
8
|
return id ? joinAllUri(parentUri(), 'hit', id) : joinUri(parentUri(), 'hit');
|
|
9
9
|
};
|
|
10
|
-
export const get = (id) => {
|
|
11
|
-
|
|
10
|
+
export const get = (id, metadata) => {
|
|
11
|
+
const params = new URLSearchParams();
|
|
12
|
+
if (metadata) {
|
|
13
|
+
params.append('metadata', metadata.join(','));
|
|
14
|
+
}
|
|
15
|
+
return hget(uri(id), params);
|
|
12
16
|
};
|
|
13
17
|
export const post = (hits) => {
|
|
14
18
|
return hpost(uri(), hits);
|
package/api/search/index.d.ts
CHANGED
package/components/app/App.js
CHANGED
|
@@ -63,7 +63,6 @@ import AnalyticProvider from './providers/AnalyticProvider';
|
|
|
63
63
|
import ApiConfigProvider, { ApiConfigContext } from './providers/ApiConfigProvider';
|
|
64
64
|
import AvatarProvider from './providers/AvatarProvider';
|
|
65
65
|
import CustomPluginProvider from './providers/CustomPluginProvider';
|
|
66
|
-
import DossierProvider from './providers/DossierProvider';
|
|
67
66
|
import FavouriteProvider from './providers/FavouritesProvider';
|
|
68
67
|
import FieldProvider from './providers/FieldProvider';
|
|
69
68
|
import HitProvider from './providers/HitProvider';
|
|
@@ -72,7 +71,6 @@ import ModalProvider from './providers/ModalProvider';
|
|
|
72
71
|
import OverviewProvider from './providers/OverviewProvider';
|
|
73
72
|
import ParameterProvider from './providers/ParameterProvider';
|
|
74
73
|
import SocketProvider from './providers/SocketProvider';
|
|
75
|
-
import TemplateProvider from './providers/TemplateProvider';
|
|
76
74
|
import UserListProvider from './providers/UserListProvider';
|
|
77
75
|
import ViewProvider from './providers/ViewProvider';
|
|
78
76
|
loader.config({ monaco });
|
|
@@ -140,7 +138,7 @@ const MyAppProvider = ({ children }) => {
|
|
|
140
138
|
const mySitemap = useMySitemap();
|
|
141
139
|
const myUser = useMyUser();
|
|
142
140
|
const mySearch = useMySearch();
|
|
143
|
-
return (_jsx(ErrorBoundary, { children: _jsx(AppProvider, { preferences: myPreferences, theme: myTheme, sitemap: mySitemap, user: myUser, search: mySearch, children: _jsx(CustomPluginProvider, { children: _jsx(ErrorBoundary, { children: _jsx(ErrorBoundary, { children: _jsx(
|
|
141
|
+
return (_jsx(ErrorBoundary, { children: _jsx(AppProvider, { preferences: myPreferences, theme: myTheme, sitemap: mySitemap, user: myUser, search: mySearch, children: _jsx(CustomPluginProvider, { children: _jsx(ErrorBoundary, { children: _jsx(ErrorBoundary, { children: _jsx(ViewProvider, { children: _jsx(AvatarProvider, { children: _jsx(ModalProvider, { children: _jsx(FieldProvider, { children: _jsx(LocalStorageProvider, { children: _jsx(SocketProvider, { children: _jsx(HitProvider, { children: _jsx(OverviewProvider, { children: _jsx(AnalyticProvider, { children: _jsx(FavouriteProvider, { children: _jsx(UserListProvider, { children: children }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
|
|
144
142
|
};
|
|
145
143
|
const AppProviderWrapper = () => {
|
|
146
144
|
return (_jsx(I18nextProvider, { i18n: i18n, defaultNS: "translation", children: _jsx(ApiConfigProvider, { children: _jsx(PluginProvider, { pluginStore: howlerPluginStore.pluginStore, children: _jsxs(MyAppProvider, { children: [_jsx(MyApp, {}), _jsx(Modal, {})] }) }) }) }));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
2
|
+
import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
|
|
3
|
+
declare const useMatchers: () => {
|
|
4
|
+
getMatchingDossiers: (hit: WithMetadata<Hit>) => Promise<import("../../../models/entities/generated/Dossier").Dossier[]>;
|
|
5
|
+
getMatchingOverview: (hit: WithMetadata<Hit>) => Promise<import("../../../models/entities/generated/Overview").Overview>;
|
|
6
|
+
getMatchingTemplate: (hit: WithMetadata<Hit>) => Promise<import("../../../models/entities/generated/Template").Template>;
|
|
7
|
+
};
|
|
8
|
+
export default useMatchers;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { has } from 'lodash-es';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { useContextSelector } from 'use-context-selector';
|
|
4
|
+
import { HitContext } from '../providers/HitProvider';
|
|
5
|
+
const useMatchers = () => {
|
|
6
|
+
const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
|
|
7
|
+
const getMatchingTemplate = useCallback(async (hit) => {
|
|
8
|
+
if (has(hit, '__template')) {
|
|
9
|
+
return hit.__template;
|
|
10
|
+
}
|
|
11
|
+
// This is a fallback in case metadata is not included. In most cases templates are shown, the template metadata
|
|
12
|
+
// should also exist
|
|
13
|
+
return (await getHit(hit.howler.id, true)).__template;
|
|
14
|
+
}, [getHit]);
|
|
15
|
+
const getMatchingOverview = useCallback(async (hit) => {
|
|
16
|
+
if (has(hit, '__overview')) {
|
|
17
|
+
return hit.__overview;
|
|
18
|
+
}
|
|
19
|
+
// This is a fallback in case metadata is not included. In most cases templates are shown, the template metadata
|
|
20
|
+
// should also exist
|
|
21
|
+
return (await getHit(hit.howler.id, true)).__overview;
|
|
22
|
+
}, [getHit]);
|
|
23
|
+
const getMatchingDossiers = useCallback(async (hit) => {
|
|
24
|
+
if (has(hit, '__dossiers')) {
|
|
25
|
+
return hit.__dossiers;
|
|
26
|
+
}
|
|
27
|
+
// This is a fallback in case metadata is not included. In most cases templates are shown, the template metadata
|
|
28
|
+
// should also exist
|
|
29
|
+
return (await getHit(hit.howler.id, true)).__dossiers;
|
|
30
|
+
}, [getHit]);
|
|
31
|
+
return {
|
|
32
|
+
getMatchingDossiers,
|
|
33
|
+
getMatchingOverview,
|
|
34
|
+
getMatchingTemplate
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export default useMatchers;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import api from '@cccsaurora/howler-ui/api';
|
|
2
1
|
import useMySitemap from '@cccsaurora/howler-ui/components/hooks/useMySitemap';
|
|
3
2
|
import { capitalize } from 'lodash-es';
|
|
4
3
|
import { useCallback, useContext, useEffect } from 'react';
|
|
5
4
|
import { useTranslation } from 'react-i18next';
|
|
6
5
|
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
|
|
6
|
+
import { useContextSelector } from 'use-context-selector';
|
|
7
7
|
import { AnalyticContext } from '../providers/AnalyticProvider';
|
|
8
|
+
import { HitContext } from '../providers/HitProvider';
|
|
8
9
|
const useTitle = () => {
|
|
9
10
|
const { t } = useTranslation();
|
|
10
11
|
const location = useLocation();
|
|
@@ -12,6 +13,7 @@ const useTitle = () => {
|
|
|
12
13
|
const searchParams = useSearchParams()[0];
|
|
13
14
|
const sitemap = useMySitemap();
|
|
14
15
|
const { getAnalyticFromId } = useContext(AnalyticContext);
|
|
16
|
+
const hits = useContextSelector(HitContext, ctx => ctx.hits);
|
|
15
17
|
const setTitle = useCallback((title) => {
|
|
16
18
|
document.querySelector('title').innerHTML = title;
|
|
17
19
|
}, []);
|
|
@@ -32,8 +34,7 @@ const useTitle = () => {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
else if (searchType === 'hit' && params.id) {
|
|
35
|
-
const
|
|
36
|
-
const hit = result.items[0];
|
|
37
|
+
const hit = hits[params.id];
|
|
37
38
|
let newTitle = `${capitalize(hit.howler.escalation)} - ${hit.howler.analytic}`;
|
|
38
39
|
if (hit.howler.detection) {
|
|
39
40
|
newTitle += `: ${hit.howler.detection}`;
|
|
@@ -59,7 +60,7 @@ const useTitle = () => {
|
|
|
59
60
|
setTitle(`Howler - ${t(matchingRoute.title)}`);
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
|
-
}, [getAnalyticFromId, location.pathname, params.id, searchParams, setTitle, sitemap.routes, t]);
|
|
63
|
+
}, [getAnalyticFromId, location.pathname, params.id, searchParams, hits, setTitle, sitemap.routes, t]);
|
|
63
64
|
useEffect(() => {
|
|
64
65
|
runChecks();
|
|
65
66
|
}, [runChecks]);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
2
|
+
import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
|
|
2
3
|
import type { FC, PropsWithChildren } from 'react';
|
|
3
4
|
interface HitProviderType {
|
|
4
5
|
hits: {
|
|
@@ -10,7 +11,7 @@ interface HitProviderType {
|
|
|
10
11
|
clearSelectedHits: (except?: string) => void;
|
|
11
12
|
loadHits: (hits: Hit[]) => void;
|
|
12
13
|
updateHit: (newHit: Hit) => void;
|
|
13
|
-
getHit: (id: string, force?: boolean) => Promise<Hit
|
|
14
|
+
getHit: (id: string, force?: boolean) => Promise<WithMetadata<Hit>>;
|
|
14
15
|
}
|
|
15
16
|
export declare const HitContext: import("use-context-selector").Context<HitProviderType>;
|
|
16
17
|
/**
|
|
@@ -32,7 +32,13 @@ const HitProvider = ({ children }) => {
|
|
|
32
32
|
// eslint-disable-next-line no-console
|
|
33
33
|
console.debug('Received websocket update for hit', data.hit.howler.id);
|
|
34
34
|
hitRequests.current[data.hit.howler.id] = Promise.resolve(data.hit);
|
|
35
|
-
setHits(_hits => ({
|
|
35
|
+
setHits(_hits => ({
|
|
36
|
+
..._hits,
|
|
37
|
+
[data.hit.howler.id]: {
|
|
38
|
+
..._hits[data.hit.howler.id],
|
|
39
|
+
...data.hit
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
36
42
|
}
|
|
37
43
|
}, []);
|
|
38
44
|
useEffect(() => {
|
|
@@ -45,7 +51,7 @@ const HitProvider = ({ children }) => {
|
|
|
45
51
|
*/
|
|
46
52
|
const getHit = useCallback(async (id, force = false) => {
|
|
47
53
|
if (!hitRequests.current[id] || force) {
|
|
48
|
-
hitRequests.current[id] = dispatchApi(api.hit.get(id));
|
|
54
|
+
hitRequests.current[id] = dispatchApi(api.hit.get(id, ['template', 'dossiers', 'analytic', 'overview']));
|
|
49
55
|
const newHit = await hitRequests.current[id];
|
|
50
56
|
setHits(_hits => ({ ..._hits, [id]: newHit }));
|
|
51
57
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HowlerSearchResponse } from '@cccsaurora/howler-ui/api/search';
|
|
2
2
|
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
3
3
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
4
|
+
import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
|
|
4
5
|
import { type Dispatch, type FC, type PropsWithChildren, type SetStateAction } from 'react';
|
|
5
6
|
export interface QueryEntry {
|
|
6
7
|
[query: string]: string;
|
|
@@ -10,7 +11,7 @@ interface HitSearchProviderType {
|
|
|
10
11
|
displayType: 'list' | 'grid';
|
|
11
12
|
searching: boolean;
|
|
12
13
|
error: string | null;
|
|
13
|
-
response: HowlerSearchResponse<Hit
|
|
14
|
+
response: HowlerSearchResponse<WithMetadata<Hit>> | null;
|
|
14
15
|
viewId: string | null;
|
|
15
16
|
bundleId: string | null;
|
|
16
17
|
queryHistory: QueryEntry;
|
|
@@ -86,7 +86,8 @@ const HitSearchProvider = ({ children }) => {
|
|
|
86
86
|
query: fullQuery,
|
|
87
87
|
sort,
|
|
88
88
|
filters,
|
|
89
|
-
track_total_hits: trackTotalHits
|
|
89
|
+
track_total_hits: trackTotalHits,
|
|
90
|
+
metadata: ['template', 'overview', 'analytic']
|
|
90
91
|
}), { showError: false, throwError: true });
|
|
91
92
|
if (_response.total < offset) {
|
|
92
93
|
setOffset(0);
|
|
@@ -121,7 +121,7 @@ const SocketProvider = ({ children }) => {
|
|
|
121
121
|
setRetry(false);
|
|
122
122
|
// Here we go!
|
|
123
123
|
setStatus(Status.CONNECTING);
|
|
124
|
-
const host = window.location.host;
|
|
124
|
+
const host = window.location.host.includes('localhost') ? 'localhost:5000' : window.location.host;
|
|
125
125
|
const protocol = window.location.protocol.startsWith('http:') ? 'ws' : 'wss';
|
|
126
126
|
const ws = new WebSocket(`${protocol}://${host}/socket/v1/connect`);
|
|
127
127
|
// Add our listeners to the websocket
|
|
@@ -11,6 +11,7 @@ import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
|
|
|
11
11
|
import { useMemo } from 'react';
|
|
12
12
|
import { usePluginStore } from 'react-pluggable';
|
|
13
13
|
import JSONViewer from '../json/JSONViewer';
|
|
14
|
+
const FETCH_RESULTS = {};
|
|
14
15
|
export const useHelpers = () => {
|
|
15
16
|
const pluginStore = usePluginStore();
|
|
16
17
|
const allHelpers = useMemo(() => [
|
|
@@ -55,8 +56,10 @@ export const useHelpers = () => {
|
|
|
55
56
|
documentation: 'Fetches the url provided and returns the given (flattened) key from the returned JSON object. Note that the result must be JSON!',
|
|
56
57
|
callback: async (url, key) => {
|
|
57
58
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
if (!FETCH_RESULTS[url]) {
|
|
60
|
+
FETCH_RESULTS[url] = fetch(url).then(res => res.json());
|
|
61
|
+
}
|
|
62
|
+
const json = await FETCH_RESULTS[url];
|
|
60
63
|
return flatten(json)[key];
|
|
61
64
|
}
|
|
62
65
|
catch (e) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { CardContent, Skeleton } from '@mui/material';
|
|
3
3
|
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
4
|
-
import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
|
|
5
4
|
import { memo, useEffect } from 'react';
|
|
6
5
|
import { useContextSelector } from 'use-context-selector';
|
|
7
6
|
import HowlerCard from '../display/HowlerCard';
|
|
@@ -10,12 +9,8 @@ import HitLabels from './HitLabels';
|
|
|
10
9
|
import { HitLayout } from './HitLayout';
|
|
11
10
|
import HitOutline from './HitOutline';
|
|
12
11
|
const HitCard = ({ id, layout, readOnly = true }) => {
|
|
13
|
-
const refresh = useContextSelector(TemplateContext, ctx => ctx.refresh);
|
|
14
12
|
const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
|
|
15
13
|
const hit = useContextSelector(HitContext, ctx => ctx.hits[id]);
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
refresh();
|
|
18
|
-
}, [refresh]);
|
|
19
14
|
useEffect(() => {
|
|
20
15
|
if (!hit) {
|
|
21
16
|
getHit(id);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
2
|
+
import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
|
|
2
3
|
import { HitLayout } from './HitLayout';
|
|
3
4
|
export declare const DEFAULT_FIELDS: string[];
|
|
4
5
|
declare const _default: import("react").NamedExoticComponent<{
|
|
5
|
-
hit: Hit
|
|
6
|
+
hit: WithMetadata<Hit>;
|
|
6
7
|
layout: HitLayout;
|
|
7
|
-
type?: "global" | "personal";
|
|
8
8
|
}>;
|
|
9
9
|
export default _default;
|
|
@@ -1,30 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Divider,
|
|
3
|
-
import
|
|
4
|
-
import { createElement, memo, useMemo } from 'react';
|
|
2
|
+
import { Box, Divider, Typography } from '@mui/material';
|
|
3
|
+
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
4
|
+
import { createElement, memo, useEffect, useMemo, useState } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import { useContextSelector } from 'use-context-selector';
|
|
7
6
|
import { HitLayout } from './HitLayout';
|
|
8
7
|
import DefaultOutline from './outlines/DefaultOutline';
|
|
9
8
|
export const DEFAULT_FIELDS = ['howler.hash'];
|
|
10
|
-
const HitOutline = ({ hit, layout
|
|
9
|
+
const HitOutline = ({ hit, layout }) => {
|
|
11
10
|
const { t } = useTranslation();
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
11
|
+
const { getMatchingTemplate } = useMatchers();
|
|
12
|
+
const [template, setTemplate] = useState(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
getMatchingTemplate(hit).then(setTemplate);
|
|
15
|
+
}, [getMatchingTemplate, hit]);
|
|
15
16
|
const outline = useMemo(() => {
|
|
16
|
-
if (template
|
|
17
|
-
return createElement(DefaultOutline, {
|
|
18
|
-
hit,
|
|
19
|
-
layout,
|
|
20
|
-
template,
|
|
21
|
-
fields: template.keys
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
else if (!loaded) {
|
|
25
|
-
return _jsx(Skeleton, { variant: "rounded", height: "50px" });
|
|
26
|
-
}
|
|
27
|
-
else if (template) {
|
|
17
|
+
if (template) {
|
|
28
18
|
return createElement(DefaultOutline, {
|
|
29
19
|
hit,
|
|
30
20
|
layout,
|
|
@@ -40,7 +30,7 @@ const HitOutline = ({ hit, layout, type }) => {
|
|
|
40
30
|
fields: DEFAULT_FIELDS
|
|
41
31
|
});
|
|
42
32
|
}
|
|
43
|
-
}, [hit, layout,
|
|
33
|
+
}, [hit, layout, template]);
|
|
44
34
|
return (_jsxs(Box, { sx: { py: 1, width: '100%', pr: 2 }, children: [layout === HitLayout.COMFY && (_jsx(Typography, { variant: "body1", fontWeight: "bold", sx: { mb: 1 }, children: t('hit.details.title') })), layout !== HitLayout.DENSE && _jsx(Divider, { orientation: "horizontal", sx: { mb: 1 } }), outline] }));
|
|
45
35
|
};
|
|
46
36
|
export default memo(HitOutline);
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { InsertLink } from '@mui/icons-material';
|
|
3
3
|
import { Box, IconButton, Skeleton } from '@mui/material';
|
|
4
|
-
import
|
|
4
|
+
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
5
5
|
import ErrorBoundary from '@cccsaurora/howler-ui/components/routes/ErrorBoundary';
|
|
6
|
-
import { memo,
|
|
6
|
+
import { memo, useEffect, useMemo, useState } from 'react';
|
|
7
7
|
import { Link } from 'react-router-dom';
|
|
8
8
|
import HandlebarsMarkdown from '../display/HandlebarsMarkdown';
|
|
9
9
|
const HitOverview = ({ content, hit }) => {
|
|
10
|
-
const { getMatchingOverview } =
|
|
11
|
-
const matchingOverview =
|
|
10
|
+
const { getMatchingOverview } = useMatchers();
|
|
11
|
+
const [matchingOverview, setMatchingOverview] = useState(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
getMatchingOverview(hit).then(setMatchingOverview);
|
|
14
|
+
}, [getMatchingOverview, hit]);
|
|
12
15
|
const link = useMemo(() => matchingOverview
|
|
13
16
|
? `/overviews/view?analytic=${encodeURIComponent(matchingOverview.analytic)}${matchingOverview.detection && '&detection=' + encodeURIComponent(matchingOverview.detection)}`
|
|
14
17
|
: hit
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { HowlerSearchResponse } from '@cccsaurora/howler-ui/api/search';
|
|
2
2
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
3
|
+
import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
|
|
3
4
|
declare const _default: import("react").NamedExoticComponent<{
|
|
4
5
|
query: string;
|
|
5
|
-
response?: HowlerSearchResponse<Hit
|
|
6
|
+
response?: HowlerSearchResponse<WithMetadata<Hit>>;
|
|
6
7
|
execute?: boolean;
|
|
7
8
|
onStart?: () => void;
|
|
8
9
|
onComplete?: () => void;
|
|
@@ -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
|
|
@@ -34,9 +34,16 @@ const PivotLink = ({ pivot, hit, compact = false }) => {
|
|
|
34
34
|
if (href) {
|
|
35
35
|
return (_jsx(RelatedLink, { title: pivot.label[i18n.language], href: href, compact: compact, children: _jsx(Icon, { fontSize: "1.5rem", icon: pivot.icon }) }));
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
try {
|
|
38
|
+
const pluginPivot = pluginStore.executeFunction(`pivot.${pivot.format}`, { pivot, hit, compact });
|
|
39
|
+
if (pluginPivot) {
|
|
40
|
+
return pluginPivot;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
// eslint-disable-next-line no-console
|
|
45
|
+
console.warn(`Pivot plugin for format ${pivot.format} does not exist, not rendering`);
|
|
46
|
+
return null;
|
|
40
47
|
}
|
|
41
48
|
return _jsx(Skeleton, { variant: "rounded" });
|
|
42
49
|
};
|
|
@@ -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 api from '@cccsaurora/howler-ui/api';
|
|
4
5
|
import 'chartjs-adapter-moment';
|
|
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
|
}
|
|
@@ -3,10 +3,9 @@ import { Clear, Code, Comment, DataObject, History, LinkSharp, OpenInNew, QueryS
|
|
|
3
3
|
import { Badge, Box, Divider, Skeleton, Stack, Tab, Tabs, Tooltip, useTheme } from '@mui/material';
|
|
4
4
|
import TuiIconButton from '@cccsaurora/howler-ui/components/elements/addons/buttons/CustomIconButton';
|
|
5
5
|
import { Icon } from '@iconify/react/dist/iconify.js';
|
|
6
|
+
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
6
7
|
import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
|
|
7
|
-
import { DossierContext } from '@cccsaurora/howler-ui/components/app/providers/DossierProvider';
|
|
8
8
|
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
9
|
-
import { OverviewContext } from '@cccsaurora/howler-ui/components/app/providers/OverviewProvider';
|
|
10
9
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
11
10
|
import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
|
|
12
11
|
import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
|
|
@@ -48,13 +47,13 @@ const InformationPane = ({ onClose }) => {
|
|
|
48
47
|
const location = useLocation();
|
|
49
48
|
const { emit, isOpen } = useContext(SocketContext);
|
|
50
49
|
const { getAnalyticFromName } = useContext(AnalyticContext);
|
|
51
|
-
const { getMatchingOverview,
|
|
50
|
+
const { getMatchingOverview, getMatchingDossiers } = useMatchers();
|
|
52
51
|
const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
|
|
53
52
|
const pluginStore = usePluginStore();
|
|
54
|
-
const getMatchingDossiers = useContextSelector(DossierContext, ctx => ctx.getMatchingDossiers);
|
|
55
53
|
const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
|
|
56
54
|
const [userIds, setUserIds] = useState(new Set());
|
|
57
55
|
const [analytic, setAnalytic] = useState();
|
|
56
|
+
const [overview, setOverview] = useState();
|
|
58
57
|
const [tab, setTab] = useState('overview');
|
|
59
58
|
const [loading, setLoading] = useState(false);
|
|
60
59
|
const [dossiers, setDossiers] = useState([]);
|
|
@@ -67,29 +66,22 @@ const InformationPane = ({ onClose }) => {
|
|
|
67
66
|
if (!selected) {
|
|
68
67
|
return;
|
|
69
68
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
else if (!hit?.howler.data) {
|
|
82
|
-
getHit(selected, true);
|
|
83
|
-
}
|
|
84
|
-
setUserIds(getUserList(hit));
|
|
85
|
-
setAnalytic(await getAnalyticFromName(hit.howler.analytic));
|
|
86
|
-
if (tab === 'hit_aggregate' && !hit.howler.is_bundle) {
|
|
87
|
-
setTab('overview');
|
|
88
|
-
}
|
|
89
|
-
})();
|
|
69
|
+
if (!hit?.howler.data) {
|
|
70
|
+
setLoading(true);
|
|
71
|
+
getHit(selected, true).finally(() => setLoading(false));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setUserIds(getUserList(hit));
|
|
75
|
+
getAnalyticFromName(hit.howler.analytic).then(setAnalytic);
|
|
76
|
+
getMatchingOverview(hit).then(setOverview);
|
|
77
|
+
getMatchingDossiers(hit).then(setDossiers);
|
|
90
78
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
91
|
-
}, [getAnalyticFromName, getHit, selected
|
|
92
|
-
|
|
79
|
+
}, [getAnalyticFromName, getHit, selected]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (tab === 'hit_aggregate' && !hit?.howler.is_bundle) {
|
|
82
|
+
setTab('overview');
|
|
83
|
+
}
|
|
84
|
+
}, [hit?.howler.is_bundle, tab]);
|
|
93
85
|
useEffect(() => {
|
|
94
86
|
if (selected && isOpen()) {
|
|
95
87
|
emit({
|
|
@@ -105,17 +97,14 @@ const InformationPane = ({ onClose }) => {
|
|
|
105
97
|
}
|
|
106
98
|
}, [emit, selected, isOpen]);
|
|
107
99
|
useEffect(() => {
|
|
108
|
-
|
|
109
|
-
}, [refresh]);
|
|
110
|
-
useEffect(() => {
|
|
111
|
-
if (matchingOverview && tab === 'details') {
|
|
100
|
+
if (overview && tab === 'details') {
|
|
112
101
|
setTab('overview');
|
|
113
102
|
}
|
|
114
|
-
else if (!
|
|
103
|
+
else if (!overview && tab === 'overview') {
|
|
115
104
|
setTab('details');
|
|
116
105
|
}
|
|
117
106
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
118
|
-
}, [
|
|
107
|
+
}, [overview]);
|
|
119
108
|
/**
|
|
120
109
|
* What to show as the header? If loading a skeleton, then it depends on bundle or not. Bundles don't
|
|
121
110
|
* show anything while normal hits do
|
|
@@ -151,12 +140,6 @@ const InformationPane = ({ onClose }) => {
|
|
|
151
140
|
])))
|
|
152
141
|
}[tab]?.();
|
|
153
142
|
}, [dossiers, hit, loading, tab, users]);
|
|
154
|
-
useEffect(() => {
|
|
155
|
-
if (!hit) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
getMatchingDossiers(hit.howler.id).then(setDossiers);
|
|
159
|
-
}, [getMatchingDossiers, hit]);
|
|
160
143
|
return (_jsxs(VSBox, { top: 10, sx: { height: '100%', flex: 1 }, children: [_jsxs(Stack, { direction: "column", flex: 1, sx: { overflowY: 'auto', flexGrow: 1 }, position: "relative", spacing: 1, ml: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, flexShrink: 0, pr: 2, sx: [hit?.howler?.is_bundle && { position: 'absolute', top: 1, right: 0, zIndex: 1100 }], children: [_jsx(FlexOne, {}), onClose && !location.pathname.startsWith('/bundles') && (_jsx(TuiIconButton, { size: "small", onClick: onClose, tooltip: t('hit.panel.details.exit'), children: _jsx(Clear, {}) })), _jsx(SocketBadge, { size: "small" }), analytic && (_jsx(TuiIconButton, { size: "small", tooltip: t('hit.panel.analytic.open'), disabled: !analytic || loading, route: `/analytics/${analytic.analytic_id}`, children: _jsx(QueryStats, {}) })), hit?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles, disabled: loading }), !!hit && !hit.howler.is_bundle && (_jsx(TuiIconButton, { tooltip: t('hit.panel.open'), href: `/hits/${selected}`, disabled: !hit || loading, size: "small", target: "_blank", children: _jsx(OpenInNew, {}) }))] }), _jsx(Box, { pr: 2, children: header }), !!hit &&
|
|
161
144
|
!hit.howler.is_bundle &&
|
|
162
145
|
(!loading ? (_jsxs(_Fragment, { children: [_jsx(HitOutline, { hit: hit, layout: HitLayout.DENSE }), _jsx(HitLabels, { hit: hit })] })) : (_jsx(Skeleton, { height: 124 }))), (hit?.howler?.links?.length > 0 ||
|
|
@@ -166,7 +149,7 @@ const InformationPane = ({ onClose }) => {
|
|
|
166
149
|
.slice(0, 3)
|
|
167
150
|
.map(l => _jsx(RelatedLink, { compact: true, ...l }, l.href)), dossiers.flatMap(_dossier => (_dossier.pivots ?? []).map((_pivot, index) => (
|
|
168
151
|
// eslint-disable-next-line react/no-array-index-key
|
|
169
|
-
_jsx(PivotLink, { pivot: _pivot, hit: hit, compact: true }, _dossier.dossier_id + index))))] })), _jsx(VSBoxHeader, { ml: -1, mr: -1, pb: 1, sx: { top: '0px' }, children: _jsxs(Tabs, { value: tab === 'overview' && !
|
|
152
|
+
_jsx(PivotLink, { pivot: _pivot, hit: hit, compact: true }, _dossier.dossier_id + index))))] })), _jsx(VSBoxHeader, { ml: -1, mr: -1, pb: 1, sx: { top: '0px' }, children: _jsxs(Tabs, { value: tab === 'overview' && !overview ? 'details' : tab, sx: {
|
|
170
153
|
display: 'flex',
|
|
171
154
|
flexDirection: 'row',
|
|
172
155
|
pr: 2,
|
|
@@ -195,7 +178,7 @@ const InformationPane = ({ onClose }) => {
|
|
|
195
178
|
right: theme.spacing(-0.5)
|
|
196
179
|
},
|
|
197
180
|
'& > svg': { zIndex: 2 }
|
|
198
|
-
}, badgeContent: hit?.howler.comment?.length ?? 0, children: _jsx(Comment, {}) }) }), value: "hit_comments", onClick: () => setTab('hit_comments') }), hit?.howler?.is_bundle && (_jsx(Tab, { label: t('hit.viewer.aggregate'), value: "hit_aggregate", onClick: () => setTab('hit_aggregate') })),
|
|
181
|
+
}, badgeContent: hit?.howler.comment?.length ?? 0, children: _jsx(Comment, {}) }) }), value: "hit_comments", onClick: () => setTab('hit_comments') }), hit?.howler?.is_bundle && (_jsx(Tab, { label: t('hit.viewer.aggregate'), value: "hit_aggregate", onClick: () => setTab('hit_aggregate') })), overview && _jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') }), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
|
|
199
182
|
// eslint-disable-next-line react/no-array-index-key
|
|
200
183
|
, { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [lead.icon && _jsx(Icon, { icon: lead.icon }), _jsx("span", { children: i18n.language === 'en' ? lead.label.en : lead.label.fr })] }), value: 'lead:' + index, onClick: () => setTab('lead:' + index) }, 'lead:' + index))), dossiers.flatMap((_dossier, dossierIndex) => _dossier.leads?.map((_lead, leadIndex) => (_jsx(Tab
|
|
201
184
|
// eslint-disable-next-line react/no-array-index-key
|
|
@@ -7,7 +7,6 @@ import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCente
|
|
|
7
7
|
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
8
8
|
import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
|
|
9
9
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
10
|
-
import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
|
|
11
10
|
import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
|
|
12
11
|
import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
|
|
13
12
|
import FlexPort from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexPort';
|
|
@@ -82,7 +81,6 @@ const SearchPane = () => {
|
|
|
82
81
|
const location = useLocation();
|
|
83
82
|
const navigate = useNavigate();
|
|
84
83
|
const routeParams = useParams();
|
|
85
|
-
const refresh = useContextSelector(TemplateContext, ctx => ctx.refresh);
|
|
86
84
|
const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
|
|
87
85
|
const setSelected = useContextSelector(ParameterContext, ctx => ctx.setSelected);
|
|
88
86
|
const query = useContextSelector(ParameterContext, ctx => ctx.query);
|
|
@@ -109,10 +107,6 @@ const SearchPane = () => {
|
|
|
109
107
|
}
|
|
110
108
|
return selectedElement.id;
|
|
111
109
|
}, []);
|
|
112
|
-
// Load the index field for a hit in order to provide autocomplete suggestions.
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
refresh();
|
|
115
|
-
}, [refresh]);
|
|
116
110
|
useEffect(() => {
|
|
117
111
|
if (location.pathname.startsWith('/bundles')) {
|
|
118
112
|
getHit(routeParams.id);
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Add, Check } from '@mui/icons-material';
|
|
3
3
|
import { Autocomplete, Chip, Divider, Grid, IconButton, Popover, Stack, TextField } from '@mui/material';
|
|
4
|
+
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
4
5
|
import { FieldContext } from '@cccsaurora/howler-ui/components/app/providers/FieldProvider';
|
|
5
6
|
import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { memo, useContext, useMemo, useState } from 'react';
|
|
7
|
+
import { has, sortBy, uniq } from 'lodash-es';
|
|
8
|
+
import { memo, useContext, useEffect, useMemo, useState } from 'react';
|
|
9
9
|
import { useTranslation } from 'react-i18next';
|
|
10
10
|
import { useContextSelector } from 'use-context-selector';
|
|
11
11
|
const AddColumnModal = ({ open, onClose, anchorEl, addColumn, columns }) => {
|
|
12
12
|
const { t } = useTranslation();
|
|
13
13
|
const { hitFields } = useContext(FieldContext);
|
|
14
14
|
const response = useContextSelector(HitSearchContext, ctx => ctx.response);
|
|
15
|
-
const getMatchingTemplate =
|
|
15
|
+
const { getMatchingTemplate } = useMatchers();
|
|
16
16
|
const [columnToAdd, setColumnToAdd] = useState(null);
|
|
17
17
|
const options = useMemo(() => hitFields.map(field => field.key), [hitFields]);
|
|
18
|
-
const suggestions
|
|
18
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
(async () => {
|
|
21
|
+
setSuggestions(uniq((await Promise.all((response?.items ?? []).map(async (_hit) => (has(_hit, '__template') ? _hit.__template?.keys : (await getMatchingTemplate(_hit))?.keys) ?? []))).flat()));
|
|
22
|
+
})();
|
|
23
|
+
}, [getMatchingTemplate, response?.items]);
|
|
19
24
|
return (_jsx(Popover, { open: open, onClose: onClose, anchorEl: anchorEl, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, children: _jsxs(Stack, { spacing: 1, p: 1, width: "500px", children: [_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Autocomplete, { sx: { flex: 1 }, size: "small", options: options, value: columnToAdd, renderInput: params => _jsx(TextField, { fullWidth: true, placeholder: t('hit.fields'), ...params }), onChange: (_ev, value) => setColumnToAdd(value) }), _jsx(IconButton, { disabled: !columnToAdd, onClick: () => {
|
|
20
25
|
addColumn(columnToAdd);
|
|
21
26
|
setColumnToAdd(null);
|
|
@@ -3,11 +3,9 @@ import { Icon } from '@iconify/react/dist/iconify.js';
|
|
|
3
3
|
import { Code, Comment, DataObject, History, LinkSharp, QueryStats, ViewAgenda } from '@mui/icons-material';
|
|
4
4
|
import { Badge, Box, CardContent, Collapse, IconButton, Skeleton, Stack, Tab, Tabs, Tooltip, useMediaQuery, useTheme } from '@mui/material';
|
|
5
5
|
import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
|
|
6
|
+
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
6
7
|
import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
|
|
7
|
-
import { DossierContext } from '@cccsaurora/howler-ui/components/app/providers/DossierProvider';
|
|
8
8
|
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
9
|
-
import { OverviewContext } from '@cccsaurora/howler-ui/components/app/providers/OverviewProvider';
|
|
10
|
-
import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
|
|
11
9
|
import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
|
|
12
10
|
import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
|
|
13
11
|
import BundleButton from '@cccsaurora/howler-ui/components/elements/display/icons/BundleButton';
|
|
@@ -49,10 +47,8 @@ const HitViewer = () => {
|
|
|
49
47
|
const theme = useTheme();
|
|
50
48
|
const isUnderLg = useMediaQuery(theme.breakpoints.down('lg'));
|
|
51
49
|
const [orientation, setOrientation] = useMyLocalStorageItem(StorageKey.VIEWER_ORIENTATION, Orientation.VERTICAL);
|
|
52
|
-
const refreshTemplates = useContextSelector(TemplateContext, ctx => ctx.refresh);
|
|
53
50
|
const { getAnalyticFromName } = useContext(AnalyticContext);
|
|
54
|
-
const { getMatchingOverview,
|
|
55
|
-
const getMatchingDossiers = useContextSelector(DossierContext, ctx => ctx.getMatchingDossiers);
|
|
51
|
+
const { getMatchingOverview, getMatchingDossiers } = useMatchers();
|
|
56
52
|
const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
|
|
57
53
|
const hit = useContextSelector(HitContext, ctx => ctx.hits[params.id]);
|
|
58
54
|
const [userIds, setUserIds] = useState(new Set());
|
|
@@ -60,22 +56,22 @@ const HitViewer = () => {
|
|
|
60
56
|
const [tab, setTab] = useState('details');
|
|
61
57
|
const [analytic, setAnalytic] = useState();
|
|
62
58
|
const [dossiers, setDossiers] = useState([]);
|
|
59
|
+
const [hasOverview, setHasOverview] = useState(false);
|
|
63
60
|
const fetchData = useCallback(async () => {
|
|
64
61
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
if (!hit) {
|
|
63
|
+
await getHit(params.id, true);
|
|
64
|
+
return;
|
|
68
65
|
}
|
|
69
|
-
setUserIds(getUserList(
|
|
70
|
-
setAnalytic(await getAnalyticFromName(
|
|
66
|
+
setUserIds(getUserList(hit));
|
|
67
|
+
setAnalytic(await getAnalyticFromName(hit.howler.analytic));
|
|
71
68
|
}
|
|
72
69
|
catch (err) {
|
|
73
70
|
if (err.cause?.api_status_code === 404) {
|
|
74
71
|
navigate('/404');
|
|
75
72
|
}
|
|
76
73
|
}
|
|
77
|
-
|
|
78
|
-
}, [getAnalyticFromName, getHit, params.id, navigate]);
|
|
74
|
+
}, [hit, getAnalyticFromName, getHit, params.id, navigate]);
|
|
79
75
|
useEffect(() => {
|
|
80
76
|
if (isUnderLg) {
|
|
81
77
|
setOrientation(Orientation.HORIZONTAL);
|
|
@@ -83,22 +79,20 @@ const HitViewer = () => {
|
|
|
83
79
|
}, [isUnderLg, setOrientation]);
|
|
84
80
|
useEffect(() => {
|
|
85
81
|
fetchData();
|
|
86
|
-
}, [params.id, fetchData]);
|
|
82
|
+
}, [params.id, fetchData, hit]);
|
|
87
83
|
const onOrientationChange = useCallback(() => setOrientation(orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL), [orientation, setOrientation]);
|
|
88
|
-
const matchingOverview = useMemo(() => getMatchingOverview(hit), [getMatchingOverview, hit]);
|
|
89
84
|
useEffect(() => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}, [refreshOverviews, refreshTemplates]);
|
|
85
|
+
getMatchingOverview(hit).then(_overview => setHasOverview(!!_overview));
|
|
86
|
+
}, [getMatchingOverview, hit]);
|
|
93
87
|
useEffect(() => {
|
|
94
|
-
if (
|
|
88
|
+
if (hasOverview && tab === 'details') {
|
|
95
89
|
setTab('overview');
|
|
96
90
|
}
|
|
97
|
-
else if (!
|
|
91
|
+
else if (!hasOverview && tab === 'overview') {
|
|
98
92
|
setTab('details');
|
|
99
93
|
}
|
|
100
94
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
-
}, [
|
|
95
|
+
}, [hasOverview]);
|
|
102
96
|
const tabContent = useMemo(() => {
|
|
103
97
|
if (!tab || !hit) {
|
|
104
98
|
return;
|
|
@@ -122,7 +116,7 @@ const HitViewer = () => {
|
|
|
122
116
|
if (!hit) {
|
|
123
117
|
return;
|
|
124
118
|
}
|
|
125
|
-
getMatchingDossiers(hit
|
|
119
|
+
getMatchingDossiers(hit).then(setDossiers);
|
|
126
120
|
}, [getMatchingDossiers, hit]);
|
|
127
121
|
if (!hit) {
|
|
128
122
|
return (_jsx(PageCenter, { children: _jsx(Skeleton, { variant: "rounded", height: "520px" }) }));
|
|
@@ -150,7 +144,7 @@ const HitViewer = () => {
|
|
|
150
144
|
position: 'absolute',
|
|
151
145
|
top: theme.spacing(2),
|
|
152
146
|
right: theme.spacing(-6)
|
|
153
|
-
}, children: [_jsx(Tooltip, { title: t('page.hits.view.layout'), children: _jsx(IconButton, { onClick: onOrientationChange, children: _jsx(ViewAgenda, { sx: { transition: 'rotate 250ms', rotate: orientation === 'vertical' ? '90deg' : '0deg' } }) }) }), _jsx(SocketBadge, { size: "medium" }), analytic && (_jsx(Tooltip, { title: t('hit.panel.analytic.open'), children: _jsx(IconButton, { onClick: () => navigate(`/analytics/${analytic.analytic_id}`), children: _jsx(QueryStats, {}) }) })), hit?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles })] }))] }), _jsx(HowlerCard, { sx: [orientation === 'horizontal' && { height: '0px' }], children: _jsx(CardContent, { sx: { padding: 1, position: 'relative' }, children: _jsx(HitActions, { hit: hit, orientation: "vertical" }) }) }), _jsx(Box, { sx: { gridColumn: '1 / span 2', mb: 1 }, children: _jsxs(Tabs, { value: tab === 'overview' && !
|
|
147
|
+
}, children: [_jsx(Tooltip, { title: t('page.hits.view.layout'), children: _jsx(IconButton, { onClick: onOrientationChange, children: _jsx(ViewAgenda, { sx: { transition: 'rotate 250ms', rotate: orientation === 'vertical' ? '90deg' : '0deg' } }) }) }), _jsx(SocketBadge, { size: "medium" }), analytic && (_jsx(Tooltip, { title: t('hit.panel.analytic.open'), children: _jsx(IconButton, { onClick: () => navigate(`/analytics/${analytic.analytic_id}`), children: _jsx(QueryStats, {}) }) })), hit?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles })] }))] }), _jsx(HowlerCard, { sx: [orientation === 'horizontal' && { height: '0px' }], children: _jsx(CardContent, { sx: { padding: 1, position: 'relative' }, children: _jsx(HitActions, { hit: hit, orientation: "vertical" }) }) }), _jsx(Box, { sx: { gridColumn: '1 / span 2', mb: 1 }, children: _jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: { display: 'flex', flexDirection: 'row', pr: 2, alignItems: 'center' }, children: [hit?.howler?.is_bundle && (_jsx(Tab, { label: t('hit.viewer.aggregate'), value: "hit_aggregate", onClick: () => setTab('hit_aggregate') })), hasOverview && (_jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') })), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
|
|
154
148
|
// eslint-disable-next-line react/no-array-index-key
|
|
155
149
|
, { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [lead.icon && _jsx(Icon, { icon: lead.icon }), _jsx("span", { children: i18n.language === 'en' ? lead.label.en : lead.label.fr })] }), value: 'lead:' + index, onClick: () => setTab('lead:' + index) }, 'lead:' + index))), dossiers.flatMap((_dossier, dossierIndex) => _dossier.leads?.map((_lead, leadIndex) => (_jsx(Tab
|
|
156
150
|
// eslint-disable-next-line react/no-array-index-key
|
|
@@ -5,22 +5,17 @@ import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCente
|
|
|
5
5
|
import TemplateEditor from '@cccsaurora/howler-ui/components/routes/templates/TemplateEditor';
|
|
6
6
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
|
-
import { Check, Delete,
|
|
8
|
+
import { Check, Delete, SsidChart } from '@mui/icons-material';
|
|
9
9
|
import AppInfoPanel from '@cccsaurora/howler-ui/commons/components/display/AppInfoPanel';
|
|
10
|
-
import {
|
|
11
|
-
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
12
|
-
import HitOutline, { DEFAULT_FIELDS } from '@cccsaurora/howler-ui/components/elements/hit/HitOutline';
|
|
10
|
+
import { DEFAULT_FIELDS } from '@cccsaurora/howler-ui/components/elements/hit/HitOutline';
|
|
13
11
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
14
12
|
import isEqual from 'lodash-es/isEqual';
|
|
15
13
|
import { useSearchParams } from 'react-router-dom';
|
|
16
|
-
import { useContextSelector } from 'use-context-selector';
|
|
17
14
|
import hitsData from '@cccsaurora/howler-ui/utils/hit.json';
|
|
18
15
|
import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
19
|
-
const CUSTOM_OUTLINES = ['cmt.aws.sigma.rules', 'assemblyline', '6tailphish'];
|
|
20
16
|
const TemplateViewer = () => {
|
|
21
17
|
const { t } = useTranslation();
|
|
22
18
|
const [params, setParams] = useSearchParams();
|
|
23
|
-
const getTemplates = useContextSelector(TemplateContext, ctx => ctx.getTemplates);
|
|
24
19
|
const { dispatchApi } = useMyApi();
|
|
25
20
|
const [templateList, setTemplateList] = useState([]);
|
|
26
21
|
const [selectedTemplate, setSelectedTemplate] = useState(null);
|
|
@@ -47,32 +42,33 @@ const TemplateViewer = () => {
|
|
|
47
42
|
}
|
|
48
43
|
setAnalytics(_analytics);
|
|
49
44
|
});
|
|
50
|
-
|
|
45
|
+
dispatchApi(api.template.get()).then(setTemplateList);
|
|
51
46
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
47
|
}, [analytic, dispatchApi]);
|
|
53
48
|
useEffect(() => {
|
|
54
|
-
if (analytic) {
|
|
55
|
-
|
|
56
|
-
dispatchApi(api.search.grouped.hit.post('howler.detection', {
|
|
57
|
-
limit: 0,
|
|
58
|
-
query: `howler.analytic:"${sanitizeLuceneQuery(analytic)}"`
|
|
59
|
-
}), {
|
|
60
|
-
logError: false,
|
|
61
|
-
showError: true,
|
|
62
|
-
throwError: true
|
|
63
|
-
})
|
|
64
|
-
.finally(() => setLoading(false))
|
|
65
|
-
.then(result => result.items.map(i => i.value))
|
|
66
|
-
.then(_detections => {
|
|
67
|
-
if (_detections.length < 1 || (type === 'global' && CUSTOM_OUTLINES.includes(analytic.toLowerCase()))) {
|
|
68
|
-
setDetection('ANY');
|
|
69
|
-
}
|
|
70
|
-
if (detection && !_detections.includes(detection)) {
|
|
71
|
-
setDetection('ANY');
|
|
72
|
-
}
|
|
73
|
-
setDetections(_detections);
|
|
74
|
-
});
|
|
49
|
+
if (!analytic) {
|
|
50
|
+
return;
|
|
75
51
|
}
|
|
52
|
+
setLoading(true);
|
|
53
|
+
dispatchApi(api.search.grouped.hit.post('howler.detection', {
|
|
54
|
+
limit: 0,
|
|
55
|
+
query: `howler.analytic:"${sanitizeLuceneQuery(analytic)}"`
|
|
56
|
+
}), {
|
|
57
|
+
logError: false,
|
|
58
|
+
showError: true,
|
|
59
|
+
throwError: true
|
|
60
|
+
})
|
|
61
|
+
.finally(() => setLoading(false))
|
|
62
|
+
.then(result => result.items.map(i => i.value))
|
|
63
|
+
.then(_detections => {
|
|
64
|
+
if (_detections.length < 1) {
|
|
65
|
+
setDetection('ANY');
|
|
66
|
+
}
|
|
67
|
+
if (detection && !_detections.includes(detection)) {
|
|
68
|
+
setDetection('ANY');
|
|
69
|
+
}
|
|
70
|
+
setDetections(_detections);
|
|
71
|
+
});
|
|
76
72
|
}, [analytic, detection, dispatchApi, params, setParams, type]);
|
|
77
73
|
useEffect(() => {
|
|
78
74
|
if (analytic && detection) {
|
|
@@ -149,17 +145,12 @@ const TemplateViewer = () => {
|
|
|
149
145
|
}
|
|
150
146
|
}
|
|
151
147
|
}, [analytic, detection, dispatchApi, displayFields, selectedTemplate, templateList, type]);
|
|
152
|
-
const isCustomOutline = useMemo(() => CUSTOM_OUTLINES.includes(analytic.toLowerCase()), [analytic]);
|
|
153
148
|
const analyticOrDetectionMissing = useMemo(() => !analytic || !detection, [analytic, detection]);
|
|
154
149
|
const noFieldChange = useMemo(() => displayFields.length < 1 || isEqual(selectedTemplate?.keys ?? DEFAULT_FIELDS, displayFields), [displayFields, selectedTemplate?.keys]);
|
|
155
|
-
return (_jsxs(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: [_jsx(LinearProgress, { sx: { mb: 1, opacity: +loading } }), _jsxs(Stack, { direction: "column", spacing: 2, divider: _jsx(Divider, { orientation: "horizontal", flexItem: true }), height: "100%", children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(FormControl, { sx: { flex: 1, maxWidth: '450px' }, children: _jsx(Autocomplete, { id: "analytic", options: analytics.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())), getOptionLabel: option => option.name, value: analytics.find(a => a.name === analytic) || null, onChange: (__, newValue) => setAnalytic(newValue ? newValue.name : ''), renderInput: autocompleteAnalyticParams => (_jsx(TextField, { ...autocompleteAnalyticParams, label: t('route.templates.analytic'), size: "small" })) }) }), !(detections?.length < 2 && detections[0]?.toLowerCase() === 'rule') ? (_jsx(FormControl, { sx: { flex: 1, maxWidth: '300px' }, disabled: !analytic
|
|
150
|
+
return (_jsxs(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: [_jsx(LinearProgress, { sx: { mb: 1, opacity: +loading } }), _jsxs(Stack, { direction: "column", spacing: 2, divider: _jsx(Divider, { orientation: "horizontal", flexItem: true }), height: "100%", children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(FormControl, { sx: { flex: 1, maxWidth: '450px' }, children: _jsx(Autocomplete, { id: "analytic", options: analytics.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())), getOptionLabel: option => option.name, value: analytics.find(a => a.name === analytic) || null, onChange: (__, newValue) => setAnalytic(newValue ? newValue.name : ''), renderInput: autocompleteAnalyticParams => (_jsx(TextField, { ...autocompleteAnalyticParams, label: t('route.templates.analytic'), size: "small" })) }) }), !(detections?.length < 2 && detections[0]?.toLowerCase() === 'rule') ? (_jsx(FormControl, { sx: { flex: 1, maxWidth: '300px' }, disabled: !analytic, children: _jsx(Autocomplete, { id: "detection", options: ['ANY', ...detections.sort()], getOptionLabel: option => option, value: detection ?? '', onChange: (__, newValue) => setDetection(newValue), renderInput: autocompleteDetectionParams => (_jsx(TextField, { ...autocompleteDetectionParams, label: t('route.templates.detection'), size: "small" })) }) })) : (_jsx(Tooltip, { title: t('route.templates.rule.explanation'), children: _jsx(SsidChart, { color: "info", sx: { alignSelf: 'center' } }) })), _jsxs(ToggleButtonGroup, { sx: { display: 'grid', gridTemplateColumns: '1fr 1fr' }, size: "small", exclusive: true, value: type, disabled: analyticOrDetectionMissing, onChange: (__, _type) => {
|
|
156
151
|
if (_type) {
|
|
157
152
|
setType(_type);
|
|
158
153
|
}
|
|
159
|
-
}, children: [_jsx(ToggleButton, { sx: { flex: 1 }, value: "personal", "aria-label": "personal", children: t('route.templates.personal') }), _jsx(ToggleButton, { sx: { flex: 1 }, value: "global", "aria-label": "global", children: t('route.templates.global') })] }), selectedTemplate && (_jsx(Button, { variant: "outlined", startIcon: _jsx(Delete, {}), onClick: onDelete, children: t('button.delete') })), _jsx(Button, { variant: "outlined", disabled: analyticOrDetectionMissing ||
|
|
160
|
-
? 'button.readonly'
|
|
161
|
-
: !analyticOrDetectionMissing && !noFieldChange
|
|
162
|
-
? 'button.save'
|
|
163
|
-
: 'button.saved') })] }), isCustomOutline && type === 'global' ? (_jsx(HitOutline, { hit: exampleHit, layout: HitLayout.COMFY, type: "global" })) : analyticOrDetectionMissing ? (_jsx(AppInfoPanel, { i18nKey: "route.templates.select", sx: { width: '100%', alignSelf: 'start' } })) : (_jsx(TemplateEditor, { hit: exampleHit, fields: displayFields, setFields: setDisplayFields, onAdd: field => setDisplayFields([...displayFields, field]), onRemove: field => setDisplayFields(displayFields.filter(f => f !== field)) }))] })] }));
|
|
154
|
+
}, children: [_jsx(ToggleButton, { sx: { flex: 1 }, value: "personal", "aria-label": "personal", children: t('route.templates.personal') }), _jsx(ToggleButton, { sx: { flex: 1 }, value: "global", "aria-label": "global", children: t('route.templates.global') })] }), selectedTemplate && (_jsx(Button, { variant: "outlined", startIcon: _jsx(Delete, {}), onClick: onDelete, children: t('button.delete') })), _jsx(Button, { variant: "outlined", disabled: analyticOrDetectionMissing || noFieldChange, startIcon: templateLoading ? _jsx(CircularProgress, { size: 16 }) : _jsx(Check, {}), onClick: onSave, children: t(!analyticOrDetectionMissing && !noFieldChange ? 'button.save' : 'button.saved') })] }), analyticOrDetectionMissing ? (_jsx(AppInfoPanel, { i18nKey: "route.templates.select", sx: { width: '100%', alignSelf: 'start' } })) : (_jsx(TemplateEditor, { hit: exampleHit, fields: displayFields, setFields: setDisplayFields, onAdd: field => setDisplayFields([...displayFields, field]), onRemove: field => setDisplayFields(displayFields.filter(f => f !== field)) }))] })] }));
|
|
164
155
|
};
|
|
165
156
|
export default TemplateViewer;
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Article
|
|
3
|
-
import {
|
|
2
|
+
import { Article } from '@mui/icons-material';
|
|
3
|
+
import { Stack, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
|
|
4
4
|
import api from '@cccsaurora/howler-ui/api';
|
|
5
5
|
import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
|
|
6
|
-
import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
|
|
7
6
|
import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
|
|
8
7
|
import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
|
|
9
8
|
import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
|
|
10
9
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
11
10
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
12
|
-
import { useCallback, useContext, useEffect,
|
|
11
|
+
import { useCallback, useContext, useEffect, useState } from 'react';
|
|
13
12
|
import { useTranslation } from 'react-i18next';
|
|
14
13
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
15
|
-
import { useContextSelector } from 'use-context-selector';
|
|
16
14
|
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
17
15
|
import TemplateCard from './TemplateCard';
|
|
18
16
|
const TemplatesBase = () => {
|
|
@@ -22,11 +20,9 @@ const TemplatesBase = () => {
|
|
|
22
20
|
const { dispatchApi } = useMyApi();
|
|
23
21
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
24
22
|
const { load } = useContext(TuiListMethodContext);
|
|
25
|
-
const templates = useContextSelector(TemplateContext, ctx => ctx.templates);
|
|
26
23
|
const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
|
|
27
24
|
const [phrase, setPhrase] = useState('');
|
|
28
25
|
const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
|
|
29
|
-
const [showBuiltins, setShowBuiltins] = useState(true);
|
|
30
26
|
const [response, setResponse] = useState(null);
|
|
31
27
|
const [types, setTypes] = useState([]);
|
|
32
28
|
const [hasError, setHasError] = useState(false);
|
|
@@ -100,15 +96,12 @@ const TemplatesBase = () => {
|
|
|
100
96
|
}
|
|
101
97
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
98
|
}, [offset]);
|
|
103
|
-
const builtInTemplates = useMemo(() => templates.filter(template => template.type === 'readonly'), [templates]);
|
|
104
99
|
const renderer = useCallback((item, className) => _jsx(TemplateCard, { template: item, className: className }), []);
|
|
105
100
|
return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, searchFilters: _jsx(Stack, { direction: "row", spacing: 1, alignItems: "center", children: _jsxs(ToggleButtonGroup, { sx: { display: 'grid', gridTemplateColumns: '1fr 1fr', alignSelf: 'start' }, size: "small", value: types, onChange: (__, _types) => {
|
|
106
101
|
if (_types) {
|
|
107
102
|
setTypes(_types.length < 2 ? _types : []);
|
|
108
103
|
}
|
|
109
|
-
}, children: [_jsx(ToggleButton, { value: "personal", "aria-label": "personal", children: t('route.templates.manager.personal') }), _jsx(ToggleButton, { value: "global", "aria-label": "global", children: t('route.templates.manager.global') })] }) }), aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.templates.search.prompt') }),
|
|
110
|
-
offset < 1 &&
|
|
111
|
-
builtInTemplates.length > 0 && (_jsx(Card, { sx: { p: 1, mb: 1 }, children: _jsxs(Stack, { children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Typography, { children: t('route.templates.builtin.show') }), _jsx(Tooltip, { title: t(`route.templates.builtin.${showBuiltins ? 'hide' : 'show'}`), children: _jsx(IconButton, { size: "small", onClick: () => setShowBuiltins(!showBuiltins), children: _jsx(KeyboardArrowDown, { fontSize: "small", sx: { transition: 'rotate 250ms', rotate: showBuiltins ? '180deg' : '0deg' } }) }) })] }), _jsxs(Collapse, { in: showBuiltins, children: [_jsx(Box, { sx: { mt: 1 } }), builtInTemplates.map(template => renderer(template))] })] }) })), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/templates/view?type=${item.item.type}&analytic=${item.item.analytic}${item.item.detection ? '&detection=' + item.item.detection : ''}`), onCreate: () => navigate('/templates/view'), createPrompt: "route.templates.create", searchPrompt: "route.templates.manager.search", createIcon: _jsx(Article, { sx: { mr: 1 } }) }));
|
|
104
|
+
}, children: [_jsx(ToggleButton, { value: "personal", "aria-label": "personal", children: t('route.templates.manager.personal') }), _jsx(ToggleButton, { value: "global", "aria-label": "global", children: t('route.templates.manager.global') })] }) }), aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.templates.search.prompt') }), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/templates/view?type=${item.item.type}&analytic=${item.item.analytic}${item.item.detection ? '&detection=' + item.item.detection : ''}`), onCreate: () => navigate('/templates/view'), createPrompt: "route.templates.create", searchPrompt: "route.templates.manager.search", createIcon: _jsx(Article, { sx: { mr: 1 } }) }));
|
|
112
105
|
};
|
|
113
106
|
const Templates = () => {
|
|
114
107
|
return (_jsx(TuiListProvider, { children: _jsx(TemplatesBase, {}) }));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Dossier } from './entities/generated/Dossier';
|
|
2
|
+
import type { Overview } from './entities/generated/Overview';
|
|
3
|
+
import type { Template } from './entities/generated/Template';
|
|
4
|
+
export type WithMetadata<T> = T & {
|
|
5
|
+
__template?: Template;
|
|
6
|
+
__overview?: Overview;
|
|
7
|
+
__dossiers?: Dossier[];
|
|
8
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Dossier } from '@cccsaurora/howler-ui/models/entities/generated/Dossier';
|
|
2
|
-
import { type FC, type PropsWithChildren } from 'react';
|
|
3
|
-
export interface DossierContextType {
|
|
4
|
-
ready: boolean;
|
|
5
|
-
dossiers: Dossier[];
|
|
6
|
-
fetchDossiers: (force?: boolean) => Promise<void>;
|
|
7
|
-
addDossier: (v: Dossier) => Promise<Dossier>;
|
|
8
|
-
editDossier: (dossier: Dossier) => Promise<Dossier>;
|
|
9
|
-
removeDossier: (id: string) => Promise<void>;
|
|
10
|
-
getCurrentDossiers: () => Dossier[];
|
|
11
|
-
getMatchingDossiers: (id: string) => Promise<Dossier[]>;
|
|
12
|
-
}
|
|
13
|
-
export declare const DossierContext: import("use-context-selector").Context<DossierContextType>;
|
|
14
|
-
declare const DossierProvider: FC<PropsWithChildren>;
|
|
15
|
-
export declare const useDossierContextSelector: <Selected>(selector: (value: DossierContextType) => Selected) => Selected;
|
|
16
|
-
export default DossierProvider;
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import api from '@cccsaurora/howler-ui/api';
|
|
3
|
-
import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
|
|
4
|
-
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
5
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
6
|
-
import { createContext, useContextSelector } from 'use-context-selector';
|
|
7
|
-
export const DossierContext = createContext(null);
|
|
8
|
-
const DossierProvider = ({ children }) => {
|
|
9
|
-
const { dispatchApi } = useMyApi();
|
|
10
|
-
const appUser = useAppUser();
|
|
11
|
-
const [loading, setLoading] = useState(false);
|
|
12
|
-
const [ready, setReady] = useState(false);
|
|
13
|
-
const [dossiers, setDossiers] = useState([]);
|
|
14
|
-
const [idToDossiers, setIdToDossiers] = useState({});
|
|
15
|
-
const fetchDossiers = useCallback(async (force = false) => {
|
|
16
|
-
if (ready && !force) {
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (!appUser.isReady()) {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
setLoading(true);
|
|
23
|
-
try {
|
|
24
|
-
setDossiers((await api.dossier.get()));
|
|
25
|
-
setReady(true);
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
setLoading(false);
|
|
29
|
-
}
|
|
30
|
-
}, [appUser, ready]);
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
if (!ready && !loading) {
|
|
33
|
-
fetchDossiers();
|
|
34
|
-
}
|
|
35
|
-
}, [fetchDossiers, ready, loading]);
|
|
36
|
-
const getCurrentDossiers = useCallback(() => {
|
|
37
|
-
return [];
|
|
38
|
-
}, []);
|
|
39
|
-
const editDossier = useCallback(async (dossier) => {
|
|
40
|
-
const result = await api.dossier.put(dossier.dossier_id, dossier);
|
|
41
|
-
setDossiers(_dossiers => _dossiers.map(_dossier => (_dossier.dossier_id === dossier.dossier_id ? { ..._dossier, dossier } : _dossier)));
|
|
42
|
-
return result;
|
|
43
|
-
}, []);
|
|
44
|
-
const addDossier = useCallback(async (dossier) => {
|
|
45
|
-
const newDossier = await dispatchApi(api.dossier.post(dossier));
|
|
46
|
-
setDossiers(_dossiers => [..._dossiers, newDossier]);
|
|
47
|
-
return newDossier;
|
|
48
|
-
}, [dispatchApi]);
|
|
49
|
-
const removeDossier = useCallback(async (id) => {
|
|
50
|
-
const result = await api.dossier.del(id);
|
|
51
|
-
setDossiers(_dossiers => _dossiers.filter(v => v.dossier_id !== id));
|
|
52
|
-
return result;
|
|
53
|
-
}, []);
|
|
54
|
-
const getMatchingDossiers = useCallback(async (id) => {
|
|
55
|
-
if (idToDossiers[id]) {
|
|
56
|
-
return idToDossiers[id];
|
|
57
|
-
}
|
|
58
|
-
const result = await dispatchApi(api.dossier.hit.get(id), { throwError: false });
|
|
59
|
-
if (result) {
|
|
60
|
-
setIdToDossiers(_dossiers => ({
|
|
61
|
-
..._dossiers,
|
|
62
|
-
[id]: result
|
|
63
|
-
}));
|
|
64
|
-
return result;
|
|
65
|
-
}
|
|
66
|
-
return [];
|
|
67
|
-
}, [dispatchApi, idToDossiers]);
|
|
68
|
-
return (_jsx(DossierContext.Provider, { value: {
|
|
69
|
-
ready,
|
|
70
|
-
dossiers,
|
|
71
|
-
fetchDossiers,
|
|
72
|
-
addDossier,
|
|
73
|
-
editDossier,
|
|
74
|
-
removeDossier,
|
|
75
|
-
getCurrentDossiers,
|
|
76
|
-
getMatchingDossiers
|
|
77
|
-
}, children: children }));
|
|
78
|
-
};
|
|
79
|
-
export const useDossierContextSelector = (selector) => {
|
|
80
|
-
return useContextSelector(DossierContext, selector);
|
|
81
|
-
};
|
|
82
|
-
export default DossierProvider;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
2
|
-
import type { Template } from '@cccsaurora/howler-ui/models/entities/generated/Template';
|
|
3
|
-
import type { FC, PropsWithChildren } from 'react';
|
|
4
|
-
interface TemplateContextType {
|
|
5
|
-
templates: Template[];
|
|
6
|
-
getTemplates: (force?: boolean) => Promise<Template[]>;
|
|
7
|
-
getMatchingTemplate: (h: Hit) => Template;
|
|
8
|
-
refresh: () => void;
|
|
9
|
-
loaded: boolean;
|
|
10
|
-
}
|
|
11
|
-
export declare const TemplateContext: import("use-context-selector").Context<TemplateContextType>;
|
|
12
|
-
declare const TemplateProvider: FC<PropsWithChildren>;
|
|
13
|
-
export declare const useTemplateContextSelector: <Selected>(selector: (value: TemplateContextType) => Selected) => Selected;
|
|
14
|
-
export default TemplateProvider;
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import api from '@cccsaurora/howler-ui/api';
|
|
3
|
-
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
4
|
-
import { useCallback, useRef, useState } from 'react';
|
|
5
|
-
import { createContext, useContextSelector } from 'use-context-selector';
|
|
6
|
-
const SIX_TAIL_PHISH_DETAILS = [
|
|
7
|
-
'event.start',
|
|
8
|
-
'event.end',
|
|
9
|
-
'destination.ip',
|
|
10
|
-
'destination.domain',
|
|
11
|
-
'cloud.service.name',
|
|
12
|
-
'error.code',
|
|
13
|
-
'error.message'
|
|
14
|
-
];
|
|
15
|
-
/**
|
|
16
|
-
* TODO: Ask analysts to move these into the API
|
|
17
|
-
*/
|
|
18
|
-
const BUILTIN_TEMPLATES = [
|
|
19
|
-
{
|
|
20
|
-
analytic: '6TailPhish',
|
|
21
|
-
keys: SIX_TAIL_PHISH_DETAILS,
|
|
22
|
-
owner: 'none',
|
|
23
|
-
template_id: '6tailphish.builtin',
|
|
24
|
-
type: 'readonly'
|
|
25
|
-
}
|
|
26
|
-
];
|
|
27
|
-
export const TemplateContext = createContext(null);
|
|
28
|
-
const TemplateProvider = ({ children }) => {
|
|
29
|
-
const request = useRef(null);
|
|
30
|
-
const { dispatchApi } = useMyApi();
|
|
31
|
-
const [loaded, setLoaded] = useState(false);
|
|
32
|
-
const templateRequest = useRef(null);
|
|
33
|
-
const [templates, setTemplates] = useState(BUILTIN_TEMPLATES);
|
|
34
|
-
const getTemplates = useCallback(async (force = false) => {
|
|
35
|
-
if (request.current) {
|
|
36
|
-
return request.current;
|
|
37
|
-
}
|
|
38
|
-
if (loaded && !force) {
|
|
39
|
-
return templates;
|
|
40
|
-
}
|
|
41
|
-
else if (templateRequest.current) {
|
|
42
|
-
return templateRequest.current;
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
try {
|
|
46
|
-
request.current = dispatchApi(api.template.get());
|
|
47
|
-
const result = await request.current;
|
|
48
|
-
const fullList = [...BUILTIN_TEMPLATES, ...result];
|
|
49
|
-
setTemplates(fullList);
|
|
50
|
-
setLoaded(true);
|
|
51
|
-
return fullList;
|
|
52
|
-
}
|
|
53
|
-
catch (e) {
|
|
54
|
-
return [];
|
|
55
|
-
}
|
|
56
|
-
finally {
|
|
57
|
-
request.current = null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}, [dispatchApi, loaded, templates]);
|
|
61
|
-
/**
|
|
62
|
-
* Based on a given hit, retrieve the best match for a template
|
|
63
|
-
*/
|
|
64
|
-
const getMatchingTemplate = useCallback((hit) => templates
|
|
65
|
-
.filter(_template =>
|
|
66
|
-
// The analytic must match, and the detection must either a) not exist or b) match the hit
|
|
67
|
-
_template.analytic === hit.howler.analytic &&
|
|
68
|
-
(!_template.detection || _template.detection.toLowerCase() === hit.howler.detection?.toLowerCase()))
|
|
69
|
-
.sort((a, b) => {
|
|
70
|
-
// Sort priority:
|
|
71
|
-
// 1. personal > readonly > global
|
|
72
|
-
// 2. detection > !detection
|
|
73
|
-
if (a.type !== b.type) {
|
|
74
|
-
const order = {
|
|
75
|
-
personal: 2,
|
|
76
|
-
readonly: 1,
|
|
77
|
-
global: 0
|
|
78
|
-
};
|
|
79
|
-
return order[b.type] - order[a.type];
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
if (a.detection && !b.detection) {
|
|
83
|
-
return -1;
|
|
84
|
-
}
|
|
85
|
-
else if (!a.detection && b.detection) {
|
|
86
|
-
return 1;
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
return 0;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
})[0], [templates]);
|
|
93
|
-
const refresh = useCallback(() => {
|
|
94
|
-
setLoaded(false);
|
|
95
|
-
getTemplates();
|
|
96
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
97
|
-
}, []);
|
|
98
|
-
return (_jsx(TemplateContext.Provider, { value: { templates, getTemplates, getMatchingTemplate, refresh, loaded }, children: children }));
|
|
99
|
-
};
|
|
100
|
-
export const useTemplateContextSelector = (selector) => {
|
|
101
|
-
return useContextSelector(TemplateContext, selector);
|
|
102
|
-
};
|
|
103
|
-
export default TemplateProvider;
|