@rancher/shell 3.0.8 → 3.0.9-rc.1

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.
@@ -11,6 +11,44 @@ export interface ModalConfig {
11
11
  * ```ts
12
12
  * props: { title: 'Hello Modal', isVisible: true }
13
13
  * ```
14
+ *
15
+ * Props can include callback functions to be invoked when confirming a modal.
16
+ *
17
+ * Example with a callback function:
18
+ *
19
+ * ```ts
20
+ * import MyCustomModal from './MyCustomModal.vue';
21
+ *
22
+ * export default {
23
+ * methods: {
24
+ * myAction() {
25
+ * console.log('Performed an action');
26
+ * },
27
+ * showModal() {
28
+ * this.$shell.modal.open(MyCustomModal, {
29
+ * props: { onConfirm: this.myAction }
30
+ * });
31
+ * }
32
+ * }
33
+ * }
34
+ * ```
35
+ *
36
+ * ```ts
37
+ * export default {
38
+ * props: {
39
+ * onConfirm: {
40
+ * type: Function,
41
+ * required: true
42
+ * }
43
+ * },
44
+ * methods:.
45
+ * confirm() {
46
+ * this.onConfirm();
47
+ * this.$emit('close');
48
+ * }
49
+ * }
50
+ * }
51
+ * ```
14
52
  */
15
53
  props?: Record<string, any>;
16
54
 
@@ -65,7 +65,9 @@ export interface SlideInConfig {
65
65
  * Useful for passing additional information or context to the component rendered inside the Slide-In window
66
66
  *
67
67
  */
68
- [key: string]: any;
68
+ props?: {
69
+ [key: string]: any;
70
+ };
69
71
  }
70
72
 
71
73
  /**
@@ -51,4 +51,40 @@ describe('slideInApiImpl', () => {
51
51
  componentProps: { ...config }, // The implementation spreads the config
52
52
  });
53
53
  });
54
+
55
+ it('should open a slide-in panel with an undefined config gracefully', () => {
56
+ // 3. Act
57
+ slideInApi.open(MockComponent, undefined);
58
+
59
+ // 4. Assert
60
+ expect(mockCommit).toHaveBeenCalledTimes(1);
61
+ expect(mockCommit).toHaveBeenCalledWith('slideInPanel/open', {
62
+ component: MockComponent,
63
+ componentProps: {},
64
+ });
65
+ });
66
+
67
+ it('should handle config with props property', () => {
68
+ const config = {
69
+ title: 'Test Panel',
70
+ props: {
71
+ foo: 'bar',
72
+ baz: 123
73
+ }
74
+ };
75
+
76
+ // 3. Act
77
+ slideInApi.open(MockComponent, config);
78
+
79
+ // 4. Assert
80
+ expect(mockCommit).toHaveBeenCalledTimes(1);
81
+ expect(mockCommit).toHaveBeenCalledWith('slideInPanel/open', {
82
+ component: MockComponent,
83
+ componentProps: {
84
+ title: 'Test Panel',
85
+ foo: 'bar',
86
+ baz: 123
87
+ },
88
+ });
89
+ });
54
90
  });
@@ -25,9 +25,13 @@ export class SlideInApiImpl implements SlideInApi {
25
25
  * @param config - Slide-In configuration object
26
26
  */
27
27
  public open(component: Component, config?: SlideInConfig): void {
28
+ const props = config?.props || {};
29
+
30
+ delete config?.props;
31
+
28
32
  this.store.commit('slideInPanel/open', {
29
33
  component,
30
- componentProps: { ...config || {} }
34
+ componentProps: { ...(config || {}), ...props }
31
35
  });
32
36
  }
33
37
  }
@@ -159,6 +159,7 @@ export default {
159
159
  :scrolling="false"
160
160
  :value="valuesYaml"
161
161
  editor-mode="VIEW_CODE"
162
+ mode="view"
162
163
  />
163
164
  </Tab>
164
165
  <Tab
@@ -4,100 +4,138 @@ import Workload from '@shell/edit/workload/index.vue';
4
4
  jest.mock('@shell/models/secret', () => ({ onmessage: jest.fn() }));
5
5
 
6
6
  describe('component: Workload', () => {
7
- it.each([
8
- [
9
- `pods \"test\" is forbidden: violates PodSecurity \"restricted:latest\": allowPrivilegeEscalation != false (container \"container-0\" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container \"container-0\" must set securityContext.capabilities.drop=[\"ALL\"]), runAsNonRoot != true (container \"container-0\" must not set securityContext.runAsNonRoot=false), seccompProfile (pod or container \"container-0\" must set securityContext.seccompProfile.type to \"RuntimeDefault\" or \"Localhost\")`,
10
- `workload.error, \"test\",\"restricted:latest\"`
11
- ]
12
- ])('should map error message into object', (oldMessage, newMessage) => {
13
- const mockedValidationMixin = {
14
- methods: {
15
- fvFormIsValid: jest.fn(),
16
- type: jest.fn(),
17
- fvGetAndReportPathRules: jest.fn(),
18
- },
19
- computed: { fvUnreportedValidationErrors: jest.fn().mockReturnValue([]) }
20
- };
21
- const mockedCREMixin = {};
22
- const mockedWorkloadMixin = {
23
- methods: {
24
- doneRoute: jest.fn(),
25
- workloadSubTypes: jest.fn(),
26
- applyHooks: jest.fn(),
27
- save: jest.fn(),
28
- selectType: jest.fn(),
29
- isCronJob: jest.fn(),
30
- spec: jest.fn(),
31
- isReplicable: jest.fn(),
32
- isStatefulSet: jest.fn(),
33
- headlessServices: jest.fn(),
34
- defaultTab: jest.fn(),
35
- allContainers: jest.fn(),
36
- isPod: jest.fn(),
37
- tabWeightMap: jest.fn(),
38
- podLabels: jest.fn(),
39
- podTemplateSpec: jest.fn(),
40
- isLoadingSecondaryResources: jest.fn(),
41
- allNodes: jest.fn(),
42
- allNodeObjects: jest.fn(),
43
- clearPvcFormState: jest.fn(),
44
- savePvcHookName: jest.fn(),
45
- namespacedConfigMaps: jest.fn(),
46
- podAnnotations: jest.fn(),
47
- isJob: jest.fn(),
48
- podFsGroup: jest.fn(),
49
- namespacedSecrets: jest.fn(),
50
- registerBeforeHook: jest.fn(),
51
- pvcs: jest.fn(),
52
- // tabWeightMap: jest.fn(),
53
- }
54
- };
7
+ const baseMockedValidationMixin = {
8
+ methods: {
9
+ fvFormIsValid: jest.fn(),
10
+ type: jest.fn(),
11
+ fvGetAndReportPathRules: jest.fn(),
12
+ },
13
+ computed: { fvUnreportedValidationErrors: jest.fn().mockReturnValue([]) }
14
+ };
15
+ const baseMockedCREMixin = {};
16
+ const baseMockedWorkloadMixin = {
17
+ methods: {
18
+ doneRoute: jest.fn(),
19
+ workloadSubTypes: jest.fn(),
20
+ applyHooks: jest.fn(),
21
+ save: jest.fn(),
22
+ selectType: jest.fn(),
23
+ isCronJob: jest.fn(),
24
+ spec: jest.fn(),
25
+ isReplicable: jest.fn(),
26
+ isStatefulSet: jest.fn(),
27
+ headlessServices: jest.fn(),
28
+ defaultTab: jest.fn(),
29
+ allContainers: jest.fn(),
30
+ isPod: jest.fn(),
31
+ tabWeightMap: jest.fn(),
32
+ podLabels: jest.fn(),
33
+ podTemplateSpec: jest.fn(),
34
+ isLoadingSecondaryResources: jest.fn(),
35
+ allNodes: jest.fn(),
36
+ clearPvcFormState: jest.fn(),
37
+ savePvcHookName: jest.fn(),
38
+ namespacedConfigMaps: jest.fn(),
39
+ podAnnotations: jest.fn(),
40
+ isJob: jest.fn(),
41
+ podFsGroup: jest.fn(),
42
+ namespacedSecrets: jest.fn(),
43
+ registerBeforeHook: jest.fn(),
44
+ pvcs: jest.fn(),
45
+ }
46
+ };
55
47
 
56
- const MockedWorkload = { ...Workload, mixins: [mockedValidationMixin, mockedCREMixin, mockedWorkloadMixin] };
57
- const wrapper = shallowMount(MockedWorkload, {
58
- props: {
59
- value: { metadata: {}, spec: { template: {} } },
60
- params: {},
61
- fvFormIsValid: {}
62
- },
48
+ describe('component: Workload', () => {
49
+ it.each([
50
+ [
51
+ `pods \"test\" is forbidden: violates PodSecurity \"restricted:latest\": allowPrivilegeEscalation != false (container \"container-0\" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container \"container-0\" must set securityContext.capabilities.drop=[\"ALL\"]), runAsNonRoot != true (container \"container-0\" must not set securityContext.runAsNonRoot=false), seccompProfile (pod or container \"container-0\" must set securityContext.seccompProfile.type to \"RuntimeDefault\" or \"Localhost\")`,
52
+ `workload.error, \"test\",\"restricted:latest\"`
53
+ ]
54
+ ])('should map error message into object', (oldMessage, newMessage) => {
55
+ // For this test, allNodeObjects is just a jest.fn() in the base mixin
56
+ const MockedWorkload = { ...Workload, mixins: [baseMockedValidationMixin, baseMockedCREMixin, { ...baseMockedWorkloadMixin, computed: { allNodeObjects: jest.fn() } }] };
57
+ const wrapper = shallowMount(MockedWorkload, {
58
+ props: {
59
+ value: { metadata: {}, spec: { template: {} } },
60
+ params: {},
61
+ fvFormIsValid: {}
62
+ },
63
63
 
64
- global: {
65
- mocks: {
66
- $route: { params: {}, query: {} },
67
- $router: { applyQuery: jest.fn() },
68
- $fetchState: { pending: false },
69
- $store: {
70
- getters: {
71
- 'cluster/schemaFor': jest.fn(),
72
- 'type-map/labelFor': jest.fn(),
73
- 'i18n/t': (text: string, v: {[key:string]: string}) => {
74
- return `${ text }, ${ Object.values(v || {}) }`;
64
+ global: {
65
+ mocks: {
66
+ $route: { params: {}, query: {} },
67
+ $router: { applyQuery: jest.fn() },
68
+ $fetchState: { pending: false },
69
+ $store: {
70
+ getters: {
71
+ 'cluster/schemaFor': jest.fn(),
72
+ 'type-map/labelFor': jest.fn(),
73
+ 'i18n/t': (text: string, v: {[key:string]: string}) => {
74
+ return `${ text }, ${ Object.values(v || {}) }`;
75
+ },
75
76
  },
76
77
  },
77
78
  },
78
- },
79
79
 
80
- stubs: {
81
- Tab: true,
82
- LabeledInput: true,
83
- VolumeClaimTemplate: true,
84
- Networking: true,
85
- Job: true,
86
- NodeScheduling: true,
87
- PodAffinity: true,
88
- Tolerations: true,
89
- Storage: true,
90
- Tabbed: true,
91
- LabeledSelect: true,
92
- NameNsDescription: true,
93
- CruResource: true,
94
- KeyValue: true
80
+ stubs: {
81
+ Tab: true,
82
+ LabeledInput: true,
83
+ VolumeClaimTemplate: true,
84
+ Networking: true,
85
+ Job: true,
86
+ NodeScheduling: true,
87
+ PodAffinity: true,
88
+ Tolerations: true,
89
+ Storage: true,
90
+ Tabbed: true,
91
+ LabeledSelect: true,
92
+ NameNsDescription: true,
93
+ CruResource: true,
94
+ KeyValue: true
95
+ },
95
96
  },
96
- },
97
+ });
98
+
99
+ const result = (wrapper.vm as any).mapError(oldMessage).message;
100
+
101
+ expect(result).toStrictEqual(newMessage);
97
102
  });
98
103
 
99
- const result = (wrapper.vm as any).mapError(oldMessage).message;
104
+ describe('secondaryResourceDataConfig', () => {
105
+ it('should filter out nodes with control-plane or etcd taints from workerNodes parsingFunc', () => {
106
+ const allNodeObjects = [
107
+ {
108
+ id: 'node-1',
109
+ spec: { taints: [{ key: 'node-role.kubernetes.io/control-plane', effect: 'NoSchedule' }] }
110
+ },
111
+ {
112
+ id: 'node-2',
113
+ spec: { taints: [{ key: 'node-role.kubernetes.io/etcd', effect: 'NoSchedule' }] }
114
+ },
115
+ {
116
+ id: 'node-3',
117
+ spec: { taints: [{ key: 'node-role.kubernetes.io/worker', effect: 'NoSchedule' }] }
118
+ },
119
+ {
120
+ id: 'node-4',
121
+ spec: { taints: [] }
122
+ },
123
+ {
124
+ id: 'node-5',
125
+ spec: {}
126
+ },
127
+ {
128
+ id: 'node-6',
129
+ spec: null
130
+ }
131
+ ];
132
+
133
+ const { data } = (Workload.mixins[2] as any).methods.secondaryResourceDataConfig.apply({ value: { metadata: { namespace: 'test' } } });
134
+ const workerNodesParsingFunc = data.node.applyTo.find((r: any) => r.var === 'workerNodes').parsingFunc;
135
+ const result = workerNodesParsingFunc(allNodeObjects);
100
136
 
101
- expect(result).toStrictEqual(newMessage);
137
+ expect(result).toStrictEqual(['node-3', 'node-4', 'node-5', 'node-6']);
138
+ });
139
+ });
102
140
  });
103
141
  });
@@ -494,7 +494,7 @@ export default {
494
494
  <NodeScheduling
495
495
  :mode="mode"
496
496
  :value="podTemplateSpec"
497
- :nodes="allNodes"
497
+ :nodes="workerNodes"
498
498
  :loading="isLoadingSecondaryResources"
499
499
  />
500
500
  </Tab>
@@ -258,6 +258,7 @@ export default {
258
258
  secondaryResourceData: this.secondaryResourceDataConfig(),
259
259
  namespacedConfigMaps: [],
260
260
  allNodes: null,
261
+ workerNodes: null,
261
262
  allNodeObjects: [],
262
263
  namespacedSecrets: [],
263
264
  imagePullNamespacedSecrets: [],
@@ -647,7 +648,24 @@ export default {
647
648
  parsingFunc: (data) => {
648
649
  return data.map((node) => node.id);
649
650
  }
650
- }
651
+ },
652
+ {
653
+ var: 'workerNodes',
654
+ parsingFunc: (data) => {
655
+ const keys = [
656
+ `node-role.kubernetes.io/control-plane`,
657
+ `node-role.kubernetes.io/etcd`
658
+ ];
659
+
660
+ return data
661
+ .filter((node) => {
662
+ const taints = node?.spec?.taints || [];
663
+
664
+ return taints.every((taint) => !keys.includes(taint.key));
665
+ })
666
+ .map((node) => node.id);
667
+ }
668
+ },
651
669
  ]
652
670
  },
653
671
  [SERVICE]: {
@@ -1,6 +1,7 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { CATALOG, MANAGEMENT } from '@shell/config/types';
3
3
  import Brand from '@shell/mixins/brand';
4
+ import CspAdapterUtils from '@shell/utils/cspAdaptor';
4
5
 
5
6
  describe('brandMixin', () => {
6
7
  const createWrapper = (vaiOn = false) => {
@@ -54,45 +55,47 @@ describe('brandMixin', () => {
54
55
  data: () => data,
55
56
  global: { mocks: { $store: store } }
56
57
  });
57
- const spyManagementFindAll = jest.spyOn(store, 'dispatch');
58
+ const spyManagementDispatch = jest.spyOn(store, 'dispatch');
59
+
60
+ CspAdapterUtils.resetState();
58
61
 
59
62
  return {
60
63
  wrapper,
61
64
  store,
62
- spyManagementFindAll,
65
+ spyManagementDispatch,
63
66
  };
64
67
  };
65
68
 
66
69
  describe('should make correct requests', () => {
67
70
  it('vai off', async() => {
68
- const { wrapper, spyManagementFindAll } = createWrapper(false);
71
+ const { wrapper, spyManagementDispatch } = createWrapper(false);
69
72
 
70
73
  // NOTE - wrapper.vm.$options.fetch() doesn't work
71
74
  await wrapper.vm.$options.fetch.apply(wrapper.vm);
72
75
 
73
76
  // wrapper.vm.$nextTick();
74
- expect(spyManagementFindAll).toHaveBeenNthCalledWith(1, 'management/findAll', {
77
+ expect(spyManagementDispatch).toHaveBeenNthCalledWith(1, 'management/findAll', {
75
78
  type: MANAGEMENT.SETTING,
76
79
  opt: {
77
80
  load: 'multi', redirectUnauthorized: false, url: `/v1/${ MANAGEMENT.SETTING }s`
78
81
  }
79
82
  });
80
- expect(spyManagementFindAll).toHaveBeenNthCalledWith(2, 'management/findAll', { type: CATALOG.APP });
83
+ expect(spyManagementDispatch).toHaveBeenNthCalledWith(2, 'management/findAll', { type: CATALOG.APP });
81
84
  });
82
85
 
83
86
  it('vai on', async() => {
84
- const { wrapper, spyManagementFindAll } = createWrapper(true);
87
+ const { wrapper, spyManagementDispatch } = createWrapper(true);
85
88
 
86
89
  // NOTE - wrapper.vm.$options.fetch() doesn't work
87
90
  await wrapper.vm.$options.fetch.apply(wrapper.vm);
88
91
 
89
- expect(spyManagementFindAll).toHaveBeenNthCalledWith(1, 'management/findAll', {
92
+ expect(spyManagementDispatch).toHaveBeenNthCalledWith(1, 'management/findAll', {
90
93
  type: MANAGEMENT.SETTING,
91
94
  opt: {
92
95
  load: 'multi', url: `/v1/${ MANAGEMENT.SETTING }s`, redirectUnauthorized: false
93
96
  }
94
97
  });
95
- expect(spyManagementFindAll).toHaveBeenNthCalledWith(2, 'management/findPage', {
98
+ expect(spyManagementDispatch).toHaveBeenNthCalledWith(2, 'management/findPage', {
96
99
  type: CATALOG.APP,
97
100
  opt: {
98
101
  pagination: {
@@ -110,7 +113,9 @@ describe('brandMixin', () => {
110
113
  pageSize: null,
111
114
  projectsOrNamespaces: [],
112
115
  sort: []
113
- }
116
+ },
117
+ transient: true,
118
+ watch: false
114
119
  }
115
120
  });
116
121
  });
@@ -120,7 +125,7 @@ describe('brandMixin', () => {
120
125
  it('should have correct csp values (off)', async() => {
121
126
  const { wrapper, store } = createWrapper();
122
127
 
123
- const spyManagementFindAll = jest.spyOn(store, 'dispatch').mockImplementation((_, options) => {
128
+ const spyManagementDispatch = jest.spyOn(store, 'dispatch').mockImplementation((_, options) => {
124
129
  const { type } = options as any;
125
130
 
126
131
  if (type === MANAGEMENT.SETTING) {
@@ -136,7 +141,7 @@ describe('brandMixin', () => {
136
141
  // NOTE - wrapper.vm.$options.fetch() doesn't work
137
142
  await wrapper.vm.$options.fetch.apply(wrapper.vm, []);
138
143
 
139
- expect(spyManagementFindAll).toHaveBeenCalledTimes(2);
144
+ expect(spyManagementDispatch).toHaveBeenCalledTimes(2);
140
145
 
141
146
  expect(wrapper.vm.canCalcCspAdapter).toBeTruthy();
142
147
  expect(wrapper.vm.cspAdapter).toBeFalsy();
@@ -145,7 +150,7 @@ describe('brandMixin', () => {
145
150
  it.each(['rancher-csp-adapter', 'rancher-csp-billing-adapter'])('should have correct csp values (on - %p )', async(chartName) => {
146
151
  const { wrapper, store } = createWrapper();
147
152
 
148
- const spyManagementFindAll = jest.spyOn(store, 'dispatch').mockImplementation((_, options) => {
153
+ const spyManagementDispatch = jest.spyOn(store, 'dispatch').mockImplementation((_, options) => {
149
154
  const { type } = options as any;
150
155
 
151
156
  if (type === MANAGEMENT.SETTING) {
@@ -161,7 +166,7 @@ describe('brandMixin', () => {
161
166
  // NOTE - wrapper.vm.$options.fetch() doesn't work
162
167
  await wrapper.vm.$options.fetch.apply(wrapper.vm, []);
163
168
 
164
- expect(spyManagementFindAll).toHaveBeenCalledTimes(2);
169
+ expect(spyManagementDispatch).toHaveBeenCalledTimes(2);
165
170
 
166
171
  expect(wrapper.vm.canCalcCspAdapter).toBeTruthy();
167
172
  expect(wrapper.vm.cspAdapter).toBeTruthy();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "3.0.8",
3
+ "version": "3.0.9-rc.1",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancher/dashboard",
6
6
  "license": "Apache-2.0",
@@ -1,4 +1,4 @@
1
- <script>
1
+ <script lang="ts">
2
2
  import debounce from 'lodash/debounce';
3
3
  import { getVersionData } from '@shell/config/version';
4
4
  import { mapState, mapGetters } from 'vuex';
@@ -322,13 +322,13 @@ export default {
322
322
  this.selectedCard = selected;
323
323
 
324
324
  this.$shell.slideIn.open(ResourceDetails, {
325
- componentProps: {
326
- showHeader: false,
327
- width: window.innerWidth / 3 > 530 ? `${ window.innerWidth / 3 }px` : '530px',
325
+ showHeader: false,
326
+ width: window.innerWidth / 3 > 530 ? `${ window.innerWidth / 3 }px` : '530px',
327
+ props: {
328
328
  value,
329
329
  statePanel,
330
330
  workspace
331
- }
331
+ },
332
332
  });
333
333
  },
334
334
 
@@ -180,6 +180,15 @@ export default {
180
180
  * Load multiple different types of resources
181
181
  */
182
182
  loadMulti(state, { data, ctx }) {
183
+ const type = data[0]?.type;
184
+ const cache = state.types[type];
185
+
186
+ if (cache?.havePage) {
187
+ console.warn(`Prevented \`loadMulti\` mutation from polluting the cache for type "${ type }" (currently represents a page).`); // eslint-disable-line no-console
188
+
189
+ return;
190
+ }
191
+
183
192
  for (const entry of data) {
184
193
  const resource = load(state, { data: entry, ctx });
185
194
 
@@ -1200,6 +1200,18 @@ const defaultActions = {
1200
1200
  },
1201
1201
 
1202
1202
  'ws.resource.create'(ctx, msg) {
1203
+ const data = msg.data;
1204
+ const type = data?.type;
1205
+
1206
+ const havePage = ctx.getters['havePage'](type);
1207
+
1208
+ if (havePage) {
1209
+ console.warn(`Prevented watch \`resource.create\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`, msg); // eslint-disable-line no-console
1210
+ ctx.dispatch('unwatch', { ...msg, type });
1211
+
1212
+ return;
1213
+ }
1214
+
1203
1215
  ctx.state.debugSocket && console.info(`Resource Create [${ ctx.getters.storeName }]`, msg.resourceType, msg); // eslint-disable-line no-console
1204
1216
  queueChange(ctx, msg, true, 'Create');
1205
1217
  },
@@ -1230,8 +1242,8 @@ const defaultActions = {
1230
1242
  const havePage = ctx.getters['havePage'](type);
1231
1243
 
1232
1244
  if (havePage) {
1233
- console.warn(`Prevented watch \`resource.change\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`, data); // eslint-disable-line no-console
1234
- ctx.dispatch('unwatch', data);
1245
+ console.warn(`Prevented watch \`resource.change\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`, msg); // eslint-disable-line no-console
1246
+ ctx.dispatch('unwatch', { ...msg, type });
1235
1247
 
1236
1248
  return;
1237
1249
  }
@@ -1277,6 +1289,15 @@ const defaultActions = {
1277
1289
  }
1278
1290
  }
1279
1291
 
1292
+ const havePage = ctx.getters['havePage'](type);
1293
+
1294
+ if (havePage) {
1295
+ console.warn(`Prevented watch \`resource.remove\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`, msg); // eslint-disable-line no-console
1296
+ ctx.dispatch('unwatch', { ...msg, type });
1297
+
1298
+ return;
1299
+ }
1300
+
1280
1301
  queueChange(ctx, msg, false, 'Remove');
1281
1302
 
1282
1303
  const typeOption = ctx.rootGetters['type-map/optionsFor'](type);
@@ -48,11 +48,6 @@ export interface ActionFindAllArgs extends ActionCoreFindArgs {
48
48
  depaginate?: boolean,
49
49
  }
50
50
 
51
- export interface ActionFindPageTransientResult<T> {
52
- pagination: StorePagination,
53
- data: T[],
54
- }
55
-
56
51
  /**
57
52
  * Args used for findPage action
58
53
  */
@@ -81,19 +76,46 @@ export interface ActionFindPageArgs extends ActionCoreFindArgs {
81
76
  * If true don't persist the http response to the store, just pass it back
82
77
  */
83
78
  transient?: boolean,
79
+ /**
80
+ * The target minimum revision for the resource.
81
+ *
82
+ * If this is higher than the latest revision known to rancher then an error will be returned
83
+ */
84
+ revision?: string
84
85
  }
85
86
 
86
- export type ActionFindPageResponse<T = any> = {
87
+ /**
88
+ * Response to a transient (not stored in cache) findPage action
89
+ */
90
+ export type ActionFindPageTransientResponse<T = any> = {
87
91
  data: T[],
88
92
  pagination?: StorePagination
89
- } | T[];
93
+ };
94
+
95
+ /**
96
+ * Response to the findPage action
97
+ *
98
+ * If the request was transient (not stored in cache) this will be an object contain all the details of the request
99
+ *
100
+ * If the request was not transient this will just be the array of resources
101
+ */
102
+ export type ActionFindPageResponse<T = any> = ActionFindPageTransientResponse | T[];
90
103
 
104
+ /**
105
+ * Args used for findPage action
106
+ */
91
107
  export interface ActionFindMatchingArgs extends ActionCoreFindArgs {
92
108
  labelSelector: KubeLabelSelector,
93
109
  namespaced?: string,
94
110
  depaginate?: boolean
95
111
  }
96
112
 
113
+ /**
114
+ * Response to the findMatching action
115
+ */
97
116
  export type ActionFindMatchingResponse<T = any> = ActionFindPageResponse<T>
98
117
 
118
+ /**
119
+ * Args used for findLabelSelector action
120
+ */
99
121
  export type ActionFindLabelSelectorArgs = ActionFindPageArgs | ActionFindMatchingArgs;
@@ -1,12 +1,13 @@
1
1
  // For testing these could be changed to something like...
2
2
 
3
3
  import { CATALOG } from '@shell/config/types';
4
+ import { ActionFindPageArgs, ActionFindPageTransientResponse } from '@shell/types/store/dashboard-store.types';
4
5
  import { FilterArgs, PaginationFilterField, PaginationParamFilter } from '@shell/types/store/pagination.types';
5
6
  import { VuexStore } from '@shell/types/store/vuex';
6
7
 
7
8
  const CSP_ADAPTER_APPS = ['rancher-csp-adapter', 'rancher-csp-billing-adapter'];
8
9
  // For testing above line could be replaced with below line...
9
- // const cspAdaptorApp = ['rancher-webhooka', 'rancher-webhook'];
10
+ // const CSP_ADAPTER_APPS = ['rancher-webhooka', 'rancher-webhook'];
10
11
 
11
12
  /**
12
13
  * Helpers in order to
@@ -16,27 +17,44 @@ class CspAdapterUtils {
16
17
  return $store.getters[`management/paginationEnabled`]({ id: CATALOG.APP, context: 'branding' });
17
18
  }
18
19
 
19
- static fetchCspAdaptorApp($store: VuexStore): Promise<any> {
20
+ private static apps?: any[] = undefined;
21
+ public static resetState() {
22
+ this.apps = undefined;
23
+ }
24
+
25
+ static async fetchCspAdaptorApp($store: VuexStore): Promise<any> {
26
+ if (this.apps) {
27
+ return this.apps;
28
+ }
29
+
20
30
  // For the login page, the schemas won't be loaded - we don't need the apps in this case
21
31
  if ($store.getters['management/canList'](CATALOG.APP)) {
22
32
  if (CspAdapterUtils.canPagination($store)) {
23
33
  // Restrict the amount of apps we need to fetch
24
- return $store.dispatch('management/findPage', {
34
+ const opt: ActionFindPageArgs = {
35
+ pagination: new FilterArgs({
36
+ filters: PaginationParamFilter.createMultipleFields(CSP_ADAPTER_APPS.map(
37
+ (t) => new PaginationFilterField({
38
+ field: 'metadata.name',
39
+ value: t,
40
+ })
41
+ )),
42
+ }),
43
+ watch: false,
44
+ transient: true
45
+ };
46
+
47
+ const resp: ActionFindPageTransientResponse = await $store.dispatch('management/findPage', {
25
48
  type: CATALOG.APP,
26
- opt: { // Of type ActionFindPageArgs
27
- pagination: new FilterArgs({
28
- filters: PaginationParamFilter.createMultipleFields(CSP_ADAPTER_APPS.map(
29
- (t) => new PaginationFilterField({
30
- field: 'metadata.name',
31
- value: t,
32
- })
33
- )),
34
- })
35
- }
49
+ opt
36
50
  });
51
+
52
+ this.apps = resp.data;
53
+ } else {
54
+ this.apps = await $store.dispatch('management/findAll', { type: CATALOG.APP });
37
55
  }
38
56
 
39
- return $store.dispatch('management/findAll', { type: CATALOG.APP });
57
+ return this.apps;
40
58
  }
41
59
 
42
60
  return Promise.resolve([]);
@@ -1,7 +1,7 @@
1
1
  import paginationUtils from '@shell/utils/pagination-utils';
2
2
  import { PaginationArgs, PaginationResourceContext } from '@shell/types/store/pagination.types';
3
3
  import { VuexStore } from '@shell/types/store/vuex';
4
- import { ActionFindPageArgs, ActionFindPageTransientResult } from '@shell/types/store/dashboard-store.types';
4
+ import { ActionFindPageArgs, ActionFindPageTransientResponse } from '@shell/types/store/dashboard-store.types';
5
5
  import { STEVE_WATCH_EVENT_TYPES, STEVE_WATCH_MODE } from '@shell/types/store/subscribe.types';
6
6
  import { Reactive, reactive } from 'vue';
7
7
  import { STEVE_UNWATCH_EVENT_PARAMS, STEVE_WATCH_EVENT_LISTENER_CALLBACK, STEVE_WATCH_EVENT_PARAMS, STEVE_WATCH_EVENT_PARAMS_COMMON } from '@shell/types/store/subscribe-events.types';
@@ -30,7 +30,7 @@ interface Args {
30
30
  }
31
31
  }
32
32
 
33
- interface Result<T> extends Omit<ActionFindPageTransientResult<T>, 'data'> {
33
+ interface Result<T> extends Omit<ActionFindPageTransientResponse<T>, 'data'> {
34
34
  data: Reactive<T[]>
35
35
  }
36
36
 
@@ -86,7 +86,7 @@ class PaginationWrapper<T extends object> {
86
86
  };
87
87
 
88
88
  // Fetch
89
- const out: ActionFindPageTransientResult<T> = await this.$store.dispatch(`${ this.enabledFor.store }/findPage`, { opt, type: this.enabledFor.resource?.id });
89
+ const out: ActionFindPageTransientResponse<T> = await this.$store.dispatch(`${ this.enabledFor.store }/findPage`, { opt, type: this.enabledFor.resource?.id });
90
90
 
91
91
  // Watch
92
92
  const firstTime = !this.steveWatchParams;