@cccsaurora/howler-ui 2.13.0-dev.107 → 2.13.0-dev.125

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 (41) hide show
  1. package/api/hit/index.d.ts +1 -1
  2. package/api/hit/index.js +6 -2
  3. package/api/search/index.d.ts +1 -0
  4. package/commons/components/utils/hooks/useEnv.d.ts +1 -1
  5. package/components/app/App.js +1 -3
  6. package/components/app/hooks/useMatchers.d.ts +8 -0
  7. package/components/app/hooks/useMatchers.js +46 -0
  8. package/components/app/hooks/useMatchers.test.d.ts +1 -0
  9. package/components/app/hooks/useMatchers.test.js +237 -0
  10. package/components/app/hooks/useTitle.js +5 -4
  11. package/components/app/providers/HitProvider.d.ts +2 -1
  12. package/components/app/providers/HitProvider.js +8 -2
  13. package/components/app/providers/HitSearchProvider.d.ts +2 -1
  14. package/components/app/providers/HitSearchProvider.js +2 -1
  15. package/components/app/providers/SocketProvider.js +1 -1
  16. package/components/elements/display/handlebars/helpers.js +5 -2
  17. package/components/elements/hit/HitCard.js +0 -5
  18. package/components/elements/hit/HitOutline.d.ts +2 -2
  19. package/components/elements/hit/HitOutline.js +11 -21
  20. package/components/elements/hit/HitOverview.js +7 -4
  21. package/components/elements/hit/HitSummary.d.ts +2 -1
  22. package/components/elements/hit/HitSummary.js +7 -6
  23. package/components/elements/hit/related/PivotLink.js +10 -3
  24. package/components/routes/analytics/AnalyticTemplates.js +9 -6
  25. package/components/routes/hits/search/InformationPane.js +25 -40
  26. package/components/routes/hits/search/SearchPane.js +2 -8
  27. package/components/routes/hits/search/grid/AddColumnModal.js +10 -5
  28. package/components/routes/hits/view/HitViewer.js +17 -23
  29. package/components/routes/templates/TemplateViewer.js +27 -36
  30. package/components/routes/templates/Templates.js +4 -11
  31. package/locales/en/translation.json +1 -0
  32. package/locales/fr/translation.json +1 -0
  33. package/models/WithMetadata.d.ts +8 -0
  34. package/models/WithMetadata.js +1 -0
  35. package/package.json +104 -104
  36. package/setupTests.d.ts +1 -0
  37. package/setupTests.js +8 -0
  38. package/components/app/providers/DossierProvider.d.ts +0 -16
  39. package/components/app/providers/DossierProvider.js +0 -82
  40. package/components/app/providers/TemplateProvider.d.ts +0 -14
  41. package/components/app/providers/TemplateProvider.js +0 -103
@@ -20,7 +20,7 @@ export type HitActionResponse = {
20
20
  success: boolean;
21
21
  };
22
22
  export declare const uri: (id?: string) => string;
23
- export declare const get: (id: string) => Promise<Hit>;
23
+ export declare const get: <T extends Hit>(id: string, metadata?: string[]) => Promise<T>;
24
24
  interface PostResponse {
25
25
  valid: Hit[];
26
26
  invalid: {
package/api/hit/index.js CHANGED
@@ -7,8 +7,12 @@ import * as transition from '@cccsaurora/howler-ui/api/hit/transition';
7
7
  export const uri = (id) => {
8
8
  return id ? joinAllUri(parentUri(), 'hit', id) : joinUri(parentUri(), 'hit');
9
9
  };
10
- export const get = (id) => {
11
- return hget(uri(id));
10
+ export const get = (id, metadata) => {
11
+ const params = new URLSearchParams();
12
+ if (metadata) {
13
+ params.append('metadata', metadata.join(','));
14
+ }
15
+ return hget(uri(id), params);
12
16
  };
13
17
  export const post = (hits) => {
14
18
  return hpost(uri(), hits);
@@ -21,6 +21,7 @@ export type HowlerSearchRequest = {
21
21
  fl?: string;
22
22
  timeout?: number;
23
23
  filters?: string[];
24
+ metadata?: string[];
24
25
  };
25
26
  export type HowlerSearchResponse<T> = {
26
27
  items: T[];
@@ -1 +1 @@
1
- export default function useAppEnv(key?: string): any;
1
+ export default function useAppEnv(key?: string): string | NodeJS.ProcessEnv;
@@ -63,7 +63,6 @@ import AnalyticProvider from './providers/AnalyticProvider';
63
63
  import ApiConfigProvider, { ApiConfigContext } from './providers/ApiConfigProvider';
64
64
  import AvatarProvider from './providers/AvatarProvider';
65
65
  import CustomPluginProvider from './providers/CustomPluginProvider';
66
- import DossierProvider from './providers/DossierProvider';
67
66
  import FavouriteProvider from './providers/FavouritesProvider';
68
67
  import FieldProvider from './providers/FieldProvider';
69
68
  import HitProvider from './providers/HitProvider';
@@ -72,7 +71,6 @@ import ModalProvider from './providers/ModalProvider';
72
71
  import OverviewProvider from './providers/OverviewProvider';
73
72
  import ParameterProvider from './providers/ParameterProvider';
74
73
  import SocketProvider from './providers/SocketProvider';
75
- import TemplateProvider from './providers/TemplateProvider';
76
74
  import UserListProvider from './providers/UserListProvider';
77
75
  import ViewProvider from './providers/ViewProvider';
78
76
  loader.config({ monaco });
@@ -140,7 +138,7 @@ const MyAppProvider = ({ children }) => {
140
138
  const mySitemap = useMySitemap();
141
139
  const myUser = useMyUser();
142
140
  const mySearch = useMySearch();
143
- return (_jsx(ErrorBoundary, { children: _jsx(AppProvider, { preferences: myPreferences, theme: myTheme, sitemap: mySitemap, user: myUser, search: mySearch, children: _jsx(CustomPluginProvider, { children: _jsx(ErrorBoundary, { children: _jsx(ErrorBoundary, { children: _jsx(DossierProvider, { children: _jsx(ViewProvider, { children: _jsx(AvatarProvider, { children: _jsx(ModalProvider, { children: _jsx(FieldProvider, { children: _jsx(LocalStorageProvider, { children: _jsx(SocketProvider, { children: _jsx(HitProvider, { children: _jsx(TemplateProvider, { children: _jsx(OverviewProvider, { children: _jsx(AnalyticProvider, { children: _jsx(FavouriteProvider, { children: _jsx(UserListProvider, { children: children }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
141
+ return (_jsx(ErrorBoundary, { children: _jsx(AppProvider, { preferences: myPreferences, theme: myTheme, sitemap: mySitemap, user: myUser, search: mySearch, children: _jsx(CustomPluginProvider, { children: _jsx(ErrorBoundary, { children: _jsx(ErrorBoundary, { children: _jsx(ViewProvider, { children: _jsx(AvatarProvider, { children: _jsx(ModalProvider, { children: _jsx(FieldProvider, { children: _jsx(LocalStorageProvider, { children: _jsx(SocketProvider, { children: _jsx(HitProvider, { children: _jsx(OverviewProvider, { children: _jsx(AnalyticProvider, { children: _jsx(FavouriteProvider, { children: _jsx(UserListProvider, { children: children }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
144
142
  };
145
143
  const AppProviderWrapper = () => {
146
144
  return (_jsx(I18nextProvider, { i18n: i18n, defaultNS: "translation", children: _jsx(ApiConfigProvider, { children: _jsx(PluginProvider, { pluginStore: howlerPluginStore.pluginStore, children: _jsxs(MyAppProvider, { children: [_jsx(MyApp, {}), _jsx(Modal, {})] }) }) }) }));
@@ -0,0 +1,8 @@
1
+ import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
+ import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
3
+ declare const useMatchers: () => {
4
+ getMatchingDossiers: (hit: WithMetadata<Hit>) => Promise<import("../../../models/entities/generated/Dossier").Dossier[]>;
5
+ getMatchingOverview: (hit: WithMetadata<Hit>) => Promise<import("../../../models/entities/generated/Overview").Overview>;
6
+ getMatchingTemplate: (hit: WithMetadata<Hit>) => Promise<import("../../../models/entities/generated/Template").Template>;
7
+ };
8
+ export default useMatchers;
@@ -0,0 +1,46 @@
1
+ import { has } from 'lodash-es';
2
+ import { useCallback } from 'react';
3
+ import { useContextSelector } from 'use-context-selector';
4
+ import { HitContext } from '../providers/HitProvider';
5
+ const useMatchers = () => {
6
+ const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
7
+ const getMatchingTemplate = useCallback(async (hit) => {
8
+ if (!hit) {
9
+ return null;
10
+ }
11
+ if (has(hit, '__template')) {
12
+ return hit.__template;
13
+ }
14
+ // This is a fallback in case metadata is not included. In most cases templates are shown, the template metadata
15
+ // should also exist
16
+ return (await getHit(hit.howler.id, true)).__template;
17
+ }, [getHit]);
18
+ const getMatchingOverview = useCallback(async (hit) => {
19
+ if (!hit) {
20
+ return null;
21
+ }
22
+ if (has(hit, '__overview')) {
23
+ return hit.__overview;
24
+ }
25
+ // This is a fallback in case metadata is not included. In most cases templates are shown, the template metadata
26
+ // should also exist
27
+ return (await getHit(hit.howler.id, true)).__overview;
28
+ }, [getHit]);
29
+ const getMatchingDossiers = useCallback(async (hit) => {
30
+ if (!hit) {
31
+ return null;
32
+ }
33
+ if (has(hit, '__dossiers')) {
34
+ return hit.__dossiers;
35
+ }
36
+ // This is a fallback in case metadata is not included. In most cases templates are shown, the template metadata
37
+ // should also exist
38
+ return (await getHit(hit.howler.id, true)).__dossiers;
39
+ }, [getHit]);
40
+ return {
41
+ getMatchingDossiers,
42
+ getMatchingOverview,
43
+ getMatchingTemplate
44
+ };
45
+ };
46
+ export default useMatchers;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,237 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { useContextSelector } from 'use-context-selector';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { HitContext } from '../providers/HitProvider';
5
+ import useMatchers from './useMatchers';
6
+ // Mock the useContextSelector hook
7
+ vi.mock('use-context-selector', () => ({
8
+ useContextSelector: vi.fn(),
9
+ createContext: vi.fn()
10
+ }));
11
+ // Mock lodash-es has function
12
+ vi.mock('lodash-es', () => ({
13
+ has: vi.fn()
14
+ }));
15
+ // Create mock data
16
+ const mockTemplate = {
17
+ name: 'test-template',
18
+ template: 'Test template content'
19
+ };
20
+ const mockOverview = {
21
+ name: 'test-overview',
22
+ overview: 'Test overview content'
23
+ };
24
+ const mockDossiers = [
25
+ {
26
+ name: 'test-dossier',
27
+ description: 'Test dossier content'
28
+ }
29
+ ];
30
+ const mockHit = {
31
+ howler: {
32
+ id: 'test-hit-id',
33
+ analytic: 'test-analytic',
34
+ data: {}
35
+ }
36
+ };
37
+ const mockHitWithMetadata = {
38
+ ...mockHit,
39
+ __template: mockTemplate,
40
+ __overview: mockOverview,
41
+ __dossiers: mockDossiers
42
+ };
43
+ const mockGetHit = vi.fn();
44
+ describe('useMatchers', () => {
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ // Mock useContextSelector to return our mock getHit function
48
+ useContextSelector.mockImplementation((context, selector) => {
49
+ if (context === HitContext) {
50
+ return selector({ getHit: mockGetHit });
51
+ }
52
+ return null;
53
+ });
54
+ });
55
+ describe('getMatchingTemplate', () => {
56
+ it('should return null when hit is null', async () => {
57
+ const { result } = renderHook(() => useMatchers());
58
+ const template = await result.current.getMatchingTemplate(null);
59
+ expect(template).toBeNull();
60
+ });
61
+ it('should return null when hit is undefined', async () => {
62
+ const { result } = renderHook(() => useMatchers());
63
+ const template = await result.current.getMatchingTemplate(undefined);
64
+ expect(template).toBeNull();
65
+ });
66
+ it('should return template from metadata when it exists', async () => {
67
+ const { has } = await import('lodash-es');
68
+ has.mockReturnValue(true);
69
+ const { result } = renderHook(() => useMatchers());
70
+ const template = await result.current.getMatchingTemplate(mockHitWithMetadata);
71
+ expect(template).toBe(mockTemplate);
72
+ expect(mockGetHit).not.toHaveBeenCalled();
73
+ });
74
+ it('should fetch hit with metadata when template is not present', async () => {
75
+ const { has } = await import('lodash-es');
76
+ has.mockReturnValue(false);
77
+ const hitWithFetchedMetadata = { ...mockHit, __template: mockTemplate };
78
+ mockGetHit.mockResolvedValue(hitWithFetchedMetadata);
79
+ const { result } = renderHook(() => useMatchers());
80
+ const template = await result.current.getMatchingTemplate(mockHit);
81
+ expect(template).toBe(mockTemplate);
82
+ expect(mockGetHit).toHaveBeenCalledWith('test-hit-id', true);
83
+ });
84
+ it('should handle getHit rejection gracefully', async () => {
85
+ const { has } = await import('lodash-es');
86
+ has.mockReturnValue(false);
87
+ mockGetHit.mockRejectedValue(new Error('Failed to fetch hit'));
88
+ const { result } = renderHook(() => useMatchers());
89
+ await expect(result.current.getMatchingTemplate(mockHit)).rejects.toThrow('Failed to fetch hit');
90
+ expect(mockGetHit).toHaveBeenCalledWith('test-hit-id', true);
91
+ });
92
+ });
93
+ describe('getMatchingOverview', () => {
94
+ it('should return null when hit is null', async () => {
95
+ const { result } = renderHook(() => useMatchers());
96
+ const overview = await result.current.getMatchingOverview(null);
97
+ expect(overview).toBeNull();
98
+ });
99
+ it('should return null when hit is undefined', async () => {
100
+ const { result } = renderHook(() => useMatchers());
101
+ const overview = await result.current.getMatchingOverview(undefined);
102
+ expect(overview).toBeNull();
103
+ });
104
+ it('should return overview from metadata when it exists', async () => {
105
+ const { has } = await import('lodash-es');
106
+ has.mockReturnValue(true);
107
+ const { result } = renderHook(() => useMatchers());
108
+ const overview = await result.current.getMatchingOverview(mockHitWithMetadata);
109
+ expect(overview).toBe(mockOverview);
110
+ expect(mockGetHit).not.toHaveBeenCalled();
111
+ });
112
+ it('should fetch hit with metadata when overview is not present', async () => {
113
+ const { has } = await import('lodash-es');
114
+ has.mockReturnValue(false);
115
+ const hitWithFetchedMetadata = { ...mockHit, __overview: mockOverview };
116
+ mockGetHit.mockResolvedValue(hitWithFetchedMetadata);
117
+ const { result } = renderHook(() => useMatchers());
118
+ const overview = await result.current.getMatchingOverview(mockHit);
119
+ expect(overview).toBe(mockOverview);
120
+ expect(mockGetHit).toHaveBeenCalledWith('test-hit-id', true);
121
+ });
122
+ it('should handle getHit rejection gracefully', async () => {
123
+ const { has } = await import('lodash-es');
124
+ has.mockReturnValue(false);
125
+ mockGetHit.mockRejectedValue(new Error('Failed to fetch hit'));
126
+ const { result } = renderHook(() => useMatchers());
127
+ await expect(result.current.getMatchingOverview(mockHit)).rejects.toThrow('Failed to fetch hit');
128
+ expect(mockGetHit).toHaveBeenCalledWith('test-hit-id', true);
129
+ });
130
+ });
131
+ describe('getMatchingDossiers', () => {
132
+ it('should return null when hit is null', async () => {
133
+ const { result } = renderHook(() => useMatchers());
134
+ const dossiers = await result.current.getMatchingDossiers(null);
135
+ expect(dossiers).toBeNull();
136
+ });
137
+ it('should return null when hit is undefined', async () => {
138
+ const { result } = renderHook(() => useMatchers());
139
+ const dossiers = await result.current.getMatchingDossiers(undefined);
140
+ expect(dossiers).toBeNull();
141
+ });
142
+ it('should return dossiers from metadata when they exist', async () => {
143
+ const { has } = await import('lodash-es');
144
+ has.mockReturnValue(true);
145
+ const { result } = renderHook(() => useMatchers());
146
+ const dossiers = await result.current.getMatchingDossiers(mockHitWithMetadata);
147
+ expect(dossiers).toBe(mockDossiers);
148
+ expect(mockGetHit).not.toHaveBeenCalled();
149
+ });
150
+ it('should fetch hit with metadata when dossiers are not present', async () => {
151
+ const { has } = await import('lodash-es');
152
+ has.mockReturnValue(false);
153
+ const hitWithFetchedMetadata = { ...mockHit, __dossiers: mockDossiers };
154
+ mockGetHit.mockResolvedValue(hitWithFetchedMetadata);
155
+ const { result } = renderHook(() => useMatchers());
156
+ const dossiers = await result.current.getMatchingDossiers(mockHit);
157
+ expect(dossiers).toBe(mockDossiers);
158
+ expect(mockGetHit).toHaveBeenCalledWith('test-hit-id', true);
159
+ });
160
+ it('should handle getHit rejection gracefully', async () => {
161
+ const { has } = await import('lodash-es');
162
+ has.mockReturnValue(false);
163
+ mockGetHit.mockRejectedValue(new Error('Failed to fetch hit'));
164
+ const { result } = renderHook(() => useMatchers());
165
+ await expect(result.current.getMatchingDossiers(mockHit)).rejects.toThrow('Failed to fetch hit');
166
+ expect(mockGetHit).toHaveBeenCalledWith('test-hit-id', true);
167
+ });
168
+ });
169
+ describe('integration tests', () => {
170
+ it('should correctly handle has function calls for different metadata properties', async () => {
171
+ const { has } = await import('lodash-es');
172
+ // Mock has to return true for __template, false for others
173
+ has.mockImplementation((_obj, prop) => {
174
+ return prop === '__template';
175
+ });
176
+ const hitWithPartialMetadata = {
177
+ ...mockHit,
178
+ __template: mockTemplate
179
+ };
180
+ mockGetHit.mockResolvedValue({
181
+ ...mockHit,
182
+ __overview: mockOverview,
183
+ __dossiers: mockDossiers
184
+ });
185
+ const { result } = renderHook(() => useMatchers());
186
+ // Should return template from metadata
187
+ const template = await result.current.getMatchingTemplate(hitWithPartialMetadata);
188
+ expect(template).toBe(mockTemplate);
189
+ // Should fetch overview
190
+ const overview = await result.current.getMatchingOverview(hitWithPartialMetadata);
191
+ expect(overview).toBe(mockOverview);
192
+ // Should fetch dossiers
193
+ const dossiers = await result.current.getMatchingDossiers(hitWithPartialMetadata);
194
+ expect(dossiers).toBe(mockDossiers);
195
+ // Verify has was called with correct parameters
196
+ expect(has).toHaveBeenCalledWith(hitWithPartialMetadata, '__template');
197
+ expect(has).toHaveBeenCalledWith(hitWithPartialMetadata, '__overview');
198
+ expect(has).toHaveBeenCalledWith(hitWithPartialMetadata, '__dossiers');
199
+ });
200
+ it('should handle empty or undefined metadata gracefully', async () => {
201
+ const { has } = await import('lodash-es');
202
+ has.mockReturnValue(false);
203
+ mockGetHit.mockResolvedValue({
204
+ ...mockHit,
205
+ __template: undefined,
206
+ __overview: null,
207
+ __dossiers: []
208
+ });
209
+ const { result } = renderHook(() => useMatchers());
210
+ const template = await result.current.getMatchingTemplate(mockHit);
211
+ const overview = await result.current.getMatchingOverview(mockHit);
212
+ const dossiers = await result.current.getMatchingDossiers(mockHit);
213
+ expect(template).toBeUndefined();
214
+ expect(overview).toBeNull();
215
+ expect(dossiers).toEqual([]);
216
+ expect(mockGetHit).toHaveBeenCalledTimes(3);
217
+ });
218
+ it('should maintain referential equality of returned functions', () => {
219
+ const { result, rerender } = renderHook(() => useMatchers());
220
+ const firstRender = {
221
+ getMatchingTemplate: result.current.getMatchingTemplate,
222
+ getMatchingOverview: result.current.getMatchingOverview,
223
+ getMatchingDossiers: result.current.getMatchingDossiers
224
+ };
225
+ rerender();
226
+ const secondRender = {
227
+ getMatchingTemplate: result.current.getMatchingTemplate,
228
+ getMatchingOverview: result.current.getMatchingOverview,
229
+ getMatchingDossiers: result.current.getMatchingDossiers
230
+ };
231
+ // Functions should be the same reference due to useCallback
232
+ expect(firstRender.getMatchingTemplate).toBe(secondRender.getMatchingTemplate);
233
+ expect(firstRender.getMatchingOverview).toBe(secondRender.getMatchingOverview);
234
+ expect(firstRender.getMatchingDossiers).toBe(secondRender.getMatchingDossiers);
235
+ });
236
+ });
237
+ });
@@ -1,10 +1,11 @@
1
- import api from '@cccsaurora/howler-ui/api';
2
1
  import useMySitemap from '@cccsaurora/howler-ui/components/hooks/useMySitemap';
3
2
  import { capitalize } from 'lodash-es';
4
3
  import { useCallback, useContext, useEffect } from 'react';
5
4
  import { useTranslation } from 'react-i18next';
6
5
  import { useLocation, useParams, useSearchParams } from 'react-router-dom';
6
+ import { useContextSelector } from 'use-context-selector';
7
7
  import { AnalyticContext } from '../providers/AnalyticProvider';
8
+ import { HitContext } from '../providers/HitProvider';
8
9
  const useTitle = () => {
9
10
  const { t } = useTranslation();
10
11
  const location = useLocation();
@@ -12,6 +13,7 @@ const useTitle = () => {
12
13
  const searchParams = useSearchParams()[0];
13
14
  const sitemap = useMySitemap();
14
15
  const { getAnalyticFromId } = useContext(AnalyticContext);
16
+ const hits = useContextSelector(HitContext, ctx => ctx.hits);
15
17
  const setTitle = useCallback((title) => {
16
18
  document.querySelector('title').innerHTML = title;
17
19
  }, []);
@@ -32,8 +34,7 @@ const useTitle = () => {
32
34
  }
33
35
  }
34
36
  else if (searchType === 'hit' && params.id) {
35
- const result = await api.search.hit.post({ query: `howler.id:${params.id}`, rows: 1 });
36
- const hit = result.items[0];
37
+ const hit = hits[params.id];
37
38
  let newTitle = `${capitalize(hit.howler.escalation)} - ${hit.howler.analytic}`;
38
39
  if (hit.howler.detection) {
39
40
  newTitle += `: ${hit.howler.detection}`;
@@ -59,7 +60,7 @@ const useTitle = () => {
59
60
  setTitle(`Howler - ${t(matchingRoute.title)}`);
60
61
  }
61
62
  }
62
- }, [getAnalyticFromId, location.pathname, params.id, searchParams, setTitle, sitemap.routes, t]);
63
+ }, [getAnalyticFromId, location.pathname, params.id, searchParams, hits, setTitle, sitemap.routes, t]);
63
64
  useEffect(() => {
64
65
  runChecks();
65
66
  }, [runChecks]);
@@ -1,4 +1,5 @@
1
1
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
+ import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
2
3
  import type { FC, PropsWithChildren } from 'react';
3
4
  interface HitProviderType {
4
5
  hits: {
@@ -10,7 +11,7 @@ interface HitProviderType {
10
11
  clearSelectedHits: (except?: string) => void;
11
12
  loadHits: (hits: Hit[]) => void;
12
13
  updateHit: (newHit: Hit) => void;
13
- getHit: (id: string, force?: boolean) => Promise<Hit>;
14
+ getHit: (id: string, force?: boolean) => Promise<WithMetadata<Hit>>;
14
15
  }
15
16
  export declare const HitContext: import("use-context-selector").Context<HitProviderType>;
16
17
  /**
@@ -32,7 +32,13 @@ const HitProvider = ({ children }) => {
32
32
  // eslint-disable-next-line no-console
33
33
  console.debug('Received websocket update for hit', data.hit.howler.id);
34
34
  hitRequests.current[data.hit.howler.id] = Promise.resolve(data.hit);
35
- setHits(_hits => ({ ..._hits, [data.hit.howler.id]: data.hit }));
35
+ setHits(_hits => ({
36
+ ..._hits,
37
+ [data.hit.howler.id]: {
38
+ ..._hits[data.hit.howler.id],
39
+ ...data.hit
40
+ }
41
+ }));
36
42
  }
37
43
  }, []);
38
44
  useEffect(() => {
@@ -45,7 +51,7 @@ const HitProvider = ({ children }) => {
45
51
  */
46
52
  const getHit = useCallback(async (id, force = false) => {
47
53
  if (!hitRequests.current[id] || force) {
48
- hitRequests.current[id] = dispatchApi(api.hit.get(id));
54
+ hitRequests.current[id] = dispatchApi(api.hit.get(id, ['template', 'dossiers', 'analytic', 'overview']));
49
55
  const newHit = await hitRequests.current[id];
50
56
  setHits(_hits => ({ ..._hits, [id]: newHit }));
51
57
  }
@@ -1,6 +1,7 @@
1
1
  import type { HowlerSearchResponse } from '@cccsaurora/howler-ui/api/search';
2
2
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
3
3
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
4
+ import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
4
5
  import { type Dispatch, type FC, type PropsWithChildren, type SetStateAction } from 'react';
5
6
  export interface QueryEntry {
6
7
  [query: string]: string;
@@ -10,7 +11,7 @@ interface HitSearchProviderType {
10
11
  displayType: 'list' | 'grid';
11
12
  searching: boolean;
12
13
  error: string | null;
13
- response: HowlerSearchResponse<Hit> | null;
14
+ response: HowlerSearchResponse<WithMetadata<Hit>> | null;
14
15
  viewId: string | null;
15
16
  bundleId: string | null;
16
17
  queryHistory: QueryEntry;
@@ -86,7 +86,8 @@ const HitSearchProvider = ({ children }) => {
86
86
  query: fullQuery,
87
87
  sort,
88
88
  filters,
89
- track_total_hits: trackTotalHits
89
+ track_total_hits: trackTotalHits,
90
+ metadata: ['template', 'overview', 'analytic']
90
91
  }), { showError: false, throwError: true });
91
92
  if (_response.total < offset) {
92
93
  setOffset(0);
@@ -121,7 +121,7 @@ const SocketProvider = ({ children }) => {
121
121
  setRetry(false);
122
122
  // Here we go!
123
123
  setStatus(Status.CONNECTING);
124
- const host = window.location.host;
124
+ const host = window.location.host.includes('localhost') ? 'localhost:5000' : window.location.host;
125
125
  const protocol = window.location.protocol.startsWith('http:') ? 'ws' : 'wss';
126
126
  const ws = new WebSocket(`${protocol}://${host}/socket/v1/connect`);
127
127
  // Add our listeners to the websocket
@@ -11,6 +11,7 @@ import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
11
11
  import { useMemo } from 'react';
12
12
  import { usePluginStore } from 'react-pluggable';
13
13
  import JSONViewer from '../json/JSONViewer';
14
+ const FETCH_RESULTS = {};
14
15
  export const useHelpers = () => {
15
16
  const pluginStore = usePluginStore();
16
17
  const allHelpers = useMemo(() => [
@@ -55,8 +56,10 @@ export const useHelpers = () => {
55
56
  documentation: 'Fetches the url provided and returns the given (flattened) key from the returned JSON object. Note that the result must be JSON!',
56
57
  callback: async (url, key) => {
57
58
  try {
58
- const response = await fetch(url);
59
- const json = await response.json();
59
+ if (!FETCH_RESULTS[url]) {
60
+ FETCH_RESULTS[url] = fetch(url).then(res => res.json());
61
+ }
62
+ const json = await FETCH_RESULTS[url];
60
63
  return flatten(json)[key];
61
64
  }
62
65
  catch (e) {
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { CardContent, Skeleton } from '@mui/material';
3
3
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
4
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
5
4
  import { memo, useEffect } from 'react';
6
5
  import { useContextSelector } from 'use-context-selector';
7
6
  import HowlerCard from '../display/HowlerCard';
@@ -10,12 +9,8 @@ import HitLabels from './HitLabels';
10
9
  import { HitLayout } from './HitLayout';
11
10
  import HitOutline from './HitOutline';
12
11
  const HitCard = ({ id, layout, readOnly = true }) => {
13
- const refresh = useContextSelector(TemplateContext, ctx => ctx.refresh);
14
12
  const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
15
13
  const hit = useContextSelector(HitContext, ctx => ctx.hits[id]);
16
- useEffect(() => {
17
- refresh();
18
- }, [refresh]);
19
14
  useEffect(() => {
20
15
  if (!hit) {
21
16
  getHit(id);
@@ -1,9 +1,9 @@
1
1
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
+ import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
2
3
  import { HitLayout } from './HitLayout';
3
4
  export declare const DEFAULT_FIELDS: string[];
4
5
  declare const _default: import("react").NamedExoticComponent<{
5
- hit: Hit;
6
+ hit: WithMetadata<Hit>;
6
7
  layout: HitLayout;
7
- type?: "global" | "personal";
8
8
  }>;
9
9
  export default _default;
@@ -1,30 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Divider, Skeleton, Typography } from '@mui/material';
3
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
4
- import { createElement, memo, useMemo } from 'react';
2
+ import { Box, Divider, Typography } from '@mui/material';
3
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
4
+ import { createElement, memo, useEffect, useMemo, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
- import { useContextSelector } from 'use-context-selector';
7
6
  import { HitLayout } from './HitLayout';
8
7
  import DefaultOutline from './outlines/DefaultOutline';
9
8
  export const DEFAULT_FIELDS = ['howler.hash'];
10
- const HitOutline = ({ hit, layout, type }) => {
9
+ const HitOutline = ({ hit, layout }) => {
11
10
  const { t } = useTranslation();
12
- const loaded = useContextSelector(TemplateContext, ctx => ctx.loaded);
13
- const getMatchingTemplate = useContextSelector(TemplateContext, ctx => ctx.getMatchingTemplate);
14
- const template = useMemo(() => getMatchingTemplate(hit), [getMatchingTemplate, hit]);
11
+ const { getMatchingTemplate } = useMatchers();
12
+ const [template, setTemplate] = useState(null);
13
+ useEffect(() => {
14
+ getMatchingTemplate(hit).then(setTemplate);
15
+ }, [getMatchingTemplate, hit]);
15
16
  const outline = useMemo(() => {
16
- if (template && template.type === type) {
17
- return createElement(DefaultOutline, {
18
- hit,
19
- layout,
20
- template,
21
- fields: template.keys
22
- });
23
- }
24
- else if (!loaded) {
25
- return _jsx(Skeleton, { variant: "rounded", height: "50px" });
26
- }
27
- else if (template) {
17
+ if (template) {
28
18
  return createElement(DefaultOutline, {
29
19
  hit,
30
20
  layout,
@@ -40,7 +30,7 @@ const HitOutline = ({ hit, layout, type }) => {
40
30
  fields: DEFAULT_FIELDS
41
31
  });
42
32
  }
43
- }, [hit, layout, loaded, template, type]);
33
+ }, [hit, layout, template]);
44
34
  return (_jsxs(Box, { sx: { py: 1, width: '100%', pr: 2 }, children: [layout === HitLayout.COMFY && (_jsx(Typography, { variant: "body1", fontWeight: "bold", sx: { mb: 1 }, children: t('hit.details.title') })), layout !== HitLayout.DENSE && _jsx(Divider, { orientation: "horizontal", sx: { mb: 1 } }), outline] }));
45
35
  };
46
36
  export default memo(HitOutline);
@@ -1,14 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { InsertLink } from '@mui/icons-material';
3
3
  import { Box, IconButton, Skeleton } from '@mui/material';
4
- import { OverviewContext } from '@cccsaurora/howler-ui/components/app/providers/OverviewProvider';
4
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
5
5
  import ErrorBoundary from '@cccsaurora/howler-ui/components/routes/ErrorBoundary';
6
- import { memo, useContext, useMemo } from 'react';
6
+ import { memo, useEffect, useMemo, useState } from 'react';
7
7
  import { Link } from 'react-router-dom';
8
8
  import HandlebarsMarkdown from '../display/HandlebarsMarkdown';
9
9
  const HitOverview = ({ content, hit }) => {
10
- const { getMatchingOverview } = useContext(OverviewContext);
11
- const matchingOverview = useMemo(() => (hit ? getMatchingOverview(hit) : null), [getMatchingOverview, hit]);
10
+ const { getMatchingOverview } = useMatchers();
11
+ const [matchingOverview, setMatchingOverview] = useState(null);
12
+ useEffect(() => {
13
+ getMatchingOverview(hit).then(setMatchingOverview);
14
+ }, [getMatchingOverview, hit]);
12
15
  const link = useMemo(() => matchingOverview
13
16
  ? `/overviews/view?analytic=${encodeURIComponent(matchingOverview.analytic)}${matchingOverview.detection && '&detection=' + encodeURIComponent(matchingOverview.detection)}`
14
17
  : hit
@@ -1,8 +1,9 @@
1
1
  import type { HowlerSearchResponse } from '@cccsaurora/howler-ui/api/search';
2
2
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
3
+ import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
3
4
  declare const _default: import("react").NamedExoticComponent<{
4
5
  query: string;
5
- response?: HowlerSearchResponse<Hit>;
6
+ response?: HowlerSearchResponse<WithMetadata<Hit>>;
6
7
  execute?: boolean;
7
8
  onStart?: () => void;
8
9
  onComplete?: () => void;