@cccsaurora/howler-ui 2.13.0-dev.75 → 2.13.0-dev.96
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/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/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/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 +1 -1
|
@@ -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
|
}
|
|
@@ -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`;
|
|
@@ -16,7 +16,6 @@ import useHitSelection from '@cccsaurora/howler-ui/components/hooks/useHitSelect
|
|
|
16
16
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
17
17
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
18
18
|
import { useTranslation } from 'react-i18next';
|
|
19
|
-
import { useLocation, useParams } from 'react-router-dom';
|
|
20
19
|
import { useContextSelector } from 'use-context-selector';
|
|
21
20
|
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
22
21
|
import HitContextMenu from '../HitContextMenu';
|
|
@@ -29,8 +28,6 @@ import HitRow from './HitRow';
|
|
|
29
28
|
const HitGrid = () => {
|
|
30
29
|
const { t } = useTranslation();
|
|
31
30
|
const { getIdFromName } = useContext(AnalyticContext);
|
|
32
|
-
const routeParams = useParams();
|
|
33
|
-
const location = useLocation();
|
|
34
31
|
const theme = useTheme();
|
|
35
32
|
const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
36
33
|
const { onClick } = useHitSelection();
|
|
@@ -39,10 +36,11 @@ const HitGrid = () => {
|
|
|
39
36
|
const setDisplayType = useContextSelector(HitSearchContext, ctx => ctx.setDisplayType);
|
|
40
37
|
const response = useContextSelector(HitSearchContext, ctx => ctx.response);
|
|
41
38
|
const searching = useContextSelector(HitSearchContext, ctx => ctx.searching);
|
|
39
|
+
const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
|
|
42
40
|
const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
|
|
43
41
|
const query = useContextSelector(ParameterContext, ctx => ctx.query);
|
|
44
|
-
const
|
|
45
|
-
const selectedView = useContextSelector(ViewContext, ctx => ctx.views
|
|
42
|
+
const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
|
|
43
|
+
const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
|
|
46
44
|
const [collapseMainColumn, setCollapseMainColumn] = useMyLocalStorageItem(StorageKey.GRID_COLLAPSE_COLUMN, false);
|
|
47
45
|
const [analyticIds, setAnalyticIds] = useState({});
|
|
48
46
|
const columnModalRef = useRef();
|
|
@@ -59,11 +57,11 @@ const HitGrid = () => {
|
|
|
59
57
|
if (selectedHits.length > 1) {
|
|
60
58
|
return true;
|
|
61
59
|
}
|
|
62
|
-
if (selectedHits.length === 1 && selectedHits[0]?.howler.id !==
|
|
60
|
+
if (selectedHits.length === 1 && selected && selectedHits[0]?.howler.id !== selected) {
|
|
63
61
|
return true;
|
|
64
62
|
}
|
|
65
63
|
return false;
|
|
66
|
-
}, [
|
|
64
|
+
}, [selected, selectedHits]);
|
|
67
65
|
useEffect(() => {
|
|
68
66
|
response?.items.forEach(hit => {
|
|
69
67
|
if (!analyticIds[hit.howler.analytic]) {
|
|
@@ -4,7 +4,7 @@ import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers
|
|
|
4
4
|
import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
|
|
5
5
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { useLocation
|
|
7
|
+
import { useLocation } from 'react-router-dom';
|
|
8
8
|
import { useContextSelector } from 'use-context-selector';
|
|
9
9
|
import CustomSort from '../CustomSort';
|
|
10
10
|
const CUSTOM = '__custom__';
|
|
@@ -21,8 +21,7 @@ const ACCEPTED_SORTS = [
|
|
|
21
21
|
const HitSort = ({ size = 'small' }) => {
|
|
22
22
|
const { t } = useTranslation();
|
|
23
23
|
const location = useLocation();
|
|
24
|
-
const
|
|
25
|
-
const views = useContextSelector(ViewContext, ctx => ctx.views);
|
|
24
|
+
const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
|
|
26
25
|
const savedSort = useContextSelector(ParameterContext, ctx => ctx.sort);
|
|
27
26
|
const setSavedSort = useContextSelector(ParameterContext, ctx => ctx.setSort);
|
|
28
27
|
const sortEntries = useMemo(() => savedSort.split(',').filter(part => !!part), [savedSort]);
|
|
@@ -38,7 +37,6 @@ const HitSort = ({ size = 'small' }) => {
|
|
|
38
37
|
* Should the custom sorter be shown? Defaults to true if there's more than one sort field, or we're sorting on a field not supported by the default dropdown
|
|
39
38
|
*/
|
|
40
39
|
const [showCustomSort, setShowCustomSort] = useState(sortEntries.length > 1 || (sortEntries.length > 0 && !ACCEPTED_SORTS.includes(sortEntries[0]?.split(' ')[0])));
|
|
41
|
-
const viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : null), [location.pathname, routeParams.id]);
|
|
42
40
|
/**
|
|
43
41
|
* This handles changing the sort if the basic sorter is used, OR enables the custom sorting.
|
|
44
42
|
*/
|
|
@@ -51,14 +49,17 @@ const HitSort = ({ size = 'small' }) => {
|
|
|
51
49
|
}
|
|
52
50
|
}, [setSavedSort, sort]);
|
|
53
51
|
useEffect(() => {
|
|
54
|
-
if (
|
|
55
|
-
|
|
52
|
+
if (location.search.includes('sort')) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
(async () => {
|
|
56
|
+
const selectedView = await getCurrentView(true);
|
|
56
57
|
if (selectedView?.sort && !location.search.includes('sort')) {
|
|
57
58
|
setSavedSort(selectedView.sort);
|
|
58
59
|
}
|
|
59
|
-
}
|
|
60
|
+
})();
|
|
60
61
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
61
|
-
}, [
|
|
62
|
+
}, [getCurrentView]);
|
|
62
63
|
return !showCustomSort ? (_jsxs(Stack, { direction: "row", spacing: 1, sx: { flex: 1.5 }, children: [_jsx(Autocomplete, { fullWidth: true, sx: { minWidth: '175px' }, size: size, value: field, options: ACCEPTED_SORTS, getOptionLabel: option => (option === CUSTOM ? t('hit.search.custom') : option), isOptionEqualToValue: (option, value) => option === value || (!value && option === ACCEPTED_SORTS[0]), renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.sort.fields') }), onChange: (_, value) => handleChange(value) }), _jsxs(Select, { size: size, sx: { minWidth: '150px' }, value: sort, onChange: e => setSavedSort(`${field} ${e.target.value}`), children: [_jsx(MenuItem, { value: "asc", children: t('asc') }), _jsx(MenuItem, { value: "desc", children: t('desc') })] })] })) : (_jsx(CustomSort, {}));
|
|
63
64
|
};
|
|
64
65
|
export default memo(HitSort);
|
|
@@ -10,7 +10,7 @@ import HitSort from './HitSort';
|
|
|
10
10
|
import SearchSpan from './SearchSpan';
|
|
11
11
|
const QuerySettings = ({ verticalSorters = false, boxSx }) => {
|
|
12
12
|
const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
|
|
13
|
-
const selectedView = useContextSelector(ViewContext, ctx => ctx.views
|
|
13
|
+
const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
|
|
14
14
|
return (_jsxs(Box, { sx: boxSx ?? { position: 'relative', maxWidth: '1200px' }, children: [_jsxs(Stack, { direction: verticalSorters ? 'column' : 'row', justifyContent: "space-between", spacing: 1, divider: !verticalSorters && _jsx(Divider, { flexItem: true, orientation: "vertical" }), sx: [
|
|
15
15
|
viewId &&
|
|
16
16
|
!selectedView && {
|
|
@@ -2,9 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Autocomplete, TextField } from '@mui/material';
|
|
3
3
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
4
4
|
import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
|
|
5
|
-
import { memo, useEffect
|
|
5
|
+
import { memo, useEffect } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { useLocation
|
|
7
|
+
import { useLocation } from 'react-router-dom';
|
|
8
8
|
import { useContextSelector } from 'use-context-selector';
|
|
9
9
|
import { convertLuceneToDate } from '@cccsaurora/howler-ui/utils/utils';
|
|
10
10
|
const DATE_RANGES = [
|
|
@@ -18,23 +18,27 @@ const DATE_RANGES = [
|
|
|
18
18
|
const SearchSpan = ({ omitCustom = false, size }) => {
|
|
19
19
|
const { t } = useTranslation();
|
|
20
20
|
const location = useLocation();
|
|
21
|
-
const routeParams = useParams();
|
|
22
21
|
const span = useContextSelector(ParameterContext, ctx => ctx.span);
|
|
23
22
|
const setSpan = useContextSelector(ParameterContext, ctx => ctx.setSpan);
|
|
24
|
-
const
|
|
25
|
-
const selectedView = useContextSelector(ViewContext, ctx => ctx.views?.find(_view => _view.view_id === viewId));
|
|
23
|
+
const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
|
|
26
24
|
useEffect(() => {
|
|
27
|
-
if (
|
|
25
|
+
if (location.search.includes('span')) {
|
|
28
26
|
return;
|
|
29
27
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
(async () => {
|
|
29
|
+
const viewSpan = (await getCurrentView(true))?.span;
|
|
30
|
+
if (!viewSpan) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (viewSpan.includes(':')) {
|
|
34
|
+
setSpan(convertLuceneToDate(viewSpan));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
setSpan(viewSpan);
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
36
40
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
37
|
-
}, [
|
|
41
|
+
}, [getCurrentView]);
|
|
38
42
|
return (_jsx(Autocomplete, { fullWidth: true, sx: { minWidth: '200px', flex: 1 }, size: size ?? 'small', value: span, options: omitCustom ? DATE_RANGES.slice(0, DATE_RANGES.length - 1) : DATE_RANGES, renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.span') }), getOptionLabel: option => t(option), onChange: (_, value) => setSpan(value), disableClearable: true }));
|
|
39
43
|
};
|
|
40
44
|
export default memo(SearchSpan);
|
|
@@ -16,9 +16,12 @@ const VISUALIZATIONS = ['assessment', 'created', 'escalation', 'status', 'detect
|
|
|
16
16
|
const AddNewCard = ({ dashboard, addCard }) => {
|
|
17
17
|
const { t } = useTranslation();
|
|
18
18
|
const views = useContextSelector(ViewContext, ctx => ctx.views ?? []);
|
|
19
|
+
const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
|
|
19
20
|
const [selectedType, setSelectedType] = useState('');
|
|
20
21
|
const [analytics, setAnalytics] = useState([]);
|
|
21
22
|
const [config, _setConfig] = useState({});
|
|
23
|
+
const [viewOpen, setViewOpen] = useState(false);
|
|
24
|
+
const [viewLoading, setViewLoading] = useState(false);
|
|
22
25
|
const setConfig = useCallback((key, value) => _setConfig(_config => ({ ..._config, [key]: value })), []);
|
|
23
26
|
const _addCard = useCallback(() => {
|
|
24
27
|
if (!selectedType) {
|
|
@@ -43,9 +46,19 @@ const AddNewCard = ({ dashboard, addCard }) => {
|
|
|
43
46
|
_setConfig({});
|
|
44
47
|
}
|
|
45
48
|
}, [selectedType]);
|
|
49
|
+
const onViewOpen = useCallback(async () => {
|
|
50
|
+
setViewOpen(true);
|
|
51
|
+
setViewLoading(true);
|
|
52
|
+
try {
|
|
53
|
+
await fetchViews();
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
setViewLoading(false);
|
|
57
|
+
}
|
|
58
|
+
}, [fetchViews]);
|
|
46
59
|
return (_jsx(Grid, { item: true, xs: 12, md: 6, children: _jsxs(Card, { variant: "outlined", sx: { height: '100%' }, children: [_jsx(CardHeader, { title: t('route.home.add'), subheader: _jsx(Typography, { variant: "body2", color: "text.secondary", children: t('route.home.add.description') }) }), _jsx(CardContent, { children: _jsxs(Stack, { spacing: 1, children: [_jsxs(FormControl, { sx: theme => ({ mt: `${theme.spacing(2)} !important` }), children: [_jsx(InputLabel, { children: t('route.home.add.type') }), _jsx(Select, { value: selectedType, onChange: event => setSelectedType(event.target.value), label: t('route.home.add.type'), children: Object.keys(TYPES).map(type => (_jsx(MenuItem, { value: type, children: _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(`route.home.add.type.${type}`) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t(`route.home.add.type.${type}.description`) })] }) }, type))) })] }), selectedType && _jsx(Divider, { flexItem: true }), selectedType === 'analytic' && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body1", children: t('route.home.add.analytic.title') }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('route.home.add.analytic.description') }), _jsx(Autocomplete, { sx: { pt: 1 }, onChange: (__, opt) => setConfig('analyticId', opt.analytic_id), options: analytics, filterOptions: (options, state) => options.filter(opt => opt.name.toLowerCase().includes(state.inputValue.toLowerCase()) ||
|
|
47
60
|
opt.description?.split('\n')[0]?.toLowerCase().includes(state.inputValue.toLowerCase())), renderOption: (props, option) => (_createElement("li", { ...props, key: option.analytic_id },
|
|
48
|
-
_jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: option.name }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: option.description?.split('\n')[0] })] }))), getOptionLabel: option => option.name, renderInput: params => _jsx(TextField, { ...params, label: t('route.home.add.analytic') }) }), _jsxs(FormControl, { sx: theme => ({ mt: `${theme.spacing(2)} !important` }), children: [_jsx(InputLabel, { children: t('route.home.add.visualization') }), _jsx(Select, { value: config.type ?? '', onChange: event => setConfig('type', event.target.value), label: t('route.home.add.visualization'), children: VISUALIZATIONS.map(viz => (_jsx(MenuItem, { value: viz, children: _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(`route.home.add.visualization.${viz}`) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t(`route.home.add.visualization.${viz}.description`) })] }) }, viz))) })] })] })), selectedType === 'view' && (_jsxs(_Fragment, { children: [_jsx(Autocomplete, { sx: { pt: 1 }, onChange: (__, opt) => setConfig('viewId', opt.view_id), options: views, filterOptions: (options, state) => options.filter(opt => !dashboard?.find(entry => entry.type === 'view' && JSON.parse(entry.config).viewId === opt.view_id) &&
|
|
61
|
+
_jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: option.name }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: option.description?.split('\n')[0] })] }))), getOptionLabel: option => option.name, renderInput: params => _jsx(TextField, { ...params, label: t('route.home.add.analytic') }) }), _jsxs(FormControl, { sx: theme => ({ mt: `${theme.spacing(2)} !important` }), children: [_jsx(InputLabel, { children: t('route.home.add.visualization') }), _jsx(Select, { value: config.type ?? '', onChange: event => setConfig('type', event.target.value), label: t('route.home.add.visualization'), children: VISUALIZATIONS.map(viz => (_jsx(MenuItem, { value: viz, children: _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(`route.home.add.visualization.${viz}`) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t(`route.home.add.visualization.${viz}.description`) })] }) }, viz))) })] })] })), selectedType === 'view' && (_jsxs(_Fragment, { children: [_jsx(Autocomplete, { sx: { pt: 1 }, onChange: (__, opt) => setConfig('viewId', opt.view_id), onOpen: onViewOpen, onClose: () => setViewOpen(false), open: viewOpen, loading: viewLoading, options: Object.values(views), filterOptions: (options, state) => options.filter(opt => !dashboard?.find(entry => entry.type === 'view' && JSON.parse(entry.config).viewId === opt.view_id) &&
|
|
49
62
|
(opt.title.toLowerCase().includes(state.inputValue.toLowerCase()) ||
|
|
50
63
|
opt.query.toLowerCase().includes(state.inputValue.toLowerCase()))), renderOption: (props, option) => (_createElement("li", { ...props, key: option.view_id },
|
|
51
64
|
_jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(option.title) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: option.query })] }))), getOptionLabel: option => t(option.title), renderInput: params => _jsx(TextField, { ...params, label: t('route.home.add.view') }) }), _jsx(Typography, { variant: "body1", sx: { pt: 1 }, children: t('route.home.add.limit') }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('route.home.add.limit.description') }), _jsx(Box, { sx: { px: 0.5 }, children: _jsx(Slider, { value: config.limit ?? 3, valueLabelDisplay: "auto", onChange: (_, value) => setConfig('limit', value), min: 1, max: 10, step: 1, marks: true }) })] })), _jsx(Stack, { direction: "row", justifyContent: "end", children: _jsx(CustomButton, { variant: "outlined", size: "small", color: "primary", startIcon: _jsx(Check, {}), disabled: !selectedType || TYPES[selectedType]?.filter(field => !config[field])?.length > 0, onClick: _addCard, children: t('create') }) })] }) })] }) }));
|
|
@@ -17,7 +17,11 @@ const ViewCard = ({ viewId, limit }) => {
|
|
|
17
17
|
const { dispatchApi } = useMyApi();
|
|
18
18
|
const [hits, setHits] = useState([]);
|
|
19
19
|
const [loading, setLoading] = useState(false);
|
|
20
|
-
const view = useContextSelector(ViewContext, ctx => ctx.views
|
|
20
|
+
const view = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
|
|
21
|
+
const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
fetchViews([viewId]);
|
|
24
|
+
}, [fetchViews, viewId]);
|
|
21
25
|
useEffect(() => {
|
|
22
26
|
if (!view?.query) {
|
|
23
27
|
return;
|
|
@@ -36,7 +36,7 @@ const ViewComposer = () => {
|
|
|
36
36
|
const navigate = useNavigate();
|
|
37
37
|
const addView = useContextSelector(ViewContext, ctx => ctx.addView);
|
|
38
38
|
const editView = useContextSelector(ViewContext, ctx => ctx.editView);
|
|
39
|
-
const
|
|
39
|
+
const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
|
|
40
40
|
const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
|
|
41
41
|
const loadHits = useContextSelector(HitContext, ctx => ctx.loadHits);
|
|
42
42
|
// view state
|
|
@@ -130,29 +130,30 @@ const ViewComposer = () => {
|
|
|
130
130
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
131
131
|
}, [sort, span]);
|
|
132
132
|
useEffect(() => {
|
|
133
|
-
if (routeParams.id) {
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
if (!routeParams.id) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
(async () => {
|
|
137
|
+
const viewToEdit = await getCurrentView();
|
|
138
|
+
if (!viewToEdit) {
|
|
136
139
|
setError('route.views.missing');
|
|
137
140
|
return;
|
|
138
141
|
}
|
|
139
142
|
else {
|
|
140
143
|
setError(null);
|
|
141
144
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
setSort(viewToEdit.sort);
|
|
148
|
-
}
|
|
149
|
-
if (viewToEdit.span) {
|
|
150
|
-
setSpan(viewToEdit.span);
|
|
151
|
-
}
|
|
145
|
+
setTitle(viewToEdit.title);
|
|
146
|
+
setAdvanceOnTriage(viewToEdit.settings?.advance_on_triage ?? false);
|
|
147
|
+
setQuery(viewToEdit.query);
|
|
148
|
+
if (viewToEdit.sort) {
|
|
149
|
+
setSort(viewToEdit.sort);
|
|
152
150
|
}
|
|
153
|
-
|
|
151
|
+
if (viewToEdit.span) {
|
|
152
|
+
setSpan(viewToEdit.span);
|
|
153
|
+
}
|
|
154
|
+
})();
|
|
154
155
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
155
|
-
}, [routeParams.id,
|
|
156
|
+
}, [routeParams.id, getCurrentView]);
|
|
156
157
|
return (_jsx(FlexPort, { children: _jsx(ErrorBoundary, { children: _jsx(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: _jsxs(VSBox, { top: 0, children: [_jsx(VSBoxHeader, { pb: 1, children: _jsxs(Stack, { spacing: 1, children: [error && (_jsx(Alert, { variant: "outlined", severity: "error", children: t(error) })), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(TextField, { label: t('route.views.name'), size: "small", value: title, onChange: e => setTitle(e.target.value), fullWidth: true }), _jsxs(ToggleButtonGroup, { sx: { display: 'grid', gridTemplateColumns: '1fr 1fr' }, size: "small", exclusive: true, value: type, onChange: (__, _type) => {
|
|
157
158
|
if (_type) {
|
|
158
159
|
setType(_type);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createElement as _createElement } from "react";
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { Clear, Edit, SavedSearch, Star, StarBorder } from '@mui/icons-material';
|
|
3
4
|
import { Autocomplete, Card, Checkbox, IconButton, Skeleton, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from '@mui/material';
|
|
@@ -12,6 +13,7 @@ import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemM
|
|
|
12
13
|
import { ViewTitle } from '@cccsaurora/howler-ui/components/elements/view/ViewTitle';
|
|
13
14
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
14
15
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
16
|
+
import { isNull, omitBy, size } from 'lodash-es';
|
|
15
17
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
|
16
18
|
import { useTranslation } from 'react-i18next';
|
|
17
19
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
|
@@ -24,8 +26,8 @@ const ViewsBase = () => {
|
|
|
24
26
|
const { user } = useAppUser();
|
|
25
27
|
const navigate = useNavigate();
|
|
26
28
|
const { dispatchApi } = useMyApi();
|
|
27
|
-
const addFavourite = useContextSelector(ViewContext, ctx => ctx.addFavourite);
|
|
28
29
|
const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
|
|
30
|
+
const addFavourite = useContextSelector(ViewContext, ctx => ctx.addFavourite);
|
|
29
31
|
const removeFavourite = useContextSelector(ViewContext, ctx => ctx.removeFavourite);
|
|
30
32
|
const removeView = useContextSelector(ViewContext, ctx => ctx.removeView);
|
|
31
33
|
const views = useContextSelector(ViewContext, ctx => ctx.views);
|
|
@@ -41,6 +43,8 @@ const ViewsBase = () => {
|
|
|
41
43
|
const [hasError, setHasError] = useState(false);
|
|
42
44
|
const [searching, setSearching] = useState(false);
|
|
43
45
|
const [favouritesOnly, setFavouritesOnly] = useState(false);
|
|
46
|
+
const [defaultViewOpen, setDefaultViewOpen] = useState(false);
|
|
47
|
+
const [defaultViewLoading, setDefaultViewLoading] = useState(false);
|
|
44
48
|
const onSearch = useCallback(async () => {
|
|
45
49
|
try {
|
|
46
50
|
setSearching(true);
|
|
@@ -52,7 +56,6 @@ const ViewsBase = () => {
|
|
|
52
56
|
searchParams.delete('phrase');
|
|
53
57
|
}
|
|
54
58
|
setSearchParams(searchParams, { replace: true });
|
|
55
|
-
fetchViews(true);
|
|
56
59
|
const searchTerm = phrase ? `*${sanitizeLuceneQuery(phrase)}*` : '*';
|
|
57
60
|
const phraseQuery = FIELDS_TO_SEARCH.map(_field => `${_field}:${searchTerm}`).join(' OR ');
|
|
58
61
|
const typeQuery = `(type:global OR owner:(${user.username} OR none)) AND type:(${types.join(' OR ') || '*'}${types.includes('personal') ? ' OR readonly' : ''})`;
|
|
@@ -73,7 +76,6 @@ const ViewsBase = () => {
|
|
|
73
76
|
phrase,
|
|
74
77
|
setSearchParams,
|
|
75
78
|
searchParams,
|
|
76
|
-
fetchViews,
|
|
77
79
|
user.username,
|
|
78
80
|
user.favourite_views,
|
|
79
81
|
types,
|
|
@@ -105,29 +107,37 @@ const ViewsBase = () => {
|
|
|
105
107
|
const onDelete = useCallback(async (event, id) => {
|
|
106
108
|
event.preventDefault();
|
|
107
109
|
event.stopPropagation();
|
|
108
|
-
await
|
|
110
|
+
await removeView(id);
|
|
109
111
|
onSearch();
|
|
110
|
-
}, [
|
|
112
|
+
}, [onSearch, removeView]);
|
|
111
113
|
const onFavourite = useCallback(async (event, id) => {
|
|
112
114
|
event.preventDefault();
|
|
113
115
|
if (user.favourite_views?.includes(id)) {
|
|
114
|
-
await
|
|
116
|
+
await removeFavourite(id);
|
|
115
117
|
if (user.favourite_views?.length < 2) {
|
|
116
118
|
setFavouritesOnly(false);
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
else {
|
|
120
|
-
await
|
|
122
|
+
await addFavourite(id);
|
|
123
|
+
}
|
|
124
|
+
}, [addFavourite, removeFavourite, user.favourite_views]);
|
|
125
|
+
const onDefaultViewOpen = useCallback(async () => {
|
|
126
|
+
setDefaultViewOpen(true);
|
|
127
|
+
setDefaultViewLoading(true);
|
|
128
|
+
try {
|
|
129
|
+
await fetchViews();
|
|
121
130
|
}
|
|
122
|
-
|
|
131
|
+
finally {
|
|
132
|
+
setDefaultViewLoading(false);
|
|
133
|
+
}
|
|
134
|
+
}, [fetchViews]);
|
|
123
135
|
useEffect(() => {
|
|
124
|
-
onSearch();
|
|
125
136
|
if (!searchParams.has('offset')) {
|
|
126
137
|
searchParams.set('offset', '0');
|
|
127
138
|
setSearchParams(searchParams, { replace: true });
|
|
128
139
|
}
|
|
129
|
-
|
|
130
|
-
}, [dispatchApi, types]);
|
|
140
|
+
}, [searchParams, setSearchParams]);
|
|
131
141
|
useEffect(() => {
|
|
132
142
|
if (response?.total <= offset) {
|
|
133
143
|
setOffset(0);
|
|
@@ -140,13 +150,14 @@ const ViewsBase = () => {
|
|
|
140
150
|
onSearch();
|
|
141
151
|
}
|
|
142
152
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
|
-
}, [offset, favouritesOnly]);
|
|
153
|
+
}, [offset, favouritesOnly, types]);
|
|
144
154
|
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' }, size: "small", value: types, onChange: (__, _types) => {
|
|
145
155
|
if (_types) {
|
|
146
156
|
setTypes(_types.length < 2 ? _types : []);
|
|
147
157
|
}
|
|
148
|
-
}, children: [_jsx(ToggleButton, { value: "personal", "aria-label": "personal", children: t('route.views.manager.personal') }), _jsx(ToggleButton, { value: "global", "aria-label": "global", children: t('route.views.manager.global') })] }) }), aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.views.search.prompt') }), afterSearch: views
|
|
149
|
-
|
|
158
|
+
}, children: [_jsx(ToggleButton, { value: "personal", "aria-label": "personal", children: t('route.views.manager.personal') }), _jsx(ToggleButton, { value: "global", "aria-label": "global", children: t('route.views.manager.global') })] }) }), aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.views.search.prompt') }), afterSearch: size(views) > 0 ? (_jsx(Autocomplete, { open: defaultViewOpen, loading: defaultViewLoading, onOpen: onDefaultViewOpen, onClose: () => setDefaultViewOpen(false), options: Object.values(omitBy(views, isNull)), renderOption: ({ key, ...props }, o) => (_createElement("li", { ...props, key: key },
|
|
159
|
+
_jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(o.title) }), _jsx(Typography, { variant: "caption", children: _jsx("code", { children: o.query }) })] }))), renderInput: params => (_jsx(TextField, { ...params, label: t('route.views.manager.default'), sx: { minWidth: '300px' } })), filterOptions: (_views, { inputValue }) => _views.filter(v => t(v.title).toLowerCase().includes(inputValue.toLowerCase()) ||
|
|
160
|
+
v.query.toLowerCase().includes(inputValue.toLowerCase())), getOptionLabel: (v) => t(v.title), isOptionEqualToValue: (view, value) => view.view_id === value.view_id, value: views[defaultView] ?? null, onChange: (_, option) => setDefaultView(option?.view_id) })) : (_jsx(Skeleton, { variant: "rounded", width: "300px", height: "initial" })), belowSearch: _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Checkbox, { size: "small", disabled: user.favourite_views?.length < 1, checked: favouritesOnly, onChange: (_, checked) => setFavouritesOnly(checked) }), _jsx(Typography, { variant: "body1", sx: theme => ({ color: theme.palette.text.disabled }), children: t('route.views.manager.favourites') })] }), renderer: ({ item }, classRenderer) => (_jsx(Card, { variant: "outlined", sx: { p: 1, mb: 1, transitionProperty: 'border-color', '&:hover': { borderColor: 'primary.main' } }, className: classRenderer(), children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { color: 'inherit', textDecoration: 'none' }, component: Link, to: `/views/${item.item.view_id}`, children: [_jsx(ViewTitle, { ...item.item }), _jsx(FlexOne, {}), ((item.item.owner === user.username && item.item.type !== 'readonly') ||
|
|
150
161
|
(item.item.type === 'global' && user.is_admin)) && (_jsx(Tooltip, { title: t('button.edit'), children: _jsx(IconButton, { component: Link, to: `/views/${item.item.view_id}/edit?query=${item.item.query}`, children: _jsx(Edit, {}) }) })), item.item.owner === user.username && item.item.type !== 'readonly' && (_jsx(Tooltip, { title: t('button.delete'), children: _jsx(IconButton, { onClick: event => onDelete(event, item.item.view_id), children: _jsx(Clear, {}) }) })), item.item.type === 'global' && item.item.owner !== user.username && (_jsx(Tooltip, { title: item.item.owner, children: _jsx("div", { children: _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important', marginLeft: '8px !important' }, userId: item.item.owner }) }) })), _jsx(Tooltip, { title: t('button.pin'), children: _jsx(IconButton, { onClick: e => onFavourite(e, item.item.view_id), children: user.favourite_views?.includes(item.item.view_id) ? _jsx(Star, {}) : _jsx(StarBorder, {}) }) })] }) }, item.item.view_id)), response: response, searchPrompt: "route.views.manager.search", onCreate: () => navigate('/views/create'), createPrompt: "route.views.create", createIcon: _jsx(SavedSearch, { sx: { mr: 1 } }) }));
|
|
151
162
|
};
|
|
152
163
|
const Views = () => {
|