Skip to content

Commit 6c61bd5

Browse files
committed
dedupe recent filters
1 parent bd10575 commit 6c61bd5

File tree

2 files changed

+106
-9
lines changed

2 files changed

+106
-9
lines changed

packages/scenes/src/variables/adhoc/AdHocFiltersRecommendations.test.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,28 @@ describe('AdHocFiltersRecommendations', () => {
4848
});
4949
});
5050

51+
it('should deduplicate recentFilters loaded from browser storage', async () => {
52+
const duplicatedFilters = [
53+
{ key: 'pod', operator: '=', value: 'abc' },
54+
{ key: 'cluster', operator: '=', value: 'us-east' },
55+
{ key: 'pod', operator: '=', value: 'abc' },
56+
];
57+
localStorage.setItem(RECENT_FILTERS_KEY, JSON.stringify(duplicatedFilters));
58+
59+
const { filtersVar } = setup({
60+
drilldownRecommendationsEnabled: true,
61+
});
62+
63+
await waitFor(() => {
64+
const recommendations = filtersVar.getRecommendations();
65+
expect(recommendations).toBeDefined();
66+
expect(recommendations?.state.recentFilters).toEqual([
67+
{ key: 'cluster', operator: '=', value: 'us-east' },
68+
{ key: 'pod', operator: '=', value: 'abc' },
69+
]);
70+
});
71+
});
72+
5173
it('should set empty recentFilters when browser storage is empty', async () => {
5274
const { filtersVar } = setup({
5375
drilldownRecommendationsEnabled: true,
@@ -113,6 +135,59 @@ describe('AdHocFiltersRecommendations', () => {
113135
expect(JSON.parse(storedFilters!)).toHaveLength(MAX_STORED_RECENT_DRILLDOWNS);
114136
});
115137

138+
it('should deduplicate when the same filter is stored multiple times', async () => {
139+
const { filtersVar } = setup({
140+
drilldownRecommendationsEnabled: true,
141+
filters: [{ key: 'cluster', value: 'us-east', operator: '=' }],
142+
});
143+
144+
let recommendations: AdHocFiltersRecommendations | undefined;
145+
await waitFor(() => {
146+
recommendations = filtersVar.getRecommendations();
147+
expect(recommendations).toBeDefined();
148+
});
149+
150+
const filter: AdHocFilterWithLabels = { key: 'cluster', value: 'us-east', operator: '=' };
151+
152+
act(() => {
153+
recommendations!.storeRecentFilter(filter);
154+
recommendations!.storeRecentFilter(filter);
155+
recommendations!.storeRecentFilter(filter);
156+
});
157+
158+
const storedFilters = JSON.parse(localStorage.getItem(RECENT_FILTERS_KEY)!);
159+
expect(storedFilters).toHaveLength(1);
160+
expect(storedFilters[0]).toEqual(filter);
161+
});
162+
163+
it('should move re-added filter to the most recent position and not duplicate', async () => {
164+
const { filtersVar } = setup({
165+
drilldownRecommendationsEnabled: true,
166+
filters: [{ key: 'cluster', value: '1', operator: '=' }],
167+
});
168+
169+
let recommendations: AdHocFiltersRecommendations | undefined;
170+
await waitFor(() => {
171+
recommendations = filtersVar.getRecommendations();
172+
expect(recommendations).toBeDefined();
173+
});
174+
175+
const filterA: AdHocFilterWithLabels = { key: 'cluster', value: 'a', operator: '=' };
176+
const filterB: AdHocFilterWithLabels = { key: 'cluster', value: 'b', operator: '=' };
177+
const filterC: AdHocFilterWithLabels = { key: 'cluster', value: 'c', operator: '=' };
178+
179+
act(() => {
180+
recommendations!.storeRecentFilter(filterA);
181+
recommendations!.storeRecentFilter(filterB);
182+
recommendations!.storeRecentFilter(filterC);
183+
recommendations!.storeRecentFilter(filterA);
184+
});
185+
186+
const storedFilters = JSON.parse(localStorage.getItem(RECENT_FILTERS_KEY)!);
187+
expect(storedFilters).toHaveLength(3);
188+
expect(storedFilters).toEqual([filterB, filterC, filterA]);
189+
});
190+
116191
it('should update state with limited recent filters for display', async () => {
117192
const { filtersVar } = setup({
118193
drilldownRecommendationsEnabled: true,

packages/scenes/src/variables/adhoc/AdHocFiltersRecommendations.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@ export interface AdHocFiltersRecommendationsState extends SceneObjectState {
2828
recommendedFilters?: AdHocFilterWithLabels[];
2929
}
3030

31+
/**
32+
* Keeps only the last occurrence of each unique (key, operator, value) triple,
33+
* preserving the most-recent-wins ordering.
34+
*/
35+
function deduplicateFilters(filters: AdHocFilterWithLabels[]): AdHocFilterWithLabels[] {
36+
return filters.reduce<AdHocFilterWithLabels[]>((acc, f) => {
37+
const idx = acc.findIndex(
38+
(existing) => existing.key === f.key && existing.operator === f.operator && existing.value === f.value
39+
);
40+
if (idx !== -1) {
41+
acc.splice(idx, 1);
42+
}
43+
acc.push(f);
44+
return acc;
45+
}, []);
46+
}
47+
3148
export class AdHocFiltersRecommendations extends SceneObjectBase<AdHocFiltersRecommendationsState> {
3249
static Component = AdHocFiltersRecommendationsRenderer;
3350

@@ -149,7 +166,8 @@ export class AdHocFiltersRecommendations extends SceneObjectBase<AdHocFiltersRec
149166
const response = await adhoc.getFiltersApplicabilityForQueries(storedFilters, queries ?? []);
150167

151168
if (!response) {
152-
this.setState({ recentFilters: storedFilters.slice(-MAX_RECENT_DRILLDOWNS) });
169+
const deduped = deduplicateFilters(storedFilters);
170+
this.setState({ recentFilters: deduped.slice(-MAX_RECENT_DRILLDOWNS) });
153171
return;
154172
}
155173

@@ -158,14 +176,14 @@ export class AdHocFiltersRecommendations extends SceneObjectBase<AdHocFiltersRec
158176
applicabilityMap.set(item.key, item.applicable !== false);
159177
});
160178

161-
const applicableFilters = storedFilters
162-
.filter((f) => {
163-
const isApplicable = applicabilityMap.get(f.key);
164-
return isApplicable === undefined || isApplicable === true;
165-
})
166-
.slice(-MAX_RECENT_DRILLDOWNS);
179+
const applicableFilters = storedFilters.filter((f) => {
180+
const isApplicable = applicabilityMap.get(f.key);
181+
return isApplicable === undefined || isApplicable === true;
182+
});
183+
184+
const recentFilters = deduplicateFilters(applicableFilters).slice(-MAX_RECENT_DRILLDOWNS);
167185

168-
this.setState({ recentFilters: applicableFilters });
186+
this.setState({ recentFilters });
169187
}
170188

171189
/**
@@ -177,7 +195,11 @@ export class AdHocFiltersRecommendations extends SceneObjectBase<AdHocFiltersRec
177195
const storedFilters = store.get(key);
178196
const allRecentFilters = storedFilters ? JSON.parse(storedFilters) : [];
179197

180-
const updatedStoredFilters = [...allRecentFilters, filter].slice(-MAX_STORED_RECENT_DRILLDOWNS);
198+
const dedupedFilters = allRecentFilters.filter(
199+
(f: AdHocFilterWithLabels) =>
200+
!(f.key === filter.key && f.operator === filter.operator && f.value === filter.value)
201+
);
202+
const updatedStoredFilters = [...dedupedFilters, filter].slice(-MAX_STORED_RECENT_DRILLDOWNS);
181203
store.set(key, JSON.stringify(updatedStoredFilters));
182204

183205
const adhoc = this._adHocFilter;

0 commit comments

Comments
 (0)