@cccsaurora/howler-ui 2.17.0-dev.564 → 2.17.0-dev.600
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/index.d.ts +2 -0
- package/api/index.js +4 -2
- package/api/search/case.d.ts +4 -0
- package/api/search/case.js +8 -0
- package/api/search/index.d.ts +2 -1
- package/api/search/index.js +2 -1
- package/api/v2/case/index.d.ts +6 -0
- package/api/v2/case/index.js +18 -0
- package/api/v2/index.d.ts +4 -0
- package/api/v2/index.js +6 -0
- package/api/v2/search/facet.d.ts +3 -0
- package/api/v2/search/facet.js +12 -0
- package/api/v2/search/index.d.ts +5 -0
- package/api/v2/search/index.js +24 -0
- package/commons/components/leftnav/LeftNavDrawer.js +1 -1
- package/components/app/App.js +14 -0
- package/components/app/providers/FavouritesProvider.js +2 -2
- package/components/app/providers/HitSearchProvider.d.ts +0 -1
- package/components/app/providers/HitSearchProvider.js +6 -11
- package/components/app/providers/HitSearchProvider.test.js +11 -32
- package/components/app/providers/ParameterProvider.d.ts +9 -2
- package/components/app/providers/ParameterProvider.js +165 -240
- package/components/app/providers/ParameterProvider.test.js +307 -14
- package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
- package/components/elements/ObjectDetails.d.ts +6 -0
- package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
- package/components/elements/PluginTypography.d.ts +2 -1
- package/components/elements/PluginTypography.js +3 -2
- package/components/elements/UserList.d.ts +5 -2
- package/components/elements/UserList.js +14 -5
- package/components/elements/addons/search/phrase/Phrase.js +1 -1
- package/components/elements/case/CaseCard.d.ts +8 -0
- package/components/elements/case/CaseCard.js +39 -0
- package/components/elements/case/CasePreview.d.ts +6 -0
- package/components/elements/case/CasePreview.js +17 -0
- package/components/elements/case/StatusIcon.d.ts +5 -0
- package/components/elements/case/StatusIcon.js +13 -0
- package/components/elements/display/ChipPopper.d.ts +1 -0
- package/components/elements/display/ChipPopper.js +2 -2
- package/components/elements/display/HowlerCard.js +1 -1
- package/components/elements/display/Modal.js +1 -0
- package/components/elements/hit/HitBanner.js +28 -48
- package/components/elements/hit/HitCard.js +1 -1
- package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
- package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
- package/components/elements/hit/HitRelated.d.ts +1 -1
- package/components/elements/hit/HitRelated.js +30 -3
- package/components/elements/hit/elements/AnalyticLink.d.ts +8 -0
- package/components/elements/hit/elements/AnalyticLink.js +22 -0
- package/components/elements/hit/outlines/DefaultOutline.js +1 -1
- package/components/elements/hit/related/RelatedRecords.js +63 -0
- package/components/elements/observable/ObservableCard.d.ts +5 -0
- package/components/elements/observable/ObservableCard.js +7 -0
- package/components/elements/observable/ObservablePreview.d.ts +6 -0
- package/components/elements/observable/ObservablePreview.js +12 -0
- package/components/elements/view/ViewTitle.js +1 -1
- package/components/hooks/useHitActions.d.ts +1 -1
- package/components/hooks/useHitActions.js +2 -2
- package/components/hooks/useHitSelection.js +3 -24
- package/components/hooks/useMyPreferences.js +10 -1
- package/components/hooks/useMySearch.js +2 -2
- package/components/hooks/useMySitemap.js +4 -1
- package/components/hooks/useMyTheme.js +9 -2
- package/components/hooks/useRelatedRecords.d.ts +13 -0
- package/components/hooks/useRelatedRecords.js +32 -0
- package/components/routes/action/view/ActionSearch.js +1 -1
- package/components/routes/advanced/QueryBuilder.js +1 -1
- package/components/routes/analytics/AnalyticDetails.js +2 -2
- package/components/routes/analytics/AnalyticSearch.js +1 -1
- package/components/routes/cases/CaseViewer.d.ts +2 -0
- package/components/routes/cases/CaseViewer.js +24 -0
- package/components/routes/cases/Cases.d.ts +2 -0
- package/components/routes/cases/Cases.js +101 -0
- package/components/routes/cases/constants.d.ts +5 -0
- package/components/routes/cases/constants.js +5 -0
- package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
- package/components/routes/cases/detail/AlertPanel.js +32 -0
- package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
- package/components/routes/cases/detail/CaseDashboard.js +49 -0
- package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
- package/components/routes/cases/detail/CaseDetails.js +61 -0
- package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
- package/components/routes/cases/detail/CaseOverview.js +43 -0
- package/components/routes/cases/detail/CaseSidebar.d.ts +6 -0
- package/components/routes/cases/detail/CaseSidebar.js +36 -0
- package/components/routes/cases/detail/CaseTask.d.ts +11 -0
- package/components/routes/cases/detail/CaseTask.js +57 -0
- package/components/routes/cases/detail/ItemPage.d.ts +6 -0
- package/components/routes/cases/detail/ItemPage.js +93 -0
- package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
- package/components/routes/cases/detail/RelatedCasePanel.js +31 -0
- package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
- package/components/routes/cases/detail/TaskPanel.js +52 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +12 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.js +19 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.js +27 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +12 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.js +179 -0
- package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
- package/components/routes/cases/hooks/useCase.d.ts +13 -0
- package/components/routes/cases/hooks/useCase.js +38 -0
- package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
- package/components/routes/cases/modals/ResolveModal.js +59 -0
- package/components/routes/help/ApiDocumentation.js +1 -1
- package/components/routes/help/HitDocumentation.js +1 -3
- package/components/routes/hits/search/HitContextMenu.js +3 -2
- package/components/routes/hits/search/InformationPane.d.ts +1 -0
- package/components/routes/hits/search/InformationPane.js +6 -28
- package/components/routes/hits/search/QuerySettings.js +2 -1
- package/components/routes/hits/search/QuerySettings.test.js +14 -9
- package/components/routes/hits/search/SearchPane.js +7 -32
- package/components/routes/hits/search/ViewLink.js +1 -1
- package/components/routes/hits/search/grid/EnhancedCell.js +1 -1
- package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
- package/components/routes/hits/search/shared/IndexPicker.js +20 -0
- package/components/routes/hits/view/HitViewer.js +3 -4
- package/components/routes/home/ViewCard.js +1 -1
- package/components/routes/observables/ObservableViewer.d.ts +7 -0
- package/components/routes/observables/ObservableViewer.js +27 -0
- package/components/routes/overviews/OverviewViewer.js +2 -2
- package/locales/en/translation.json +437 -398
- package/locales/fr/translation.json +442 -408
- package/models/WithMetadata.d.ts +2 -1
- package/models/entities/generated/AttachmentsFile.d.ts +12 -0
- package/models/entities/generated/Case.d.ts +28 -0
- package/models/entities/generated/DestinationOriginal.d.ts +19 -0
- package/models/entities/generated/EmailAttachment.d.ts +8 -0
- package/models/entities/generated/EmailParent.d.ts +19 -0
- package/models/entities/generated/Enrichments.d.ts +7 -0
- package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
- package/models/entities/generated/Howler.d.ts +0 -4
- package/models/entities/generated/HttpResponse.d.ts +11 -0
- package/models/entities/generated/Item.d.ts +9 -0
- package/models/entities/generated/Observable.d.ts +84 -0
- package/models/entities/generated/ObservableCloud.d.ts +20 -0
- package/models/entities/generated/ObservableDestination.d.ts +23 -0
- package/models/entities/generated/ObservableEmail.d.ts +30 -0
- package/models/entities/generated/ObservableFile.d.ts +36 -0
- package/models/entities/generated/ObservableHowler.d.ts +44 -0
- package/models/entities/generated/ObservableHttp.d.ts +11 -0
- package/models/entities/generated/ObservableObserver.d.ts +21 -0
- package/models/entities/generated/ObservableOrganization.d.ts +7 -0
- package/models/entities/generated/ObservableProcess.d.ts +34 -0
- package/models/entities/generated/ObservableSource.d.ts +23 -0
- package/models/entities/generated/ObservableThreat.d.ts +21 -0
- package/models/entities/generated/ObservableTls.d.ts +12 -0
- package/models/entities/generated/ObserverIngress.d.ts +9 -0
- package/models/entities/generated/Rule.d.ts +2 -10
- package/models/entities/generated/Task.d.ts +10 -0
- package/models/entities/generated/Threat.d.ts +2 -2
- package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
- package/package.json +16 -1
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +2 -1
- package/utils/constants.d.ts +3 -3
- package/utils/typeUtils.d.ts +7 -0
- package/utils/typeUtils.js +18 -0
- package/components/elements/display/icons/BundleButton.d.ts +0 -6
- package/components/elements/display/icons/BundleButton.js +0 -32
- package/components/routes/help/BundleDocumentation.d.ts +0 -3
- package/components/routes/help/BundleDocumentation.js +0 -12
- package/components/routes/help/markdown/en/bundles.md.js +0 -1
- package/components/routes/help/markdown/fr/bundles.md.js +0 -1
- package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
- package/components/routes/hits/search/BundleParentMenu.js +0 -32
- /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
- /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { isEmpty, isEqual, isUndefined, omitBy, uniq } from 'lodash-es';
|
|
2
|
+
import { identity, isEmpty, isEqual, isUndefined, omitBy, uniq } from 'lodash-es';
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
|
|
5
5
|
import { createContext, useContextSelector } from 'use-context-selector';
|
|
@@ -9,12 +9,10 @@ export const ParameterContext = createContext(null);
|
|
|
9
9
|
const DEFAULT_VALUES = {
|
|
10
10
|
query: DEFAULT_QUERY,
|
|
11
11
|
sort: 'event.created desc',
|
|
12
|
-
span: 'date.range.1.month'
|
|
12
|
+
span: 'date.range.1.month',
|
|
13
|
+
indexes: ['hit']
|
|
13
14
|
};
|
|
14
|
-
/**
|
|
15
|
-
* Mapping of URL parameter keys to internal state keys
|
|
16
|
-
* Note: 'filters' is handled separately due to multi-value support
|
|
17
|
-
*/
|
|
15
|
+
/** Scalar URL params that map 1:1 to a state key */
|
|
18
16
|
const PARAM_MAPPINGS = [
|
|
19
17
|
['query', 'query'],
|
|
20
18
|
['sort', 'sort'],
|
|
@@ -22,15 +20,21 @@ const PARAM_MAPPINGS = [
|
|
|
22
20
|
['start_date', 'startDate'],
|
|
23
21
|
['end_date', 'endDate']
|
|
24
22
|
];
|
|
23
|
+
/** Multi-value URL params that map to array state keys */
|
|
24
|
+
const ARRAY_PARAMS = [
|
|
25
|
+
{ urlKey: 'filter', stateKey: 'filters' },
|
|
26
|
+
{ urlKey: 'view', stateKey: 'views' },
|
|
27
|
+
{ urlKey: 'index', stateKey: 'indexes', default: DEFAULT_VALUES.indexes }
|
|
28
|
+
];
|
|
29
|
+
const ARRAY_URL_KEYS = new Set(ARRAY_PARAMS.map(p => p.urlKey));
|
|
25
30
|
const WRITE_THROTTLER = new Throttler(100);
|
|
26
31
|
/**
|
|
27
32
|
* Helper function to convert a number/string representation of a number into a valid offset.
|
|
28
33
|
* @returns
|
|
29
34
|
*/
|
|
30
35
|
const parseOffset = (_offset) => {
|
|
31
|
-
if (typeof _offset === 'number')
|
|
36
|
+
if (typeof _offset === 'number')
|
|
32
37
|
return _offset;
|
|
33
|
-
}
|
|
34
38
|
const candidate = parseInt(_offset);
|
|
35
39
|
return isNaN(candidate) ? 0 : candidate;
|
|
36
40
|
};
|
|
@@ -38,261 +42,131 @@ const parseOffset = (_offset) => {
|
|
|
38
42
|
* Helper function to determine the selected value based on URL params and route context.
|
|
39
43
|
*/
|
|
40
44
|
const getSelectedValue = (params, pathname, bundleId) => {
|
|
41
|
-
if (params.has('selected'))
|
|
45
|
+
if (params.has('selected'))
|
|
42
46
|
return params.get('selected');
|
|
43
|
-
|
|
44
|
-
if (pathname.startsWith('/bundles') && bundleId) {
|
|
47
|
+
if (pathname.startsWith('/bundles') && bundleId)
|
|
45
48
|
return bundleId;
|
|
46
|
-
}
|
|
47
49
|
return null;
|
|
48
50
|
};
|
|
49
51
|
/**
|
|
50
|
-
*
|
|
52
|
+
* Returns stable add / remove / setAt / setAll / clear handlers for a list field in
|
|
53
|
+
* SearchValues. All returned functions are memoized; since _setValues (from useState)
|
|
54
|
+
* and key are both stable for the lifetime of the component, the deps array is empty.
|
|
51
55
|
*/
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
pendingChanges.current.selected = value;
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
pendingChanges.current[key] = value ?? DEFAULT_VALUES[key] ?? null;
|
|
89
|
-
}
|
|
90
|
-
if (key === 'span' && typeof value === 'string' && !value.endsWith('custom')) {
|
|
91
|
-
pendingChanges.current.startDate = null;
|
|
92
|
-
pendingChanges.current.endDate = null;
|
|
93
|
-
}
|
|
94
|
-
WRITE_THROTTLER.debounce(() => {
|
|
95
|
-
_setValues(_current => ({ ..._current, ...pendingChanges.current }));
|
|
96
|
-
pendingChanges.current = {};
|
|
97
|
-
});
|
|
98
|
-
}, [values]);
|
|
99
|
-
const setOffset = useCallback(_offset => _setValues(_current => ({ ..._current, offset: parseOffset(_offset) })), []);
|
|
100
|
-
const setCustomSpan = useCallback((startDate, endDate) => {
|
|
101
|
-
_setValues(_values => ({
|
|
102
|
-
..._values,
|
|
103
|
-
startDate,
|
|
104
|
-
endDate
|
|
105
|
-
}));
|
|
106
|
-
}, []);
|
|
107
|
-
/**
|
|
108
|
-
* Filter manipulation
|
|
109
|
-
*/
|
|
110
|
-
const addFilter = useCallback(filter => {
|
|
111
|
-
_setValues(_current => ({
|
|
112
|
-
..._current,
|
|
113
|
-
filters: uniq([..._current.filters, filter])
|
|
114
|
-
}));
|
|
115
|
-
}, []);
|
|
116
|
-
const removeFilter = useCallback(filter => {
|
|
117
|
-
_setValues(_current => {
|
|
118
|
-
const index = _current.filters.indexOf(filter);
|
|
119
|
-
if (index === -1) {
|
|
120
|
-
return _current;
|
|
121
|
-
}
|
|
122
|
-
return {
|
|
123
|
-
..._current,
|
|
124
|
-
filters: _current.filters.filter((_, i) => i !== index)
|
|
125
|
-
};
|
|
126
|
-
});
|
|
127
|
-
}, []);
|
|
128
|
-
const setFilter = useCallback((index, filter) => {
|
|
129
|
-
_setValues(_current => {
|
|
130
|
-
// Validate index
|
|
131
|
-
if (index < 0 || index >= _current.filters.length) {
|
|
132
|
-
return _current;
|
|
133
|
-
}
|
|
134
|
-
const newFilters = [..._current.filters];
|
|
135
|
-
newFilters[index] = filter;
|
|
136
|
-
return {
|
|
137
|
-
..._current,
|
|
138
|
-
filters: newFilters
|
|
139
|
-
};
|
|
140
|
-
});
|
|
141
|
-
}, []);
|
|
142
|
-
const clearFilters = useCallback(() => {
|
|
143
|
-
_setValues(_current => ({
|
|
144
|
-
..._current,
|
|
145
|
-
filters: []
|
|
146
|
-
}));
|
|
147
|
-
}, []);
|
|
148
|
-
/**
|
|
149
|
-
* View manipulation
|
|
150
|
-
*/
|
|
151
|
-
const addView = useCallback(view => {
|
|
152
|
-
_setValues(_current => ({
|
|
153
|
-
..._current,
|
|
154
|
-
views: uniq([..._current.views, view])
|
|
155
|
-
}));
|
|
156
|
-
}, []);
|
|
157
|
-
const removeView = useCallback(view => {
|
|
158
|
-
_setValues(_current => {
|
|
159
|
-
const index = _current.views.indexOf(view);
|
|
160
|
-
if (index === -1) {
|
|
161
|
-
return _current;
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
..._current,
|
|
165
|
-
views: _current.views.filter((_, i) => i !== index)
|
|
166
|
-
};
|
|
167
|
-
});
|
|
168
|
-
}, []);
|
|
169
|
-
const setView = useCallback((index, view) => {
|
|
170
|
-
_setValues(_current => {
|
|
171
|
-
// Validate index
|
|
172
|
-
if (index < 0 || index >= _current.views.length) {
|
|
173
|
-
return _current;
|
|
174
|
-
}
|
|
175
|
-
const newViews = [..._current.views];
|
|
176
|
-
newViews[index] = view;
|
|
177
|
-
return {
|
|
178
|
-
..._current,
|
|
179
|
-
views: newViews
|
|
180
|
-
};
|
|
181
|
-
});
|
|
182
|
-
}, []);
|
|
183
|
-
const clearViews = useCallback(() => {
|
|
184
|
-
_setValues(_current => ({
|
|
185
|
-
..._current,
|
|
186
|
-
views: []
|
|
187
|
-
}));
|
|
188
|
-
}, []);
|
|
189
|
-
/**
|
|
190
|
-
* Get URL parameter changes needed to sync internal state to the address bar.
|
|
191
|
-
* Returns null values for params that should be removed from URL.
|
|
192
|
-
*/
|
|
56
|
+
const useListHandlers = (key, _setValues) => {
|
|
57
|
+
const add = useCallback((item) => _setValues(c => ({ ...c, [key]: uniq([...c[key], item]) })),
|
|
58
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
59
|
+
[]);
|
|
60
|
+
const remove = useCallback((item) => _setValues(c => {
|
|
61
|
+
const arr = c[key];
|
|
62
|
+
const i = arr.indexOf(item);
|
|
63
|
+
return i === -1 ? c : { ...c, [key]: arr.filter((_, idx) => idx !== i) };
|
|
64
|
+
}),
|
|
65
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
+
[]);
|
|
67
|
+
const setAt = useCallback((pos, item) => _setValues(c => {
|
|
68
|
+
const arr = c[key];
|
|
69
|
+
if (pos < 0 || pos >= arr.length)
|
|
70
|
+
return c;
|
|
71
|
+
const next = [...arr];
|
|
72
|
+
next[pos] = item;
|
|
73
|
+
return { ...c, [key]: next };
|
|
74
|
+
}),
|
|
75
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
|
+
[]);
|
|
77
|
+
const setAll = useCallback((items) => _setValues(c => ({ ...c, [key]: uniq(items) })),
|
|
78
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
|
+
[]);
|
|
80
|
+
const reset = useCallback((defaultValue = []) => _setValues(c => ({ ...c, [key]: defaultValue })),
|
|
81
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
+
[]);
|
|
83
|
+
return { add, remove, setAt, setAll, reset };
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Synchronizes SearchValues state with the URL search string, and vice-versa.
|
|
87
|
+
*/
|
|
88
|
+
const useUrlSync = (values, _setValues, params, setParams, pathname, search, routeId) => {
|
|
193
89
|
const getUrlFromState = useCallback(() => {
|
|
194
90
|
const changes = {};
|
|
91
|
+
// Scalar params: write if changed from URL, remove if back to default
|
|
195
92
|
PARAM_MAPPINGS.forEach(([urlKey, stateKey]) => {
|
|
196
93
|
const stateValue = values[stateKey];
|
|
197
94
|
const urlValue = params.get(urlKey);
|
|
198
|
-
if (stateValue
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
95
|
+
if (stateValue === urlValue)
|
|
96
|
+
return;
|
|
97
|
+
if (params.has(urlKey) && stateValue === DEFAULT_VALUES[stateKey]) {
|
|
98
|
+
changes[urlKey] = null; // remove
|
|
99
|
+
}
|
|
100
|
+
else if (stateValue !== DEFAULT_VALUES[stateKey]) {
|
|
101
|
+
changes[urlKey] = stateValue; // write
|
|
206
102
|
}
|
|
207
103
|
});
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
104
|
+
// Array params: skip when state equals default and URL is already empty
|
|
105
|
+
ARRAY_PARAMS.forEach(({ urlKey, stateKey, default: def }) => {
|
|
106
|
+
const stateArr = values[stateKey];
|
|
107
|
+
const urlArr = params.getAll(urlKey);
|
|
108
|
+
if (isEqual(stateArr, urlArr))
|
|
109
|
+
return;
|
|
110
|
+
const isDefault = def ? isEqual(stateArr, def) : stateArr.length === 0;
|
|
111
|
+
if (!isDefault) {
|
|
112
|
+
changes[urlKey] = stateArr.length === 0 ? null : stateArr;
|
|
113
|
+
}
|
|
114
|
+
else if (urlArr.length > 0) {
|
|
115
|
+
changes[urlKey] = null; // state is default but URL isn't — remove
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// selected
|
|
119
|
+
if (pathname.startsWith('/bundles') && (!params.has('selected') || values.selected === params.get('selected'))) {
|
|
223
120
|
changes.selected = null;
|
|
224
121
|
}
|
|
225
122
|
else if (values.selected !== params.get('selected')) {
|
|
226
123
|
changes.selected = values.selected;
|
|
227
124
|
}
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
if (urlOffset !== values.offset) {
|
|
125
|
+
// offset: remove when 0
|
|
126
|
+
if (parseOffset(params.get('offset')) !== values.offset) {
|
|
231
127
|
changes.offset = values.offset || null;
|
|
232
128
|
}
|
|
233
|
-
//
|
|
234
|
-
return omitBy(changes, (val, key) =>
|
|
235
|
-
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
return val == params.get(key);
|
|
239
|
-
});
|
|
240
|
-
}, [values, params, location.pathname]);
|
|
241
|
-
/**
|
|
242
|
-
* Get state changes needed to sync URL parameters to internal state.
|
|
243
|
-
*/
|
|
129
|
+
// Drop scalar entries that already match the URL
|
|
130
|
+
return omitBy(changes, (val, key) => !ARRAY_URL_KEYS.has(key) && val == params.get(key));
|
|
131
|
+
}, [values, params, pathname]);
|
|
244
132
|
const getStateFromUrl = useCallback(() => {
|
|
245
133
|
const changes = {};
|
|
134
|
+
// Scalar params: fall back to default when absent from URL
|
|
246
135
|
PARAM_MAPPINGS.forEach(([urlKey, stateKey]) => {
|
|
247
136
|
const urlValue = params.has(urlKey) ? params.get(urlKey) : (DEFAULT_VALUES[stateKey] ?? undefined);
|
|
248
137
|
if (urlValue !== values[stateKey]) {
|
|
249
138
|
changes[stateKey] = urlValue;
|
|
250
139
|
}
|
|
251
140
|
});
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const selectedValue = getSelectedValue(params, location.pathname, routeParams.id);
|
|
264
|
-
if (selectedValue !== values.selected) {
|
|
141
|
+
// Array params: fall back to their declared default when absent from URL
|
|
142
|
+
ARRAY_PARAMS.forEach(({ urlKey, stateKey, default: def }) => {
|
|
143
|
+
const raw = params.getAll(urlKey);
|
|
144
|
+
const resolved = (isEmpty(raw) && def ? def : uniq(raw));
|
|
145
|
+
if (!isEqual(resolved, values[stateKey])) {
|
|
146
|
+
changes[stateKey] = resolved;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
// selected
|
|
150
|
+
const selectedValue = getSelectedValue(params, pathname, routeId);
|
|
151
|
+
if (selectedValue !== values.selected)
|
|
265
152
|
changes.selected = selectedValue;
|
|
266
|
-
|
|
267
|
-
// Handle offset
|
|
153
|
+
// offset
|
|
268
154
|
const urlOffset = parseOffset(params.get('offset'));
|
|
269
|
-
if (urlOffset !== values.offset)
|
|
155
|
+
if (urlOffset !== values.offset)
|
|
270
156
|
changes.offset = urlOffset;
|
|
271
|
-
}
|
|
272
|
-
// Filter out undefined values and values that already match state
|
|
273
157
|
return omitBy(omitBy(changes, isUndefined), (val, key) => val == values[key]);
|
|
274
|
-
}, [values, params,
|
|
275
|
-
|
|
276
|
-
* Effect to synchronize the context's state with the address bar
|
|
277
|
-
*/
|
|
158
|
+
}, [values, params, pathname, routeId]);
|
|
159
|
+
// State → URL
|
|
278
160
|
useEffect(() => {
|
|
279
161
|
const changes = getUrlFromState();
|
|
280
|
-
if (isEmpty(changes))
|
|
162
|
+
if (isEmpty(changes))
|
|
281
163
|
return;
|
|
282
|
-
}
|
|
283
164
|
setParams(_params => {
|
|
284
|
-
// Build fresh URLSearchParams from existing params
|
|
285
165
|
const newParams = new URLSearchParams(_params);
|
|
286
|
-
// Handle standard params
|
|
287
166
|
Object.entries(changes).forEach(([key, value]) => {
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
newParams.delete(multiFieldKey);
|
|
292
|
-
if (Array.isArray(value)) {
|
|
293
|
-
value.forEach(val => newParams.append(multiFieldKey, val));
|
|
294
|
-
}
|
|
295
|
-
// null/undefined means remove (already deleted above)
|
|
167
|
+
if (Array.isArray(value)) {
|
|
168
|
+
newParams.delete(key);
|
|
169
|
+
value.forEach(val => newParams.append(key, val));
|
|
296
170
|
}
|
|
297
171
|
else if (value === null || value === undefined) {
|
|
298
172
|
newParams.delete(key);
|
|
@@ -305,17 +179,63 @@ const ParameterProvider = ({ children }) => {
|
|
|
305
179
|
}, { replace: !changes.query && !Object.keys(changes).includes('offset') });
|
|
306
180
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
307
181
|
}, [values]);
|
|
182
|
+
// URL → State
|
|
308
183
|
useEffect(() => {
|
|
309
184
|
const changes = getStateFromUrl();
|
|
310
|
-
if (isEmpty(changes))
|
|
185
|
+
if (isEmpty(changes))
|
|
311
186
|
return;
|
|
312
|
-
}
|
|
313
|
-
_setValues(_current => ({
|
|
314
|
-
..._current,
|
|
315
|
-
...changes
|
|
316
|
-
}));
|
|
187
|
+
_setValues(c => ({ ...c, ...changes }));
|
|
317
188
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
318
|
-
}, [
|
|
189
|
+
}, [search, pathname, routeId]);
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Context responsible for tracking updates to query operations in hit and view search.
|
|
193
|
+
*/
|
|
194
|
+
const ParameterProvider = ({ children }) => {
|
|
195
|
+
const location = useLocation();
|
|
196
|
+
const routeParams = useParams();
|
|
197
|
+
const [params, setParams] = useSearchParams();
|
|
198
|
+
const pendingChanges = useRef({});
|
|
199
|
+
const [values, _setValues] = useState({
|
|
200
|
+
selected: getSelectedValue(params, location.pathname, routeParams.id),
|
|
201
|
+
query: params.get('query') ?? DEFAULT_VALUES.query,
|
|
202
|
+
sort: params.get('sort') ?? DEFAULT_VALUES.sort,
|
|
203
|
+
span: params.get('span') ?? DEFAULT_VALUES.span,
|
|
204
|
+
indexes: params.has('index')
|
|
205
|
+
? uniq(params.getAll('index')).filter(identity)
|
|
206
|
+
: DEFAULT_VALUES.indexes,
|
|
207
|
+
filters: params.getAll('filter'),
|
|
208
|
+
views: params.getAll('view'),
|
|
209
|
+
startDate: params.get('start_date'),
|
|
210
|
+
endDate: params.get('end_date'),
|
|
211
|
+
offset: parseOffset(params.get('offset')),
|
|
212
|
+
trackTotalHits: (params.get('track_total_hits') ?? 'false') !== 'false'
|
|
213
|
+
});
|
|
214
|
+
// TODO: SELECTING A BUNDLE STILL CAUSES A FREAKOUT
|
|
215
|
+
useUrlSync(values, _setValues, params, setParams, location.pathname, location.search, routeParams.id);
|
|
216
|
+
const set = useCallback((key) => (value) => {
|
|
217
|
+
if (value === values[key])
|
|
218
|
+
return;
|
|
219
|
+
if (key === 'selected') {
|
|
220
|
+
pendingChanges.current.selected = value;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
pendingChanges.current[key] = value ?? DEFAULT_VALUES[key] ?? null;
|
|
224
|
+
}
|
|
225
|
+
if (key === 'span' && typeof value === 'string' && !value.endsWith('custom')) {
|
|
226
|
+
pendingChanges.current.startDate = null;
|
|
227
|
+
pendingChanges.current.endDate = null;
|
|
228
|
+
}
|
|
229
|
+
WRITE_THROTTLER.debounce(() => {
|
|
230
|
+
_setValues(c => ({ ...c, ...pendingChanges.current }));
|
|
231
|
+
pendingChanges.current = {};
|
|
232
|
+
});
|
|
233
|
+
}, [values]);
|
|
234
|
+
const setOffset = useCallback((_offset) => _setValues(c => ({ ...c, offset: parseOffset(_offset) })), []);
|
|
235
|
+
const setCustomSpan = useCallback((startDate, endDate) => _setValues(c => ({ ...c, startDate, endDate })), []);
|
|
236
|
+
const filters = useListHandlers('filters', _setValues);
|
|
237
|
+
const indexes = useListHandlers('indexes', _setValues);
|
|
238
|
+
const views = useListHandlers('views', _setValues);
|
|
319
239
|
return (_jsx(ParameterContext.Provider, { value: {
|
|
320
240
|
...values,
|
|
321
241
|
setOffset,
|
|
@@ -324,14 +244,19 @@ const ParameterProvider = ({ children }) => {
|
|
|
324
244
|
setQuery: useMemo(() => set('query'), [set]),
|
|
325
245
|
setSort: useMemo(() => set('sort'), [set]),
|
|
326
246
|
setSpan: useMemo(() => set('span'), [set]),
|
|
327
|
-
addFilter,
|
|
328
|
-
removeFilter,
|
|
329
|
-
setFilter,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
247
|
+
addFilter: filters.add,
|
|
248
|
+
removeFilter: filters.remove,
|
|
249
|
+
setFilter: filters.setAt,
|
|
250
|
+
resetFilters: filters.reset,
|
|
251
|
+
addIndex: indexes.add,
|
|
252
|
+
removeIndex: indexes.remove,
|
|
253
|
+
setIndex: indexes.setAt,
|
|
254
|
+
setIndexes: indexes.setAll,
|
|
255
|
+
resetIndexes: useCallback(() => indexes.reset(DEFAULT_VALUES.indexes), [indexes]),
|
|
256
|
+
addView: views.add,
|
|
257
|
+
removeView: views.remove,
|
|
258
|
+
setView: views.setAt,
|
|
259
|
+
resetViews: views.reset
|
|
335
260
|
}, children: children }));
|
|
336
261
|
};
|
|
337
262
|
export const useParameterContextSelector = (selector) => {
|