Skip to content

Commit efd5ecf

Browse files
authored
Merge pull request #15855 from richard-cox/fix-ssp-selection
Ensure we keep the same cached list and previous entries when SSP pages update
2 parents 974a0ca + 7edcca2 commit efd5ecf

File tree

2 files changed

+202
-2
lines changed

2 files changed

+202
-2
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import mutations from '@shell/plugins/dashboard-store/mutations';
2+
import getters from '@shell/plugins/steve/getters';
3+
import { StorePagination } from '@shell/types/store/pagination.types';
4+
import { VuexStore } from '@shell/types/store/vuex';
5+
import Pod from '@shell/models/pod';
6+
7+
describe('mutations', () => {
8+
jest.mock('vue', () => ({ reactive: jest.fn((arr) => arr) }));
9+
10+
// Import the function we are testing
11+
const { loadPage } = mutations;
12+
13+
let state: any;
14+
let ctx: Partial<VuexStore>;
15+
let resourceType: string;
16+
let pagination: Partial<StorePagination>;
17+
let revision: string;
18+
19+
beforeEach(() => {
20+
jest.clearAllMocks();
21+
22+
state = { types: {} };
23+
ctx = {
24+
getters: {
25+
keyFieldForType: jest.fn(() => 'id'),
26+
cleanResource: getters.cleanResource()
27+
}
28+
};
29+
resourceType = 'pod';
30+
pagination = { request: undefined, result: undefined };
31+
revision = 'abc-123';
32+
ctx.getters.classify = () => Pod;
33+
});
34+
35+
describe('loadPage', () => {
36+
it('should perform an initial load into an empty cache', () => {
37+
const data = [
38+
{
39+
id: 'a', name: 'pod-a', type: resourceType
40+
},
41+
{
42+
id: 'b', name: 'pod-b', type: resourceType
43+
}
44+
];
45+
46+
loadPage(state, {
47+
type: resourceType, data, ctx, pagination, revision
48+
});
49+
50+
// Get the cache created by the *real* 'registerType'
51+
const cache = state.types[resourceType];
52+
53+
// Check cache metadata
54+
expect(cache.generation).toBe(1);
55+
expect(cache.havePage).toBe(pagination);
56+
expect(cache.revision).toBe(revision);
57+
58+
// Check 'list' (should contain classified proxies)
59+
expect(cache.list).toHaveLength(2);
60+
expect(cache.list[0]).toStrictEqual(expect.objectContaining({ id: 'a' }));
61+
expect(cache.list[1]).toStrictEqual(expect.objectContaining({ id: 'b' }));
62+
63+
// Check 'map'
64+
expect(cache.map.size).toBe(2);
65+
expect(cache.map.get('a')).toBe(cache.list[0]); // Map and list must reference the *same* objects
66+
expect(cache.map.get('b')).toBe(cache.list[1]);
67+
});
68+
69+
it('should update existing resources in-place (tests "replaceResource" effect)', () => {
70+
const v1Resource = new Pod({ id: 'a', metadata: { a: 'v1-a' } }, ctx);
71+
72+
state.types[resourceType] = {
73+
list: [v1Resource],
74+
map: new Map([['a', v1Resource]]),
75+
generation: 1,
76+
};
77+
78+
// Keep a reference to the *original list array* and *original resource object*
79+
const originalListRef = state.types[resourceType].list;
80+
const originalResourceRef = v1Resource;
81+
82+
const newData = [
83+
{ id: 'a', metadata: { a: 'v2-a' } }, // This is the update
84+
];
85+
const newRevision = 'rev-2';
86+
87+
loadPage(state, {
88+
type: resourceType, data: newData, ctx, pagination, revision: newRevision
89+
});
90+
91+
const cache = state.types[resourceType];
92+
93+
expect(cache.list).toBe(originalListRef);
94+
expect(cache.list).toHaveLength(1);
95+
96+
expect(cache.list[0]).toBe(originalResourceRef);
97+
98+
expect(originalResourceRef.metadata.a).toBe('v2-a');
99+
100+
expect(cache.map.get('a')).toBe(originalResourceRef);
101+
102+
expect(cache.generation).toBe(2);
103+
expect(cache.revision).toBe(newRevision);
104+
});
105+
106+
it('should remove stale data from list and map (paging)', () => {
107+
const page1Proxy = new Pod({ id: 'a', name: 'stale-pod' }, ctx);
108+
109+
state.types[resourceType] = {
110+
list: [page1Proxy],
111+
map: new Map([['a', page1Proxy]]),
112+
generation: 1,
113+
};
114+
115+
const listRef = state.types[resourceType].list;
116+
117+
const page2Data = [
118+
{ id: 'b', name: 'new-pod' },
119+
];
120+
121+
loadPage(state, {
122+
type: resourceType, data: page2Data, ctx, pagination, revision
123+
});
124+
125+
const cache = state.types[resourceType];
126+
127+
expect(cache.list).toBe(listRef); // List reference must be preserved
128+
129+
expect(cache.list).toHaveLength(1);
130+
expect(cache.list[0].id).toBe('b');
131+
132+
expect(cache.map.size).toBe(1);
133+
expect(cache.map.has('a')).toBe(false); // Stale 'a' is gone
134+
expect(cache.map.has('b')).toBe(true);
135+
});
136+
137+
it('should handle partial overlaps (update, remove stale, add new)', () => {
138+
// 1. Pre-load cache
139+
const staleResource = new Pod({ id: 'a', prop: 'stale-pod' }, ctx);
140+
const v1Resource = new Pod({ id: 'b', prop: 'v1-pod-b' }, ctx);
141+
142+
state.types[resourceType] = {
143+
list: [staleResource, v1Resource],
144+
map: new Map([['a', staleResource], ['b', v1Resource]]),
145+
generation: 1,
146+
};
147+
148+
const listRef = state.types[resourceType].list;
149+
const v1ResourceRef = v1Resource;
150+
151+
const newData = [
152+
{ id: 'b', prop: 'v2-pod-b' }, // Updated
153+
{ id: 'c', prop: 'new-pod-c' }, // New
154+
];
155+
156+
loadPage(state, {
157+
type: resourceType, data: newData, ctx, pagination, revision
158+
});
159+
160+
const cache = state.types[resourceType];
161+
162+
expect(cache.list).toBe(listRef); // List reference preserved
163+
164+
expect(cache.list).toHaveLength(2);
165+
expect(cache.list[0].id).toBe('b');
166+
expect(cache.list[1].id).toBe('c');
167+
168+
expect(cache.list[0]).toBe(v1ResourceRef);
169+
expect(v1ResourceRef.prop).toBe('v2-pod-b');
170+
171+
expect(cache.list[1]).toStrictEqual(expect.objectContaining({ id: 'c' }));
172+
173+
expect(cache.map.size).toBe(2);
174+
expect(cache.map.has('a')).toBe(false); // Stale, removed
175+
expect(cache.map.get('b')).toBe(v1ResourceRef); // Updated
176+
expect(cache.map.get('c')).toBe(cache.list[1]); // Added
177+
});
178+
});
179+
});

shell/plugins/dashboard-store/mutations.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -515,8 +515,29 @@ export default {
515515
cache.generation++;
516516

517517
// Update list
518-
clear(cache.list);
519-
addObjects(cache.list, proxies);
518+
// We want to update the old page with the new page
519+
// We need to keep the cache.list object reference the same
520+
// We need to keep objects that represent the same resource the same (don't remove the old object and add a new object for the same resource)
521+
// - this helps anywhere that works with object references (sortable table selection is maintained by object reference)
522+
523+
// Create a map of the current references in cache.list
524+
const currentPageMap = new Map(cache.list.map((i) => [i[keyField], i]));
525+
526+
// Create an array containing the new page, but using the same object for resources that exist in old and new page
527+
const newPage = proxies.map((p) => {
528+
const existing = currentPageMap.get(p[keyField]);
529+
530+
if (existing) {
531+
replaceResource(existing, p, ctx.getters);
532+
533+
return existing;
534+
}
535+
536+
return p;
537+
});
538+
539+
// Replace the old cache.list entries with the new page
540+
cache.list.splice(0, cache.list.length, ...newPage);
520541

521542
// Update Map (remove stale)
522543
cache.map.forEach((value, key) => {

0 commit comments

Comments
 (0)