@cccsaurora/howler-ui 2.12.2 → 2.13.0-dev.103
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/search/facet/hit.d.ts +4 -2
- package/api/search/facet/hit.js +5 -5
- package/api/search/facet/index.d.ts +1 -0
- package/components/app/providers/FavouritesProvider.js +27 -30
- package/components/app/providers/HitSearchProvider.js +7 -10
- package/components/app/providers/ViewProvider.d.ts +5 -4
- package/components/app/providers/ViewProvider.js +58 -36
- package/components/elements/hit/HitActions.js +1 -1
- package/components/elements/hit/HitSummary.js +6 -6
- package/components/elements/hit/aggregate/HitGraph.js +6 -9
- package/components/routes/advanced/luceneCompletionProvider.js +4 -2
- package/components/routes/analytics/widgets/Assessment.js +3 -2
- package/components/routes/analytics/widgets/Escalation.js +4 -3
- package/components/routes/analytics/widgets/Stacked.js +4 -3
- package/components/routes/hits/search/HitBrowser.js +8 -8
- package/components/routes/hits/search/HitQuery.js +1 -1
- package/components/routes/hits/search/SearchPane.js +1 -1
- package/components/routes/hits/search/ViewLink.js +3 -2
- package/components/routes/hits/search/grid/HitGrid.js +5 -7
- package/components/routes/hits/search/shared/HitFilter.js +2 -2
- package/components/routes/hits/search/shared/HitSort.js +9 -8
- package/components/routes/hits/search/shared/QuerySettings.js +1 -1
- package/components/routes/hits/search/shared/SearchSpan.js +17 -13
- package/components/routes/home/AddNewCard.js +14 -1
- package/components/routes/home/ViewCard.js +5 -1
- package/components/routes/home/index.js +1 -1
- package/components/routes/views/ViewComposer.js +17 -16
- package/components/routes/views/Views.js +25 -14
- package/package.json +7 -3
- package/plugins/borealis/Provider.d.ts +3 -0
- package/plugins/borealis/Provider.js +14 -0
- package/plugins/borealis/components/BorealisChip.d.ts +3 -0
- package/plugins/borealis/components/BorealisChip.js +27 -0
- package/plugins/borealis/components/BorealisLeadForm.d.ts +4 -0
- package/plugins/borealis/components/BorealisLeadForm.js +23 -0
- package/plugins/borealis/components/BorealisPivot.d.ts +3 -0
- package/plugins/borealis/components/BorealisPivot.js +83 -0
- package/plugins/borealis/components/BorealisPivotForm.d.ts +4 -0
- package/plugins/borealis/components/BorealisPivotForm.js +44 -0
- package/plugins/borealis/components/BorealisTypography.d.ts +3 -0
- package/plugins/borealis/components/BorealisTypography.js +53 -0
- package/plugins/borealis/helpers.d.ts +6 -0
- package/plugins/borealis/helpers.js +137 -0
- package/plugins/borealis/index.d.ts +21 -0
- package/plugins/borealis/index.js +46 -0
- package/plugins/borealis/locales/borealis.en.json +7 -0
- package/plugins/borealis/locales/borealis.fr.json +7 -0
- package/plugins/borealis/setup.d.ts +2 -0
- package/plugins/borealis/setup.js +44 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { HowlerFacetSearchRequest, HowlerFacetSearchResponse } from '@cccsaurora/howler-ui/api/search/facet';
|
|
2
|
-
export declare const uri: (
|
|
3
|
-
export declare const post: (
|
|
2
|
+
export declare const uri: () => string;
|
|
3
|
+
export declare const post: (request?: HowlerFacetSearchRequest) => Promise<{
|
|
4
|
+
[index: string]: HowlerFacetSearchResponse;
|
|
5
|
+
}>;
|
package/api/search/facet/hit.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { hpost,
|
|
1
|
+
import { hpost, joinUri } from '@cccsaurora/howler-ui/api';
|
|
2
2
|
import { uri as parentUri } from '@cccsaurora/howler-ui/api/search/facet';
|
|
3
|
-
export const uri = (
|
|
4
|
-
return
|
|
3
|
+
export const uri = () => {
|
|
4
|
+
return joinUri(parentUri(), 'hit');
|
|
5
5
|
};
|
|
6
|
-
export const post = (
|
|
7
|
-
return hpost(uri(
|
|
6
|
+
export const post = (request) => {
|
|
7
|
+
return hpost(uri(), { ...(request || {}), query: request?.query || 'howler.id:*' });
|
|
8
8
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { QueryStats, SavedSearch } from '@mui/icons-material';
|
|
3
3
|
import { useAppLeftNav, useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
|
|
4
|
-
import {
|
|
4
|
+
import { uniq } from 'lodash-es';
|
|
5
5
|
import { createContext, useCallback, useContext, useEffect } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { useContextSelector } from 'use-context-selector';
|
|
@@ -13,11 +13,10 @@ const FavouriteProvider = ({ children }) => {
|
|
|
13
13
|
const leftNav = useAppLeftNav();
|
|
14
14
|
const appUser = useAppUser();
|
|
15
15
|
const analytics = useContext(AnalyticContext);
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const processViewElement = useCallback(() => {
|
|
16
|
+
const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
|
|
17
|
+
const processViewElement = useCallback(async () => {
|
|
19
18
|
const viewElement = leftNav.elements.find(el => el.element?.id === 'views');
|
|
20
|
-
const favourites = appUser.user?.favourite_views || [];
|
|
19
|
+
const favourites = uniq(appUser.user?.favourite_views || []);
|
|
21
20
|
// There are no favourites and no nav elements - return
|
|
22
21
|
if (favourites.length < 1 && !viewElement) {
|
|
23
22
|
return null;
|
|
@@ -30,19 +29,15 @@ const FavouriteProvider = ({ children }) => {
|
|
|
30
29
|
if (favourites.length === viewElement?.element?.items?.length) {
|
|
31
30
|
return viewElement;
|
|
32
31
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
: null;
|
|
44
|
-
})
|
|
45
|
-
.filter(v => !!v), val => val.id);
|
|
32
|
+
const savedViews = await fetchViews(favourites);
|
|
33
|
+
const items = savedViews
|
|
34
|
+
.filter(view => !!view)
|
|
35
|
+
.map(view => ({
|
|
36
|
+
id: view.view_id,
|
|
37
|
+
text: t(view.title),
|
|
38
|
+
route: `/views/${view.view_id}`,
|
|
39
|
+
nested: true
|
|
40
|
+
}));
|
|
46
41
|
if (viewElement) {
|
|
47
42
|
const newViewElement = {
|
|
48
43
|
...viewElement,
|
|
@@ -64,7 +59,7 @@ const FavouriteProvider = ({ children }) => {
|
|
|
64
59
|
}
|
|
65
60
|
};
|
|
66
61
|
}
|
|
67
|
-
}, [appUser.user?.favourite_views, leftNav, t
|
|
62
|
+
}, [appUser.user?.favourite_views, fetchViews, leftNav.elements, t]);
|
|
68
63
|
const processAnalyticElement = useCallback(() => {
|
|
69
64
|
const analyticElement = leftNav.elements.find(el => el.element?.id === 'analytics');
|
|
70
65
|
const favourites = appUser.user?.favourite_analytics;
|
|
@@ -115,23 +110,25 @@ const FavouriteProvider = ({ children }) => {
|
|
|
115
110
|
}
|
|
116
111
|
}, [analytics.analytics, appUser.user?.favourite_analytics, leftNav, t]);
|
|
117
112
|
useEffect(() => {
|
|
118
|
-
if (!appUser.isReady() || !
|
|
113
|
+
if (!appUser.isReady() || !analytics.ready) {
|
|
119
114
|
return;
|
|
120
115
|
}
|
|
121
116
|
const newElements = leftNav.elements
|
|
122
117
|
.filter(el => !['views', 'analytics'].includes(el.element?.id))
|
|
123
118
|
.filter(el => !!el);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
119
|
+
(async () => {
|
|
120
|
+
const analyticElement = processAnalyticElement();
|
|
121
|
+
if (analyticElement) {
|
|
122
|
+
newElements.splice(1, 0, analyticElement);
|
|
123
|
+
}
|
|
124
|
+
const viewElement = await processViewElement();
|
|
125
|
+
if (viewElement) {
|
|
126
|
+
newElements.splice(1, 0, viewElement);
|
|
127
|
+
}
|
|
128
|
+
leftNav.setElements(newElements);
|
|
129
|
+
})();
|
|
133
130
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
134
|
-
}, [analytics.ready, appUser
|
|
131
|
+
}, [analytics.ready, appUser]);
|
|
135
132
|
return _jsx(FavouriteContext.Provider, { value: {}, children: children });
|
|
136
133
|
};
|
|
137
134
|
export default FavouriteProvider;
|
|
@@ -25,8 +25,8 @@ const HitSearchProvider = ({ children }) => {
|
|
|
25
25
|
const location = useLocation();
|
|
26
26
|
const { dispatchApi } = useMyApi();
|
|
27
27
|
const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
|
|
28
|
-
const
|
|
29
|
-
const
|
|
28
|
+
const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
|
|
29
|
+
const defaultView = useContextSelector(ViewContext, ctx => ctx.defaultView);
|
|
30
30
|
const query = useContextSelector(ParameterContext, ctx => ctx.query);
|
|
31
31
|
const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
|
|
32
32
|
const offset = useContextSelector(ParameterContext, ctx => ctx.offset);
|
|
@@ -44,7 +44,7 @@ const HitSearchProvider = ({ children }) => {
|
|
|
44
44
|
const [response, setResponse] = useState();
|
|
45
45
|
const [queryHistory, setQueryHistory] = useState(JSON.parse(get(StorageKey.QUERY_HISTORY)) || { 'howler.id: *': new Date().toISOString() });
|
|
46
46
|
const [fzfSearch, setFzfSearch] = useState(false);
|
|
47
|
-
const viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : null
|
|
47
|
+
const viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : defaultView) ?? null, [defaultView, location.pathname, routeParams.id]);
|
|
48
48
|
const bundleId = useMemo(() => (location.pathname.startsWith('/bundles') ? routeParams.id : null), [location.pathname, routeParams.id]);
|
|
49
49
|
const layout = useMemo(() => (isMobile ? HitLayout.COMFY : (get(StorageKey.HIT_LAYOUT) ?? HitLayout.NORMAL)), [get]);
|
|
50
50
|
const search = useCallback(async (_query, appendResults) => {
|
|
@@ -78,7 +78,7 @@ const HitSearchProvider = ({ children }) => {
|
|
|
78
78
|
fullQuery = `(howler.bundles:${bundle}) AND (${fullQuery})`;
|
|
79
79
|
}
|
|
80
80
|
else if (viewId) {
|
|
81
|
-
fullQuery = `(${
|
|
81
|
+
fullQuery = `(${(await getCurrentView())?.query || 'howler.id:*'}) AND (${fullQuery})`;
|
|
82
82
|
}
|
|
83
83
|
const _response = await dispatchApi(api.search.hit.post({
|
|
84
84
|
offset: appendResults ? response.rows : offset,
|
|
@@ -130,15 +130,12 @@ const HitSearchProvider = ({ children }) => {
|
|
|
130
130
|
pageCount,
|
|
131
131
|
trackTotalHits,
|
|
132
132
|
loadHits,
|
|
133
|
-
|
|
133
|
+
getCurrentView,
|
|
134
|
+
defaultView,
|
|
134
135
|
setOffset
|
|
135
136
|
]);
|
|
136
137
|
// We only run this when ancillary properties (i.e. filters, sorting) change
|
|
137
138
|
useEffect(() => {
|
|
138
|
-
// We're being asked to present a view, but we don't currently have the views loaded
|
|
139
|
-
if (viewId && !viewsReady) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
139
|
if (span.endsWith('custom') && (!startDate || !endDate)) {
|
|
143
140
|
return;
|
|
144
141
|
}
|
|
@@ -149,7 +146,7 @@ const HitSearchProvider = ({ children }) => {
|
|
|
149
146
|
setResponse(null);
|
|
150
147
|
}
|
|
151
148
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
152
|
-
}, [filter, offset, pageCount, sort, span, bundleId, location.pathname,
|
|
149
|
+
}, [filter, offset, pageCount, sort, span, bundleId, location.pathname, startDate, endDate]);
|
|
153
150
|
return (_jsx(HitSearchContext.Provider, { value: {
|
|
154
151
|
layout,
|
|
155
152
|
displayType,
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import type { View } from '@cccsaurora/howler-ui/models/entities/generated/View';
|
|
2
2
|
import { type FC, type PropsWithChildren } from 'react';
|
|
3
3
|
export interface ViewContextType {
|
|
4
|
-
ready: boolean;
|
|
5
4
|
defaultView: string;
|
|
6
5
|
setDefaultView: (viewId: string) => void;
|
|
7
|
-
views:
|
|
6
|
+
views: {
|
|
7
|
+
[viewId: string]: View;
|
|
8
|
+
};
|
|
8
9
|
addFavourite: (id: string) => Promise<void>;
|
|
9
10
|
removeFavourite: (id: string) => Promise<void>;
|
|
10
|
-
fetchViews: (
|
|
11
|
+
fetchViews: (ids?: string[]) => Promise<View[]>;
|
|
11
12
|
addView: (v: View) => Promise<View>;
|
|
12
13
|
editView: (id: string, title: string, query: string, sort: string, span: string, advanceOnTriage: boolean) => Promise<View>;
|
|
13
14
|
removeView: (id: string) => Promise<void>;
|
|
14
|
-
getCurrentView: () => View
|
|
15
|
+
getCurrentView: (lazy?: boolean) => Promise<View>;
|
|
15
16
|
}
|
|
16
17
|
export declare const ViewContext: import("use-context-selector").Context<ViewContextType>;
|
|
17
18
|
declare const ViewProvider: FC<PropsWithChildren>;
|
|
@@ -3,6 +3,7 @@ import api from '@cccsaurora/howler-ui/api';
|
|
|
3
3
|
import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
|
|
4
4
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
5
5
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
6
|
+
import { has, omit } from 'lodash-es';
|
|
6
7
|
import { useCallback, useEffect, useState } from 'react';
|
|
7
8
|
import { useLocation, useParams } from 'react-router-dom';
|
|
8
9
|
import { createContext, useContextSelector } from 'use-context-selector';
|
|
@@ -14,77 +15,98 @@ const ViewProvider = ({ children }) => {
|
|
|
14
15
|
const [defaultView, setDefaultView] = useMyLocalStorageItem(StorageKey.DEFAULT_VIEW);
|
|
15
16
|
const location = useLocation();
|
|
16
17
|
const routeParams = useParams();
|
|
17
|
-
const [
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const [views, setViews] = useState({});
|
|
19
|
+
const fetchViews = useCallback(async (ids) => {
|
|
20
|
+
if (!ids) {
|
|
21
|
+
const newViews = (await dispatchApi(api.view.get(), { throwError: false })) ?? [];
|
|
22
|
+
setViews(_views => ({
|
|
23
|
+
..._views,
|
|
24
|
+
...Object.fromEntries(newViews.map(_view => [_view.view_id, _view]))
|
|
25
|
+
}));
|
|
26
|
+
return newViews;
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
const missingIds = ids.filter(_id => !views[_id]);
|
|
29
|
+
if (missingIds.length < 1) {
|
|
30
|
+
return ids.map(id => views[id]);
|
|
25
31
|
}
|
|
26
|
-
setLoading(true);
|
|
27
32
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
const response = await dispatchApi(api.search.view.post({
|
|
34
|
+
query: `view_id:(${missingIds.join(' OR ')})`,
|
|
35
|
+
rows: missingIds.length
|
|
36
|
+
}));
|
|
37
|
+
const newViews = Object.fromEntries(response.items.map(_view => [_view.view_id, _view]));
|
|
38
|
+
setViews(_views => ({
|
|
39
|
+
..._views,
|
|
40
|
+
...Object.fromEntries(missingIds.map(_view_id => [_view_id, null])),
|
|
41
|
+
...newViews
|
|
42
|
+
}));
|
|
43
|
+
return ids.map(id => views[id] ?? newViews[id]);
|
|
32
44
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
catch (e) {
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
47
|
+
console.warn(e);
|
|
48
|
+
return [];
|
|
37
49
|
}
|
|
38
|
-
}, [
|
|
50
|
+
}, [dispatchApi, views]);
|
|
39
51
|
useEffect(() => {
|
|
40
|
-
if (defaultView
|
|
41
|
-
|
|
52
|
+
if (!defaultView || has(views, defaultView)) {
|
|
53
|
+
return;
|
|
42
54
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
(async () => {
|
|
56
|
+
const result = await fetchViews([defaultView]);
|
|
57
|
+
if (!result.length) {
|
|
58
|
+
setDefaultView(undefined);
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
}, [defaultView, fetchViews, setDefaultView, views]);
|
|
62
|
+
const getCurrentView = useCallback(async (lazy = false) => {
|
|
63
|
+
const id = location.pathname.startsWith('/views') ? routeParams.id : defaultView;
|
|
64
|
+
if (!id) {
|
|
46
65
|
return null;
|
|
47
66
|
}
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
if (!has(views, id) && !lazy) {
|
|
68
|
+
return (await fetchViews([id]))[0];
|
|
69
|
+
}
|
|
70
|
+
return views[id];
|
|
71
|
+
}, [defaultView, fetchViews, location.pathname, routeParams.id, views]);
|
|
50
72
|
const editView = useCallback(async (id, title, query, sort, span, advanceOnTriage) => {
|
|
51
|
-
const result = await api.view.put(id, title, query, sort, span, advanceOnTriage);
|
|
73
|
+
const result = await dispatchApi(api.view.put(id, title, query, sort, span, advanceOnTriage));
|
|
52
74
|
setViews(_views => ({
|
|
53
75
|
..._views,
|
|
54
|
-
|
|
76
|
+
[id]: { ...(_views[id] ?? {}), title, query, sort, span, settings: { advance_on_triage: advanceOnTriage } }
|
|
55
77
|
}));
|
|
56
78
|
return result;
|
|
57
|
-
}, []);
|
|
79
|
+
}, [dispatchApi]);
|
|
58
80
|
const addFavourite = useCallback(async (id) => {
|
|
59
|
-
await api.view.favourite.post(id);
|
|
81
|
+
await dispatchApi(api.view.favourite.post(id));
|
|
60
82
|
appUser.setUser({
|
|
61
83
|
...appUser.user,
|
|
62
84
|
favourite_views: [...appUser.user.favourite_views, id]
|
|
63
85
|
});
|
|
64
|
-
}, [appUser]);
|
|
86
|
+
}, [appUser, dispatchApi]);
|
|
65
87
|
const addView = useCallback(async (view) => {
|
|
66
88
|
const newView = await dispatchApi(api.view.post(view));
|
|
67
|
-
setViews(_views => ({ ..._views,
|
|
89
|
+
setViews(_views => ({ ..._views, [newView.view_id]: newView }));
|
|
68
90
|
addFavourite(newView.view_id);
|
|
69
91
|
return newView;
|
|
70
92
|
}, [addFavourite, dispatchApi]);
|
|
71
93
|
const removeFavourite = useCallback(async (id) => {
|
|
72
|
-
await api.view.favourite.del(id);
|
|
94
|
+
await dispatchApi(api.view.favourite.del(id));
|
|
73
95
|
appUser.setUser({
|
|
74
96
|
...appUser.user,
|
|
75
97
|
favourite_views: appUser.user.favourite_views.filter(v => v !== id)
|
|
76
98
|
});
|
|
77
|
-
}, [appUser]);
|
|
99
|
+
}, [appUser, dispatchApi]);
|
|
78
100
|
const removeView = useCallback(async (id) => {
|
|
79
|
-
const result = await api.view.del(id);
|
|
80
|
-
setViews(_views => (
|
|
101
|
+
const result = await dispatchApi(api.view.del(id));
|
|
102
|
+
setViews(_views => omit(_views, id));
|
|
81
103
|
if (appUser.user?.favourite_views.includes(id)) {
|
|
82
104
|
removeFavourite(id);
|
|
83
105
|
}
|
|
84
106
|
return result;
|
|
85
|
-
}, [appUser.user?.favourite_views, removeFavourite]);
|
|
107
|
+
}, [appUser.user?.favourite_views, dispatchApi, removeFavourite]);
|
|
86
108
|
return (_jsx(ViewContext.Provider, { value: {
|
|
87
|
-
|
|
109
|
+
views,
|
|
88
110
|
addFavourite,
|
|
89
111
|
removeFavourite,
|
|
90
112
|
fetchViews,
|
|
@@ -68,7 +68,7 @@ const HitActions = ({ hit, orientation = 'horizontal' }) => {
|
|
|
68
68
|
actionFunction: async () => {
|
|
69
69
|
if (!loading) {
|
|
70
70
|
await assess(assessment, analytic?.triage_settings?.skip_rationale);
|
|
71
|
-
if (getCurrentView()?.settings?.advance_on_triage && nextHit) {
|
|
71
|
+
if ((await getCurrentView())?.settings?.advance_on_triage && nextHit) {
|
|
72
72
|
clearSelectedHits(nextHit.howler.id);
|
|
73
73
|
setSelected?.(nextHit.howler.id);
|
|
74
74
|
}
|
|
@@ -84,10 +84,10 @@ const HitSummary = ({ query, response, onStart, onComplete }) => {
|
|
|
84
84
|
setAggregateResults({});
|
|
85
85
|
// Sort the fields based on the number of occurrences
|
|
86
86
|
const sortedKeys = Object.keys(_keyCounts).sort((a, b) => (_keyCounts[b]?.count ?? 0) - (_keyCounts[a]?.count ?? 0));
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
if (sortedKeys.length > 0) {
|
|
88
|
+
setLoading(true);
|
|
89
|
+
const result = await dispatchApi(api.search.facet.hit.post({
|
|
90
|
+
fields: sortedKeys,
|
|
91
91
|
query,
|
|
92
92
|
rows: pageCount,
|
|
93
93
|
filters
|
|
@@ -99,7 +99,7 @@ const HitSummary = ({ query, response, onStart, onComplete }) => {
|
|
|
99
99
|
if (result) {
|
|
100
100
|
setAggregateResults(_results => ({
|
|
101
101
|
..._results,
|
|
102
|
-
|
|
102
|
+
...result
|
|
103
103
|
}));
|
|
104
104
|
}
|
|
105
105
|
}
|
|
@@ -142,7 +142,7 @@ const HitSummary = ({ query, response, onStart, onComplete }) => {
|
|
|
142
142
|
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
143
|
.filter(key => !isEmpty(aggregateResults[key]))
|
|
144
144
|
.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
|
|
145
|
+
_jsx(Fade, { in: true, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Typography, { variant: "body1", children: key }, key + '-title'), keyCounts[key]?.count < 0 ? (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", t('hit.summary.adhoc.custom'), ")"] })) : (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", keyCounts[key]?.count, " ", t('references'), ")"] })), _jsx(Tooltip, { title: _jsxs(Stack, { children: [_jsx(Typography, { variant: "caption", children: t('hit.summary.aggregate.sources') }), keyCounts[key].sources.map(source => (_jsx(Typography, { variant: "caption", children: source }, source)))] }), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }) }, key + '-refs'),
|
|
146
146
|
_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
147
|
.map(d => new Date(d).toLocaleString())
|
|
148
148
|
.join(' - '), onClick: () => setSearch(key, `[${getTimeRange(Object.keys(aggregateResults[key])).join(' TO ')}]`) })) }, key + '-results')
|
|
@@ -41,7 +41,6 @@ const HitGraph = ({ query }) => {
|
|
|
41
41
|
const addHitToSelection = useContextSelector(HitContext, ctx => ctx.addHitToSelection);
|
|
42
42
|
const removeHitFromSelection = useContextSelector(HitContext, ctx => ctx.removeHitFromSelection);
|
|
43
43
|
const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
|
|
44
|
-
const searching = useContextSelector(HitSearchContext, ctx => ctx.searching);
|
|
45
44
|
const error = useContextSelector(HitSearchContext, ctx => ctx.error);
|
|
46
45
|
const chartRef = useRef();
|
|
47
46
|
const [loading, setLoading] = useState(false);
|
|
@@ -63,6 +62,9 @@ const HitGraph = ({ query }) => {
|
|
|
63
62
|
else if (startDate && endDate) {
|
|
64
63
|
filters.push(`event.created:${convertCustomDateRangeToLucene(startDate, endDate)}`);
|
|
65
64
|
}
|
|
65
|
+
if (escalationFilter) {
|
|
66
|
+
filters.push(`howler.escalation:${escalationFilter}`);
|
|
67
|
+
}
|
|
66
68
|
const total = (await dispatchApi(api.search.count.hit.post({
|
|
67
69
|
query,
|
|
68
70
|
filters
|
|
@@ -75,13 +77,8 @@ const HitGraph = ({ query }) => {
|
|
|
75
77
|
else {
|
|
76
78
|
setDisabled(false);
|
|
77
79
|
}
|
|
78
|
-
const subQueries = [query || 'howler.id:*'];
|
|
79
|
-
if (escalationFilter) {
|
|
80
|
-
subQueries.push(`howler.escalation:${escalationFilter}`);
|
|
81
|
-
}
|
|
82
|
-
const graphQuery = subQueries.map(_query => `(${_query})`).join(' AND ');
|
|
83
80
|
const _data = await dispatchApi(api.search.grouped.hit.post(filterField, {
|
|
84
|
-
query:
|
|
81
|
+
query: query || 'howler.id:*',
|
|
85
82
|
fl: 'event.created,howler.assessment,howler.analytic,howler.detection,howler.outline.threat,howler.outline.target,howler.outline.summary,howler.id',
|
|
86
83
|
// We want a generally random sample across all date ranges, so we use hash.
|
|
87
84
|
// If we used event.created instead, when 1 million hits/hour are created, you'd only see hits from this past minute
|
|
@@ -116,12 +113,12 @@ const HitGraph = ({ query }) => {
|
|
|
116
113
|
}
|
|
117
114
|
}, [dispatchApi, endDate, escalationFilter, filterField, override, query, span, startDate]);
|
|
118
115
|
useEffect(() => {
|
|
119
|
-
if ((!query && !viewId) ||
|
|
116
|
+
if ((!query && !viewId) || error) {
|
|
120
117
|
return;
|
|
121
118
|
}
|
|
122
119
|
performQuery();
|
|
123
120
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
124
|
-
}, [query, viewId,
|
|
121
|
+
}, [query, viewId, error, span]);
|
|
125
122
|
const options = useMemo(() => {
|
|
126
123
|
const parentOptions = scatter('hit.summary.title', 'hit.summary.subtitle');
|
|
127
124
|
return {
|
|
@@ -41,10 +41,12 @@ const useLuceneCompletionProvider = () => {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
else {
|
|
44
|
-
const options = await api.search.facet.hit
|
|
44
|
+
const options = await api.search.facet.hit
|
|
45
|
+
.post({ query: 'howler.id:*', rows: 250, fields: [key] })
|
|
46
|
+
.catch(() => ({}));
|
|
45
47
|
const _position = model.getWordUntilPosition(position);
|
|
46
48
|
return {
|
|
47
|
-
suggestions: Object.keys(options).map(_value => ({
|
|
49
|
+
suggestions: Object.keys(options[key] || {}).map(_value => ({
|
|
48
50
|
label: _value,
|
|
49
51
|
kind: monaco.languages.CompletionItemKind.Constant,
|
|
50
52
|
insertText: `"${_value}"`,
|
|
@@ -18,10 +18,11 @@ const Assessment = forwardRef(({ analytic }, ref) => {
|
|
|
18
18
|
}
|
|
19
19
|
setLoading(true);
|
|
20
20
|
api.search.facet.hit
|
|
21
|
-
.post(
|
|
21
|
+
.post({
|
|
22
|
+
fields: ['howler.assessment'],
|
|
22
23
|
query: `howler.analytic:("${analytic.name}")`
|
|
23
24
|
})
|
|
24
|
-
.then(data => setAssessmentData(data))
|
|
25
|
+
.then(data => setAssessmentData(data['howler.assessment']))
|
|
25
26
|
.finally(() => setLoading(false));
|
|
26
27
|
}, [analytic]);
|
|
27
28
|
if (!loading && assessmentData.length < 1) {
|
|
@@ -18,10 +18,11 @@ const Escalation = forwardRef(({ analytic, maxWidth = '45%' }, ref) => {
|
|
|
18
18
|
}
|
|
19
19
|
setLoading(true);
|
|
20
20
|
api.search.facet.hit
|
|
21
|
-
.post(
|
|
22
|
-
query: `howler.analytic:("${analytic.name}")
|
|
21
|
+
.post({
|
|
22
|
+
query: `howler.analytic:("${analytic.name}")`,
|
|
23
|
+
fields: ['howler.escalation']
|
|
23
24
|
})
|
|
24
|
-
.then(data => setEscalationData(data))
|
|
25
|
+
.then(data => setEscalationData(data['howler.escalation']))
|
|
25
26
|
.finally(() => setLoading(false));
|
|
26
27
|
}, [analytic]);
|
|
27
28
|
return analytic && !loading ? (_jsx("div", { style: { maxWidth }, children: _jsx(Doughnut, { ref: ref, options: doughnut('route.analytics.escalation.title', ''), data: {
|
|
@@ -13,10 +13,11 @@ const Stacked = forwardRef(({ analytic, field, color }, ref) => {
|
|
|
13
13
|
const fetchData = useCallback(async () => {
|
|
14
14
|
try {
|
|
15
15
|
setLoading(true);
|
|
16
|
-
const result = await api.search.facet.hit.post(
|
|
17
|
-
query: `howler.analytic:("${analytic.name}")
|
|
16
|
+
const result = await api.search.facet.hit.post({
|
|
17
|
+
query: `howler.analytic:("${analytic.name}")`,
|
|
18
|
+
fields: [field]
|
|
18
19
|
});
|
|
19
|
-
const values = Object.entries(result)
|
|
20
|
+
const values = Object.entries(result[field])
|
|
20
21
|
.sort(([__, valA], [___, valB]) => valB - valA)
|
|
21
22
|
.map(([key]) => key);
|
|
22
23
|
setDatasets(await Promise.all(values.map(async (_value) => {
|
|
@@ -10,7 +10,7 @@ import FlexPort from '@cccsaurora/howler-ui/components/elements/addons/layout/Fl
|
|
|
10
10
|
import HitSummary from '@cccsaurora/howler-ui/components/elements/hit/HitSummary';
|
|
11
11
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
12
12
|
import ErrorBoundary from '@cccsaurora/howler-ui/components/routes/ErrorBoundary';
|
|
13
|
-
import { isNull } from 'lodash-es';
|
|
13
|
+
import { has, isNull } from 'lodash-es';
|
|
14
14
|
import moment from 'moment';
|
|
15
15
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|
16
16
|
import { Trans, useTranslation } from 'react-i18next';
|
|
@@ -28,7 +28,6 @@ const HitBrowser = () => {
|
|
|
28
28
|
const { t } = useTranslation();
|
|
29
29
|
const theme = useTheme();
|
|
30
30
|
const views = useContextSelector(ViewContext, ctx => ctx.views);
|
|
31
|
-
const viewsReady = useContextSelector(ViewContext, ctx => ctx.ready);
|
|
32
31
|
const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
|
|
33
32
|
const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
|
|
34
33
|
const setSelected = useContextSelector(ParameterContext, ctx => ctx.setSelected);
|
|
@@ -61,7 +60,7 @@ const HitBrowser = () => {
|
|
|
61
60
|
_fullQuery = `(howler.bundles:${bundle}) AND (${_fullQuery})`;
|
|
62
61
|
}
|
|
63
62
|
else if (viewId) {
|
|
64
|
-
_fullQuery = `(${views
|
|
63
|
+
_fullQuery = `(${views[viewId]?.query || 'howler.id:*'}) AND (${_fullQuery})`;
|
|
65
64
|
}
|
|
66
65
|
return _fullQuery;
|
|
67
66
|
}, [location.pathname, query, routeParams.id, views, viewId]);
|
|
@@ -69,11 +68,11 @@ const HitBrowser = () => {
|
|
|
69
68
|
if (selectedHits.length > 1) {
|
|
70
69
|
return true;
|
|
71
70
|
}
|
|
72
|
-
if (selectedHits.length === 1 && selectedHits[0]?.howler.id !==
|
|
71
|
+
if (selectedHits.length === 1 && selected && selectedHits[0]?.howler.id !== selected) {
|
|
73
72
|
return true;
|
|
74
73
|
}
|
|
75
74
|
return false;
|
|
76
|
-
}, [
|
|
75
|
+
}, [selected, selectedHits]);
|
|
77
76
|
useEffect(() => {
|
|
78
77
|
const newQuery = searchParams.get('query');
|
|
79
78
|
if (newQuery) {
|
|
@@ -94,11 +93,12 @@ const HitBrowser = () => {
|
|
|
94
93
|
});
|
|
95
94
|
}, [setQueryHistory]);
|
|
96
95
|
useEffect(() => {
|
|
97
|
-
if (location.pathname.startsWith('/views')
|
|
98
|
-
|
|
96
|
+
if (!location.pathname.startsWith('/views') || has(views, viewId)) {
|
|
97
|
+
return;
|
|
99
98
|
}
|
|
99
|
+
fetchViews([viewId]);
|
|
100
100
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
-
}, [location.pathname,
|
|
101
|
+
}, [location.pathname, viewId]);
|
|
102
102
|
const onClose = useCallback(() => {
|
|
103
103
|
setSelected(null);
|
|
104
104
|
}, [setSelected]);
|
|
@@ -128,7 +128,7 @@ const HitQuery = ({ searching = false, disabled = false, compact = false, trigge
|
|
|
128
128
|
p: 0.5,
|
|
129
129
|
height: multiline ? `${DEFAULT_MULTILINE_HEIGHT + y}px` : theme.spacing(5)
|
|
130
130
|
}
|
|
131
|
-
], onKeyDown: e => e.stopPropagation(), children: [_jsx(TuiIconButton, { disabled: query.includes('\n#') || disabled, sx: { mr: 1, alignSelf: 'start' }, onClick: () => setMultiline(!multiline), color: multiline ? 'primary' : theme.palette.text.primary, transparent: !multiline, size: compact ? 'small' : 'medium', children: _jsx(Height, { sx: { fontSize: '20px' } }) }), _jsx(QueryEditor, { query: preppedQuery, setQuery: setQuery, language: "lucene", height: multiline ?
|
|
131
|
+
], onKeyDown: e => e.stopPropagation(), children: [_jsx(TuiIconButton, { disabled: query.includes('\n#') || disabled, sx: { mr: 1, alignSelf: 'start' }, onClick: () => setMultiline(!multiline), color: multiline ? 'primary' : theme.palette.text.primary, transparent: !multiline, size: compact ? 'small' : 'medium', children: _jsx(Height, { sx: { fontSize: '20px' } }) }), _jsx(QueryEditor, { query: preppedQuery, setQuery: setQuery, language: "lucene", height: multiline ? `${DEFAULT_MULTILINE_HEIGHT - 30}px` : '20px', onMount: onMount, editorOptions: options }), fzfSearch && (_jsx(Tooltip, { title: t('route.history'), children: _jsx(History, {}) })), _jsx(TuiIconButton, { disabled: searching || disabled, onClick: search, sx: { ml: 1, alignSelf: 'start', flexShrink: 0 }, size: compact ? 'small' : 'medium', children: _jsx(Tooltip, { title: t('route.search'), children: _jsx(Badge, { invisible: !isDirty, color: "warning", variant: "dot", children: _jsx(Search, { sx: { fontSize: '20px' } }) }) }) }), !loaded && (_jsx(Skeleton, { variant: "rectangular", sx: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }, height: "100%" })), multiline && (_jsx(Box, { sx: {
|
|
132
132
|
position: 'absolute',
|
|
133
133
|
left: 0,
|
|
134
134
|
right: 0,
|
|
@@ -100,7 +100,7 @@ const SearchPane = () => {
|
|
|
100
100
|
const bundleHit = useContextSelector(HitContext, ctx => location.pathname.startsWith('/bundles') ? ctx.hits[routeParams.id] : null);
|
|
101
101
|
const searchPaneWidth = useMyLocalStorageItem(StorageKey.SEARCH_PANE_WIDTH, null)[0];
|
|
102
102
|
const verticalSorters = useMediaQuery('(max-width: 1919px)') || (searchPaneWidth ?? Number.MAX_SAFE_INTEGER) < 900;
|
|
103
|
-
const selectedView = useContextSelector(ViewContext, ctx => ctx.views
|
|
103
|
+
const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
|
|
104
104
|
const getSelectedId = useCallback((event) => {
|
|
105
105
|
const target = event.target;
|
|
106
106
|
const selectedElement = target.closest('[id]');
|
|
@@ -4,6 +4,7 @@ import { Alert, IconButton, Stack, Tooltip, Typography } from '@mui/material';
|
|
|
4
4
|
import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
|
|
5
5
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
6
6
|
import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
|
|
7
|
+
import { has } from 'lodash-es';
|
|
7
8
|
import { memo, useMemo } from 'react';
|
|
8
9
|
import { useTranslation } from 'react-i18next';
|
|
9
10
|
import { Link } from 'react-router-dom';
|
|
@@ -14,8 +15,8 @@ const ViewLink = () => {
|
|
|
14
15
|
const sort = useContextSelector(ParameterContext, ctx => ctx.sort);
|
|
15
16
|
const span = useContextSelector(ParameterContext, ctx => ctx.span);
|
|
16
17
|
const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
|
|
17
|
-
const viewsReady = useContextSelector(ViewContext, ctx => ctx.
|
|
18
|
-
const selectedView = useContextSelector(ViewContext, ctx => ctx.views
|
|
18
|
+
const viewsReady = useContextSelector(ViewContext, ctx => has(ctx.views, viewId));
|
|
19
|
+
const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
|
|
19
20
|
const viewUrl = useMemo(() => {
|
|
20
21
|
if (viewId) {
|
|
21
22
|
return `/views/${viewId}/edit`;
|