@cccsaurora/howler-ui 2.12.1 → 2.13.0-dev.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/api/search/facet/hit.d.ts +4 -2
  2. package/api/search/facet/hit.js +5 -5
  3. package/api/search/facet/index.d.ts +1 -0
  4. package/components/app/providers/FavouritesProvider.js +27 -30
  5. package/components/app/providers/HitSearchProvider.js +7 -10
  6. package/components/app/providers/ViewProvider.d.ts +5 -4
  7. package/components/app/providers/ViewProvider.js +58 -36
  8. package/components/elements/hit/HitActions.js +1 -1
  9. package/components/elements/hit/HitSummary.js +5 -5
  10. package/components/elements/hit/aggregate/HitGraph.js +6 -9
  11. package/components/routes/advanced/luceneCompletionProvider.js +4 -2
  12. package/components/routes/analytics/widgets/Assessment.js +3 -2
  13. package/components/routes/analytics/widgets/Escalation.js +4 -3
  14. package/components/routes/analytics/widgets/Stacked.js +4 -3
  15. package/components/routes/hits/search/HitBrowser.js +8 -8
  16. package/components/routes/hits/search/SearchPane.js +1 -1
  17. package/components/routes/hits/search/ViewLink.js +3 -2
  18. package/components/routes/hits/search/grid/HitGrid.js +5 -7
  19. package/components/routes/hits/search/shared/HitFilter.js +2 -2
  20. package/components/routes/hits/search/shared/HitSort.js +9 -8
  21. package/components/routes/hits/search/shared/QuerySettings.js +1 -1
  22. package/components/routes/hits/search/shared/SearchSpan.js +17 -13
  23. package/components/routes/home/AddNewCard.js +14 -1
  24. package/components/routes/home/ViewCard.js +5 -1
  25. package/components/routes/home/index.js +1 -1
  26. package/components/routes/views/ViewComposer.js +17 -16
  27. package/components/routes/views/Views.js +25 -14
  28. package/package.json +5 -1
  29. package/plugins/borealis/Provider.d.ts +3 -0
  30. package/plugins/borealis/Provider.js +14 -0
  31. package/plugins/borealis/components/BorealisChip.d.ts +3 -0
  32. package/plugins/borealis/components/BorealisChip.js +27 -0
  33. package/plugins/borealis/components/BorealisLeadForm.d.ts +4 -0
  34. package/plugins/borealis/components/BorealisLeadForm.js +23 -0
  35. package/plugins/borealis/components/BorealisPivot.d.ts +3 -0
  36. package/plugins/borealis/components/BorealisPivot.js +83 -0
  37. package/plugins/borealis/components/BorealisPivotForm.d.ts +4 -0
  38. package/plugins/borealis/components/BorealisPivotForm.js +44 -0
  39. package/plugins/borealis/components/BorealisTypography.d.ts +3 -0
  40. package/plugins/borealis/components/BorealisTypography.js +53 -0
  41. package/plugins/borealis/helpers.d.ts +6 -0
  42. package/plugins/borealis/helpers.js +137 -0
  43. package/plugins/borealis/index.d.ts +21 -0
  44. package/plugins/borealis/index.js +46 -0
  45. package/plugins/borealis/locales/borealis.en.json +7 -0
  46. package/plugins/borealis/locales/borealis.fr.json +7 -0
  47. package/plugins/borealis/setup.d.ts +2 -0
  48. 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: (field: string) => string;
3
- export declare const post: (field: string, request?: HowlerFacetSearchRequest) => Promise<HowlerFacetSearchResponse>;
2
+ export declare const uri: () => string;
3
+ export declare const post: (request?: HowlerFacetSearchRequest) => Promise<{
4
+ [index: string]: HowlerFacetSearchResponse;
5
+ }>;
@@ -1,8 +1,8 @@
1
- import { hpost, joinAllUri } from '@cccsaurora/howler-ui/api';
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 = (field) => {
4
- return joinAllUri(parentUri(), 'hit', field);
3
+ export const uri = () => {
4
+ return joinUri(parentUri(), 'hit');
5
5
  };
6
- export const post = (field, request) => {
7
- return hpost(uri(field), { ...(request || {}), query: request?.query || 'howler.id:*' });
6
+ export const post = (request) => {
7
+ return hpost(uri(), { ...(request || {}), query: request?.query || 'howler.id:*' });
8
8
  };
@@ -1,6 +1,7 @@
1
1
  import * as hit from '@cccsaurora/howler-ui/api/search/facet/hit';
2
2
  export declare const uri: () => string;
3
3
  export type HowlerFacetSearchRequest = {
4
+ fields?: string[];
4
5
  query?: string;
5
6
  mincount?: number;
6
7
  rows?: number;
@@ -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 { uniqBy } from 'lodash-es';
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 views = useContextSelector(ViewContext, ctx => ctx.views);
17
- const viewsReady = useContextSelector(ViewContext, ctx => ctx.ready);
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 items = uniqBy(favourites
34
- .map(view_id => {
35
- const view = views?.find(v => v.view_id === view_id);
36
- return view
37
- ? {
38
- id: view.view_id,
39
- text: t(view.title),
40
- route: `/views/${view.view_id}`,
41
- nested: true
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, views]);
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() || !viewsReady || !analytics.ready) {
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
- const analyticElement = processAnalyticElement();
125
- if (analyticElement) {
126
- newElements.splice(1, 0, analyticElement);
127
- }
128
- const viewElement = processViewElement();
129
- if (viewElement) {
130
- newElements.splice(1, 0, viewElement);
131
- }
132
- leftNav.setElements(newElements);
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, viewsReady]);
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 viewsReady = useContextSelector(ViewContext, ctx => ctx.ready);
29
- const views = useContextSelector(ViewContext, ctx => ctx.views);
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), [location.pathname, routeParams.id]);
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 = `(${views.find(_view => _view.view_id === viewId)?.query || 'howler.id:*'}) AND (${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
- views,
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, viewsReady, startDate, endDate]);
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: View[];
6
+ views: {
7
+ [viewId: string]: View;
8
+ };
8
9
  addFavourite: (id: string) => Promise<void>;
9
10
  removeFavourite: (id: string) => Promise<void>;
10
- fetchViews: (force?: boolean) => Promise<void>;
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 [loading, setLoading] = useState(false);
18
- const [views, setViews] = useState({ ready: false, views: [] });
19
- const fetchViews = useCallback(async (force = false) => {
20
- if (views.ready && !force) {
21
- return;
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
- if (!appUser.isReady()) {
24
- return;
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
- setViews({ ready: true, views: await api.view.get() });
29
- }
30
- finally {
31
- setLoading(false);
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
- }, [appUser, views.ready]);
34
- useEffect(() => {
35
- if (!views.ready && !loading) {
36
- fetchViews();
45
+ catch (e) {
46
+ // eslint-disable-next-line no-console
47
+ console.warn(e);
48
+ return [];
37
49
  }
38
- }, [fetchViews, views.ready, appUser, loading]);
50
+ }, [dispatchApi, views]);
39
51
  useEffect(() => {
40
- if (defaultView && views.views?.length > 0 && !views.views?.find(v => v.view_id === defaultView)) {
41
- setDefaultView(undefined);
52
+ if (!defaultView || has(views, defaultView)) {
53
+ return;
42
54
  }
43
- });
44
- const getCurrentView = useCallback(() => {
45
- if (!location.pathname.startsWith('/views')) {
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
- return views.views.find(_view => _view.view_id === routeParams.id);
49
- }, [location.pathname, routeParams.id, views.views]);
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
- views: _views.views.map(v => v.view_id === id ? { ...v, title, query, sort, span, settings: { advance_on_triage: advanceOnTriage } } : v)
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, views: [..._views.views, newView] }));
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 => ({ ..._views, views: _views.views.filter(v => v.view_id !== id) }));
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
- ...views,
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
- setLoading(true);
88
- // Facet each field
89
- for (const key of sortedKeys) {
90
- const result = await dispatchApi(api.search.facet.hit.post(key, {
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
- [key]: result
102
+ ...result
103
103
  }));
104
104
  }
105
105
  }
@@ -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: graphQuery,
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) || searching || error) {
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, searching, error]);
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.post(key, { query: 'howler.id:*', rows: 250 }).catch(() => ({}));
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('howler.assessment', {
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('howler.escalation', {
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(field, {
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.find(_view => _view.view_id === viewId)?.query || 'howler.id:*'}) AND (${_fullQuery})`;
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 !== routeParams.id) {
71
+ if (selectedHits.length === 1 && selected && selectedHits[0]?.howler.id !== selected) {
73
72
  return true;
74
73
  }
75
74
  return false;
76
- }, [routeParams.id, selectedHits]);
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') && !viewsReady) {
98
- fetchViews(true);
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, viewsReady]);
101
+ }, [location.pathname, viewId]);
102
102
  const onClose = useCallback(() => {
103
103
  setSelected(null);
104
104
  }, [setSelected]);
@@ -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?.find(val => val.view_id === viewId));
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.ready);
18
- const selectedView = useContextSelector(ViewContext, ctx => ctx.views?.find(val => val.view_id === viewId));
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 viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : null), [location.pathname, routeParams.id]);
45
- const selectedView = useContextSelector(ViewContext, ctx => ctx.views?.find(val => val.view_id === viewId));
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 !== routeParams.id) {
60
+ if (selectedHits.length === 1 && selected && selectedHits[0]?.howler.id !== selected) {
63
61
  return true;
64
62
  }
65
63
  return false;
66
- }, [routeParams.id, selectedHits]);
64
+ }, [selected, selectedHits]);
67
65
  useEffect(() => {
68
66
  response?.items.forEach(hit => {
69
67
  if (!analyticIds[hit.howler.analytic]) {