@rancher/shell 3.0.10 → 3.0.11
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/assets/translations/en-us.yaml +7 -5
- package/chart/__tests__/rancher-backup-index.test.ts +248 -0
- package/chart/rancher-backup/index.vue +41 -2
- package/components/BrandImage.vue +6 -5
- package/components/ConsumptionGauge.vue +12 -4
- package/components/DynamicContent/DynamicContentIcon.vue +3 -2
- package/components/ExplorerProjectsNamespaces.vue +1 -4
- package/components/LazyImage.vue +2 -1
- package/components/Resource/Detail/Card/Scaler.vue +4 -4
- package/components/Tabbed/index.vue +6 -0
- package/components/__tests__/ConsumptionGauge.test.ts +31 -0
- package/components/form/ProjectMemberEditor.vue +0 -10
- package/components/nav/TopLevelMenu.helper.ts +7 -79
- package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
- package/config/private-label.js +2 -1
- package/config/product/apps.js +1 -0
- package/core/__tests__/extension-manager-impl.test.js +187 -2
- package/core/extension-manager-impl.js +4 -2
- package/core/plugin-helpers.ts +31 -0
- package/detail/__tests__/node.test.ts +83 -0
- package/detail/management.cattle.io.oidcclient.vue +2 -1
- package/detail/node.vue +1 -0
- package/edit/catalog.cattle.io.clusterrepo.vue +17 -3
- package/edit/cloudcredential.vue +2 -1
- package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -6
- package/edit/provisioning.cattle.io.cluster/index.vue +5 -4
- package/edit/provisioning.cattle.io.cluster/shared.ts +4 -2
- package/edit/secret/generic.vue +1 -0
- package/edit/secret/index.vue +2 -1
- package/edit/service.vue +2 -14
- package/list/management.cattle.io.feature.vue +7 -1
- package/list/provisioning.cattle.io.cluster.vue +0 -49
- package/mixins/brand.js +2 -1
- package/models/catalog.cattle.io.clusterrepo.js +9 -0
- package/models/cluster.x-k8s.io.machinedeployment.js +8 -3
- package/models/management.cattle.io.authconfig.js +2 -1
- package/models/management.cattle.io.cluster.js +4 -3
- package/models/monitoring.coreos.com.receiver.js +11 -6
- package/models/provisioning.cattle.io.cluster.js +2 -2
- package/package.json +5 -5
- package/pages/c/_cluster/apps/charts/index.vue +3 -8
- package/pages/c/_cluster/apps/charts/install.vue +8 -9
- package/pages/c/_cluster/istio/index.vue +4 -2
- package/pages/c/_cluster/longhorn/index.vue +2 -1
- package/pages/c/_cluster/monitoring/index.vue +2 -2
- package/pages/c/_cluster/neuvector/index.vue +2 -1
- package/pages/c/_cluster/settings/performance.vue +0 -5
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
- package/pages/c/_cluster/uiplugins/index.vue +2 -1
- package/plugins/steve/steve-pagination-utils.ts +1 -2
- package/plugins/steve/subscribe.js +29 -4
- package/rancher-components/RcButton/RcButton.vue +3 -3
- package/rancher-components/RcButtonSplit/RcButtonSplit.test.ts +253 -0
- package/rancher-components/RcButtonSplit/RcButtonSplit.vue +158 -0
- package/rancher-components/RcButtonSplit/index.ts +1 -0
- package/scripts/test-plugins-build.sh +4 -4
- package/types/shell/index.d.ts +1 -0
- package/utils/__tests__/require-asset.test.ts +98 -0
- package/utils/async.ts +1 -5
- package/utils/brand.ts +3 -1
- package/utils/favicon.js +4 -3
- package/utils/require-asset.ts +95 -0
- package/vue.config.js +4 -3
- package/components/HarvesterServiceAddOnConfig.vue +0 -207
|
@@ -107,7 +107,6 @@ export interface TopLevelMenuHelper {
|
|
|
107
107
|
|
|
108
108
|
export abstract class BaseTopLevelMenuHelper {
|
|
109
109
|
protected $store: VuexStore;
|
|
110
|
-
protected hasProvCluster: boolean;
|
|
111
110
|
|
|
112
111
|
/**
|
|
113
112
|
* Filter mgmt clusters by
|
|
@@ -143,11 +142,9 @@ export abstract class BaseTopLevelMenuHelper {
|
|
|
143
142
|
$store: VuexStore,
|
|
144
143
|
}) {
|
|
145
144
|
this.$store = $store;
|
|
146
|
-
|
|
147
|
-
this.hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
|
|
148
145
|
}
|
|
149
146
|
|
|
150
|
-
protected convertToCluster(mgmtCluster: MgmtCluster, provCluster
|
|
147
|
+
protected convertToCluster(mgmtCluster: MgmtCluster, provCluster?: ProvCluster): TopLevelMenuCluster {
|
|
151
148
|
return {
|
|
152
149
|
id: mgmtCluster.id,
|
|
153
150
|
label: mgmtCluster.nameDisplay,
|
|
@@ -173,7 +170,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
173
170
|
|
|
174
171
|
private clustersPinnedWrapper: PaginationWrapper<any>;
|
|
175
172
|
private clustersOthersWrapper: PaginationWrapper<any>;
|
|
176
|
-
private provClusterWrapper: PaginationWrapper<any>;
|
|
177
173
|
|
|
178
174
|
private clusterCount = 0;
|
|
179
175
|
|
|
@@ -223,43 +219,10 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
223
219
|
},
|
|
224
220
|
formatResponse: { classify: true },
|
|
225
221
|
});
|
|
226
|
-
// Fetch all prov clusters for the mgmt clusters we have
|
|
227
|
-
this.provClusterWrapper = new PaginationWrapper({
|
|
228
|
-
$store,
|
|
229
|
-
id: 'top-level-menu-prov-clusters',
|
|
230
|
-
onChange: async({ forceWatch, revision }) => {
|
|
231
|
-
if (!this.args) {
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
try {
|
|
235
|
-
await this.update({
|
|
236
|
-
...this.args,
|
|
237
|
-
forceWatch,
|
|
238
|
-
provClusterRevision: revision,
|
|
239
|
-
});
|
|
240
|
-
} catch {
|
|
241
|
-
// Failures should be logged lower down, not much we can do here except catch to prevent whole ui page warnings in dev mode
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
enabledFor: {
|
|
245
|
-
store: STORE.MANAGEMENT,
|
|
246
|
-
resource: {
|
|
247
|
-
id: CAPI.RANCHER_CLUSTER,
|
|
248
|
-
context: 'side-bar',
|
|
249
|
-
}
|
|
250
|
-
},
|
|
251
|
-
formatResponse: { classify: true }
|
|
252
|
-
});
|
|
253
222
|
}
|
|
254
223
|
|
|
255
224
|
// ---------- requests ----------
|
|
256
225
|
async update(args: UpdateArgs) {
|
|
257
|
-
if (!this.hasProvCluster) {
|
|
258
|
-
// We're filtering out mgmt clusters without prov clusters, so if the user can't see any prov clusters at all
|
|
259
|
-
// exit early
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
226
|
this.args = args;
|
|
264
227
|
const promises = {
|
|
265
228
|
pinned: this.updatePinned(args),
|
|
@@ -271,22 +234,11 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
271
234
|
notPinned: MgmtCluster[]
|
|
272
235
|
} = await allHash(promises) as any;
|
|
273
236
|
|
|
274
|
-
const provClusters = await this.updateProvCluster(res.notPinned, res.pinned, args);
|
|
275
|
-
const provClustersByMgmtId = provClusters.reduce((res: { [mgmtId: string]: ProvCluster}, provCluster: ProvCluster) => {
|
|
276
|
-
if (provCluster.mgmtClusterId) {
|
|
277
|
-
res[provCluster.mgmtClusterId] = provCluster;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return res;
|
|
281
|
-
}, {} as { [mgmtId: string]: ProvCluster});
|
|
282
|
-
|
|
283
237
|
// Filter out mgmt clusters that don't have matching prov cluster and convert remaining to required format
|
|
284
238
|
const _clustersNotPinned = res.notPinned
|
|
285
|
-
.
|
|
286
|
-
.map((mgmtCluster) => this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
|
|
239
|
+
.map((mgmtCluster) => this.convertToCluster(mgmtCluster));
|
|
287
240
|
const _clustersPinned = res.pinned
|
|
288
|
-
.
|
|
289
|
-
.map((mgmtCluster) => this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
|
|
241
|
+
.map((mgmtCluster) => this.convertToCluster(mgmtCluster));
|
|
290
242
|
|
|
291
243
|
this.clustersPinned.length = 0;
|
|
292
244
|
this.clustersOthers.length = 0;
|
|
@@ -298,7 +250,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
298
250
|
async destroy() {
|
|
299
251
|
this.clustersPinnedWrapper.onDestroy();
|
|
300
252
|
this.clustersOthersWrapper.onDestroy();
|
|
301
|
-
this.provClusterWrapper.onDestroy();
|
|
302
253
|
}
|
|
303
254
|
|
|
304
255
|
/**
|
|
@@ -445,41 +396,21 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
445
396
|
console.warn('Unable to set saved count for clusters', err); // eslint-disable-line no-console
|
|
446
397
|
}
|
|
447
398
|
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Find all provisioning clusters associated with the displayed mgmt clusters
|
|
451
|
-
*/
|
|
452
|
-
private async updateProvCluster(notPinned: MgmtCluster[], pinned: MgmtCluster[], args: UpdateArgs): Promise<ProvCluster[]> {
|
|
453
|
-
return this.provClusterWrapper.request({
|
|
454
|
-
forceWatch: args.forceWatch,
|
|
455
|
-
pagination: {
|
|
456
|
-
filters: [
|
|
457
|
-
PaginationParamFilter.createMultipleFields(
|
|
458
|
-
[...notPinned, ...pinned]
|
|
459
|
-
.map((mgmtCluster) => ({
|
|
460
|
-
field: 'status.clusterName', value: mgmtCluster.id, equals: true, exact: true
|
|
461
|
-
}))
|
|
462
|
-
)
|
|
463
|
-
],
|
|
464
|
-
page: 1,
|
|
465
|
-
sort: [],
|
|
466
|
-
projectsOrNamespaces: []
|
|
467
|
-
},
|
|
468
|
-
revision: args.provClusterRevision
|
|
469
|
-
})
|
|
470
|
-
.then((r) => r.data);
|
|
471
|
-
}
|
|
472
399
|
}
|
|
473
400
|
|
|
474
401
|
/**
|
|
475
402
|
* Helper designed to supply non-paginated results for the top level menu cluster resources
|
|
476
403
|
*/
|
|
477
404
|
export class TopLevelMenuHelperLegacy extends BaseTopLevelMenuHelper implements TopLevelMenuHelper {
|
|
405
|
+
protected hasProvCluster: boolean;
|
|
406
|
+
|
|
478
407
|
constructor({ $store }: {
|
|
479
408
|
$store: VuexStore,
|
|
480
409
|
}) {
|
|
481
410
|
super({ $store });
|
|
482
411
|
|
|
412
|
+
this.hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
|
|
413
|
+
|
|
483
414
|
if (this.hasProvCluster) {
|
|
484
415
|
$store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER });
|
|
485
416
|
}
|
|
@@ -664,9 +595,6 @@ class TopLevelMenuHelperService {
|
|
|
664
595
|
const canPagination = $store.getters[`management/paginationEnabled`]({
|
|
665
596
|
id: MANAGEMENT.CLUSTER,
|
|
666
597
|
context: 'side-bar',
|
|
667
|
-
}) && $store.getters[`management/paginationEnabled`]({
|
|
668
|
-
id: CAPI.RANCHER_CLUSTER,
|
|
669
|
-
context: 'side-bar',
|
|
670
598
|
});
|
|
671
599
|
|
|
672
600
|
this._helper = canPagination ? new TopLevelMenuHelperPagination({ $store }) : new TopLevelMenuHelperLegacy({ $store });
|
|
@@ -102,7 +102,7 @@ describe('topLevelMenu.helper', () => {
|
|
|
102
102
|
it('should initialize PaginationWrappers', () => {
|
|
103
103
|
mockStore.getters['management/schemaFor'].mockReturnValue(true);
|
|
104
104
|
new TopLevelMenuHelperPagination({ $store: mockStore });
|
|
105
|
-
expect(PaginationWrapper).toHaveBeenCalledTimes(
|
|
105
|
+
expect(PaginationWrapper).toHaveBeenCalledTimes(2);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
it('should update clusters correctly', async() => {
|
|
@@ -113,19 +113,13 @@ describe('topLevelMenu.helper', () => {
|
|
|
113
113
|
const mgmtOthers = [{
|
|
114
114
|
id: 'c2', nameDisplay: 'Other', isReady: true, pinned: false, pin: jest.fn(), unpin: jest.fn()
|
|
115
115
|
}];
|
|
116
|
-
const provClusters = [
|
|
117
|
-
{ mgmtClusterId: 'c1' },
|
|
118
|
-
{ mgmtClusterId: 'c2' }
|
|
119
|
-
];
|
|
120
116
|
|
|
121
117
|
const mockRequestPinned = jest.fn().mockResolvedValue({ data: mgmtPinned });
|
|
122
118
|
const mockRequestOthers = jest.fn().mockResolvedValue({ data: mgmtOthers });
|
|
123
|
-
const mockRequestProv = jest.fn().mockResolvedValue({ data: provClusters });
|
|
124
119
|
|
|
125
120
|
(PaginationWrapper as unknown as jest.Mock)
|
|
126
121
|
.mockImplementationOnce(() => ({ request: mockRequestPinned, onDestroy: jest.fn() }))
|
|
127
|
-
.mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }))
|
|
128
|
-
.mockImplementationOnce(() => ({ request: mockRequestProv, onDestroy: jest.fn() }));
|
|
122
|
+
.mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }));
|
|
129
123
|
|
|
130
124
|
const helper = new TopLevelMenuHelperPagination({ $store: mockStore });
|
|
131
125
|
|
|
@@ -169,57 +163,12 @@ describe('topLevelMenu.helper', () => {
|
|
|
169
163
|
},
|
|
170
164
|
revision: undefined
|
|
171
165
|
});
|
|
172
|
-
expect(mockRequestProv).toHaveBeenCalledWith({
|
|
173
|
-
forceWatch: undefined,
|
|
174
|
-
pagination: {
|
|
175
|
-
filters: [{
|
|
176
|
-
equals: true,
|
|
177
|
-
fields: [{
|
|
178
|
-
equals: true, exact: true, field: 'status.clusterName', value: mgmtOthers[0].id
|
|
179
|
-
}, {
|
|
180
|
-
equals: true, exact: true, field: 'status.clusterName', value: mgmtPinned[0].id
|
|
181
|
-
}],
|
|
182
|
-
param: 'filter'
|
|
183
|
-
}],
|
|
184
|
-
page: 1,
|
|
185
|
-
projectsOrNamespaces: [],
|
|
186
|
-
sort: []
|
|
187
|
-
},
|
|
188
|
-
revision: undefined
|
|
189
|
-
});
|
|
190
166
|
|
|
191
167
|
expect(helper.clustersPinned).toHaveLength(1);
|
|
192
168
|
expect(helper.clustersPinned[0].id).toBe('c1');
|
|
193
169
|
expect(helper.clustersOthers).toHaveLength(1);
|
|
194
170
|
expect(helper.clustersOthers[0].id).toBe('c2');
|
|
195
171
|
});
|
|
196
|
-
|
|
197
|
-
it('should filter out mgmt clusters without matching prov clusters', async() => {
|
|
198
|
-
mockStore.getters['management/schemaFor'].mockReturnValue(true);
|
|
199
|
-
const mgmtOthers = [{
|
|
200
|
-
id: 'c2', nameDisplay: 'Other', isReady: true, pinned: false, pin: jest.fn(), unpin: jest.fn()
|
|
201
|
-
}];
|
|
202
|
-
// No prov cluster for c2
|
|
203
|
-
const provClusters: any[] = [];
|
|
204
|
-
|
|
205
|
-
const mockRequestPinned = jest.fn().mockResolvedValue({ data: [] });
|
|
206
|
-
const mockRequestOthers = jest.fn().mockResolvedValue({ data: mgmtOthers });
|
|
207
|
-
const mockRequestProv = jest.fn().mockResolvedValue({ data: provClusters });
|
|
208
|
-
|
|
209
|
-
(PaginationWrapper as unknown as jest.Mock)
|
|
210
|
-
.mockImplementationOnce(() => ({ request: mockRequestPinned, onDestroy: jest.fn() }))
|
|
211
|
-
.mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }))
|
|
212
|
-
.mockImplementationOnce(() => ({ request: mockRequestProv, onDestroy: jest.fn() }));
|
|
213
|
-
|
|
214
|
-
const helper = new TopLevelMenuHelperPagination({ $store: mockStore });
|
|
215
|
-
|
|
216
|
-
await helper.update({
|
|
217
|
-
searchTerm: '',
|
|
218
|
-
pinnedIds: [],
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
expect(helper.clustersOthers).toHaveLength(0);
|
|
222
|
-
});
|
|
223
172
|
});
|
|
224
173
|
|
|
225
174
|
describe('class: TopLevelMenuHelperService', () => {
|
package/config/private-label.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SETTING } from './settings';
|
|
2
2
|
import { CURRENT_RANCHER_VERSION } from './version';
|
|
3
|
+
import { requireAsset } from '@shell/utils/require-asset';
|
|
3
4
|
|
|
4
5
|
export const ANY = 0;
|
|
5
6
|
export const STANDARD = 1;
|
|
@@ -78,7 +79,7 @@ export function setTitle() {
|
|
|
78
79
|
const v = getVendor();
|
|
79
80
|
|
|
80
81
|
if (v === 'Harvester') {
|
|
81
|
-
const ico =
|
|
82
|
+
const ico = requireAsset(`~shell/assets/images/pl/harvester.png`);
|
|
82
83
|
|
|
83
84
|
document.title = 'Harvester';
|
|
84
85
|
const link = document.createElement('link');
|
package/config/product/apps.js
CHANGED
|
@@ -53,6 +53,7 @@ export function init(store) {
|
|
|
53
53
|
|
|
54
54
|
configureType(CATALOG.APP, { isCreatable: false, isEditable: false });
|
|
55
55
|
configureType(CATALOG.OPERATION, { isCreatable: false, isEditable: false });
|
|
56
|
+
configureType(CATALOG.CLUSTER_REPO, { listCreateButtonLabelKey: 'catalog.repo.add' });
|
|
56
57
|
|
|
57
58
|
const repoType = {
|
|
58
59
|
name: 'type',
|
|
@@ -38,8 +38,30 @@ jest.mock('@shell/core/plugin', () => {
|
|
|
38
38
|
};
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
// Mock ExtensionPoint
|
|
42
|
-
|
|
41
|
+
// Mock ExtensionPoint — only EDIT_YAML, simulating an older dashboard (e.g. 2.13)
|
|
42
|
+
// that does not know about newer ExtensionPoints like 'Table'.
|
|
43
|
+
// The ensureUIConfigCompat tests override this to include TABLE for plugin-helpers,
|
|
44
|
+
// simulating an extension shipping a newer shell.
|
|
45
|
+
jest.mock('@shell/core/types', () => ({
|
|
46
|
+
ExtensionPoint: { EDIT_YAML: 'edit-yaml' },
|
|
47
|
+
ActionLocation: { TABLE: 'table-action' },
|
|
48
|
+
CardLocation: {},
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
jest.mock('@shell/utils/platform', () => ({ isMac: false }));
|
|
52
|
+
jest.mock('@shell/utils/string', () => ({
|
|
53
|
+
ucFirst: jest.fn((s) => s),
|
|
54
|
+
randomStr: jest.fn(() => 'abc123'),
|
|
55
|
+
}));
|
|
56
|
+
jest.mock('@shell/config/query-params', () => ({
|
|
57
|
+
_EDIT: 'edit',
|
|
58
|
+
_CONFIG: 'config',
|
|
59
|
+
_DETAIL: 'detail',
|
|
60
|
+
_LIST: 'list',
|
|
61
|
+
_CREATE: 'create',
|
|
62
|
+
}));
|
|
63
|
+
jest.mock('@shell/utils/router', () => ({ getProductFromRoute: jest.fn() }));
|
|
64
|
+
jest.mock('@shell/utils/object', () => ({ isEqual: jest.fn() }));
|
|
43
65
|
|
|
44
66
|
// Mock PluginRoutes
|
|
45
67
|
jest.mock('@shell/core/plugin-routes', () => {
|
|
@@ -299,6 +321,28 @@ describe('extension Manager', () => {
|
|
|
299
321
|
delete window[pluginId];
|
|
300
322
|
});
|
|
301
323
|
|
|
324
|
+
it('surfaces the plugin id and original error message when initialization fails', async() => {
|
|
325
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
326
|
+
const pluginId = 'surface-error-plugin';
|
|
327
|
+
const originalError = new Error('something went wrong');
|
|
328
|
+
const mockPluginInit = jest.fn().mockImplementation(() => {
|
|
329
|
+
throw originalError;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
window[pluginId] = { default: mockPluginInit };
|
|
333
|
+
|
|
334
|
+
const loadPromise = manager.loadAsync(pluginId, 'test.js');
|
|
335
|
+
const script = document.head.querySelector(`script[id="${ pluginId }"]`);
|
|
336
|
+
|
|
337
|
+
script.onload();
|
|
338
|
+
|
|
339
|
+
await expect(loadPromise).rejects.toThrow(`Could not initialize plugin ${ pluginId } - ${ originalError.message }`);
|
|
340
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(`Could not initialize plugin ${ pluginId }`, originalError);
|
|
341
|
+
|
|
342
|
+
consoleErrorSpy.mockRestore();
|
|
343
|
+
delete window[pluginId];
|
|
344
|
+
});
|
|
345
|
+
|
|
302
346
|
it('rejects if script load fails', async() => {
|
|
303
347
|
const pluginId = 'fail-plugin';
|
|
304
348
|
const loadPromise = manager.loadAsync(pluginId, 'bad-url.js');
|
|
@@ -433,5 +477,146 @@ describe('extension Manager', () => {
|
|
|
433
477
|
|
|
434
478
|
expect(config).toStrictEqual([]);
|
|
435
479
|
});
|
|
480
|
+
|
|
481
|
+
it('returns empty array for non-existent type', () => {
|
|
482
|
+
// Simulates an extension using a newer ExtensionPoint (e.g. 'Table')
|
|
483
|
+
// that the running dashboard does not have in its uiConfig
|
|
484
|
+
const config = manager.getUIConfig('unknown-type', 'some-area');
|
|
485
|
+
|
|
486
|
+
expect(config).toStrictEqual([]);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe('ensureUIConfigCompat (via plugin-helpers)', () => {
|
|
491
|
+
// These tests verify that getApplicableExtensionEnhancements tracks missing
|
|
492
|
+
// ExtensionPoint keys and exits early when accessing them.
|
|
493
|
+
// This is the forwards-compatibility mechanism for extensions running on
|
|
494
|
+
// older dashboards that don't know about newer ExtensionPoints (e.g. 'Table').
|
|
495
|
+
|
|
496
|
+
const mockRoute = {
|
|
497
|
+
name: 'test',
|
|
498
|
+
params: {},
|
|
499
|
+
query: {},
|
|
500
|
+
meta: {},
|
|
501
|
+
hash: '',
|
|
502
|
+
path: '/test',
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Each test needs a fresh plugin-helpers module to reset the _uiConfigPatched tracking.
|
|
506
|
+
// The extension-manager is created with the "old" ExtensionPoint (only EDIT_YAML),
|
|
507
|
+
// then plugin-helpers is re-required with an "upgraded" ExtensionPoint that includes TABLE,
|
|
508
|
+
// simulating an extension built with a newer shell running on an older dashboard.
|
|
509
|
+
let freshGetApplicable;
|
|
510
|
+
let freshManager;
|
|
511
|
+
|
|
512
|
+
beforeEach(() => {
|
|
513
|
+
freshManager = createExtensionManager(context);
|
|
514
|
+
|
|
515
|
+
// Reset modules so plugin-helpers._uiConfigPatched starts as empty object
|
|
516
|
+
jest.resetModules();
|
|
517
|
+
|
|
518
|
+
// Override the types mock so plugin-helpers sees a newer ExtensionPoint with TABLE
|
|
519
|
+
jest.doMock('@shell/core/types', () => ({
|
|
520
|
+
ExtensionPoint: {
|
|
521
|
+
EDIT_YAML: 'edit-yaml',
|
|
522
|
+
TABLE: 'Table',
|
|
523
|
+
},
|
|
524
|
+
ActionLocation: { TABLE: 'table-action' },
|
|
525
|
+
CardLocation: {},
|
|
526
|
+
}));
|
|
527
|
+
|
|
528
|
+
freshGetApplicable = require('@shell/core/plugin-helpers').getApplicableExtensionEnhancements;
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('returns empty array for missing ExtensionPoint and does not mutate uiConfig', () => {
|
|
532
|
+
// Verify 'Table' is NOT in uiConfig before the call
|
|
533
|
+
const uiConfig = freshManager.getAllUIConfig();
|
|
534
|
+
|
|
535
|
+
expect(uiConfig['Table']).toBeUndefined();
|
|
536
|
+
|
|
537
|
+
// Call getApplicableExtensionEnhancements which triggers ensureUIConfigCompat
|
|
538
|
+
const result = freshGetApplicable({ $extension: freshManager }, 'Table', 'some-area', mockRoute);
|
|
539
|
+
|
|
540
|
+
// Should return empty array for missing extension point
|
|
541
|
+
expect(result).toStrictEqual([]);
|
|
542
|
+
|
|
543
|
+
// uiConfig should NOT be mutated - 'Table' should remain undefined
|
|
544
|
+
expect(uiConfig['Table']).toBeUndefined();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('does not affect existing ExtensionPoint keys', () => {
|
|
548
|
+
const mockAction = { label: 'Test', locationConfig: {} };
|
|
549
|
+
const plugin = {
|
|
550
|
+
types: {},
|
|
551
|
+
uiConfig: { 'edit-yaml': { header: [mockAction] } },
|
|
552
|
+
l10n: {},
|
|
553
|
+
modelExtensions: {},
|
|
554
|
+
stores: [],
|
|
555
|
+
locales: [],
|
|
556
|
+
routes: [],
|
|
557
|
+
validators: {},
|
|
558
|
+
productNames: [],
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
freshManager.applyPlugin(plugin);
|
|
562
|
+
|
|
563
|
+
const result = freshGetApplicable({ $extension: freshManager }, 'edit-yaml', 'header', mockRoute);
|
|
564
|
+
|
|
565
|
+
// Should return the actual action
|
|
566
|
+
expect(result).toHaveLength(1);
|
|
567
|
+
expect(result[0].label).toBe('Test');
|
|
568
|
+
|
|
569
|
+
// Existing config should be untouched
|
|
570
|
+
const uiConfig = freshManager.getAllUIConfig();
|
|
571
|
+
|
|
572
|
+
expect(uiConfig['edit-yaml'].header).toHaveLength(1);
|
|
573
|
+
expect(uiConfig['edit-yaml'].header[0]).toBe(mockAction);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('logs a warning for missing ExtensionPoints', () => {
|
|
577
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
578
|
+
|
|
579
|
+
freshGetApplicable({ $extension: freshManager }, 'Table', 'some-area', mockRoute);
|
|
580
|
+
|
|
581
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
582
|
+
expect.stringContaining('Table')
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
consoleWarnSpy.mockRestore();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('dashboard-side getUIConfig calls remain safe with optional chaining', () => {
|
|
589
|
+
// getUIConfig('Table', ...) should not crash even though 'Table' is missing
|
|
590
|
+
// Returns empty array due to optional chaining in extension-manager
|
|
591
|
+
expect(freshManager.getUIConfig('Table', 'some-area')).toStrictEqual([]);
|
|
592
|
+
|
|
593
|
+
// Trigger ensureUIConfigCompat via getApplicableExtensionEnhancements
|
|
594
|
+
const result = freshGetApplicable({ $extension: freshManager }, 'Table', 'some-area', mockRoute);
|
|
595
|
+
|
|
596
|
+
// getApplicableExtensionEnhancements exits early and returns empty array
|
|
597
|
+
expect(result).toStrictEqual([]);
|
|
598
|
+
|
|
599
|
+
// uiConfig is NOT mutated - 'Table' key still doesn't exist
|
|
600
|
+
const uiConfig = freshManager.getAllUIConfig();
|
|
601
|
+
|
|
602
|
+
expect(uiConfig['Table']).toBeUndefined();
|
|
603
|
+
|
|
604
|
+
// Direct getUIConfig calls still work safely (optional chaining)
|
|
605
|
+
expect(freshManager.getUIConfig('Table', 'some-area')).toStrictEqual([]);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('only logs warning once for each missing ExtensionPoint', () => {
|
|
609
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
610
|
+
|
|
611
|
+
// First call logs warning and tracks 'Table' as missing
|
|
612
|
+
freshGetApplicable({ $extension: freshManager }, 'Table', 'some-area', mockRoute);
|
|
613
|
+
// Second call should not log warning again (already tracked)
|
|
614
|
+
freshGetApplicable({ $extension: freshManager }, 'Table', 'some-area', mockRoute);
|
|
615
|
+
|
|
616
|
+
// Warning should only be logged once
|
|
617
|
+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
|
618
|
+
|
|
619
|
+
consoleWarnSpy.mockRestore();
|
|
620
|
+
});
|
|
436
621
|
});
|
|
437
622
|
});
|
|
@@ -124,7 +124,9 @@ export const createExtensionManager = (context) => {
|
|
|
124
124
|
} catch (e) {
|
|
125
125
|
delete plugins[id];
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
console.error(`Could not initialize plugin ${ id }`, e); // eslint-disable-line no-console
|
|
128
|
+
|
|
129
|
+
return reject(new Error(`Could not initialize plugin ${ id } - ${ e?.message }`));
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
// Load all of the types etc from the plugin
|
|
@@ -426,7 +428,7 @@ export const createExtensionManager = (context) => {
|
|
|
426
428
|
* Return the UI configuration for the given type and location
|
|
427
429
|
*/
|
|
428
430
|
getUIConfig(type, uiArea) {
|
|
429
|
-
return uiConfig[type][uiArea] || [];
|
|
431
|
+
return uiConfig[type]?.[uiArea] || [];
|
|
430
432
|
},
|
|
431
433
|
|
|
432
434
|
/**
|
package/core/plugin-helpers.ts
CHANGED
|
@@ -137,6 +137,30 @@ function checkExtensionRouteBinding($route: any, locationConfig: any, context: a
|
|
|
137
137
|
return res;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// Track which ExtensionPoint keys are missing from the extension manager's uiConfig.
|
|
141
|
+
// This handles forwards-compatibility when extensions ship a newer shell that defines
|
|
142
|
+
// ExtensionPoint values the running dashboard doesn't know about (e.g. 'Table' on 2.13).
|
|
143
|
+
const _uiConfigPatched: { [point: string]: boolean } = {};
|
|
144
|
+
|
|
145
|
+
function ensureUIConfigCompat(extensionManager: any) {
|
|
146
|
+
const uiConfig = extensionManager.getAllUIConfig?.();
|
|
147
|
+
|
|
148
|
+
if (uiConfig) {
|
|
149
|
+
const missingPoints: { [point: string]: boolean } = {};
|
|
150
|
+
|
|
151
|
+
Object.values(ExtensionPoint).forEach((ep) => {
|
|
152
|
+
if (!uiConfig[ep] && !_uiConfigPatched[ep]) {
|
|
153
|
+
missingPoints[ep] = true;
|
|
154
|
+
_uiConfigPatched[ep] = true;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (Object.keys(missingPoints).length) {
|
|
159
|
+
console.warn(`[plugin-helpers] These ExtensionPoints aren't available for usage in this Rancher version: ${ Object.keys(missingPoints).join(', ') }`); // eslint-disable-line no-console
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
140
164
|
export function getApplicableExtensionEnhancements<T>(
|
|
141
165
|
pluginCtx: ComponentOptionsMixin,
|
|
142
166
|
actionType: ExtensionPoint,
|
|
@@ -148,6 +172,13 @@ export function getApplicableExtensionEnhancements<T>(
|
|
|
148
172
|
|
|
149
173
|
// gate it so that we prevent errors on older versions of dashboard
|
|
150
174
|
if (pluginCtx.$extension?.getUIConfig) {
|
|
175
|
+
ensureUIConfigCompat(pluginCtx.$extension);
|
|
176
|
+
|
|
177
|
+
// Exit early if actionType doesn't exist in the extension manager's uiConfig
|
|
178
|
+
if (_uiConfigPatched[actionType]) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
151
182
|
const actions = pluginCtx.$extension.getUIConfig(actionType, uiArea);
|
|
152
183
|
|
|
153
184
|
actions.forEach((action: any, i: number) => {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import node from '@shell/detail/node.vue';
|
|
3
|
+
import ConsumptionGauge from '@shell/components/ConsumptionGauge.vue';
|
|
4
|
+
|
|
5
|
+
describe('view: node detail', () => {
|
|
6
|
+
const mockStore = {
|
|
7
|
+
getters: {
|
|
8
|
+
'cluster/schemaFor': () => undefined,
|
|
9
|
+
'cluster/paginationEnabled': () => false,
|
|
10
|
+
'type-map/headersFor': jest.fn(),
|
|
11
|
+
'i18n/t': (key: string) => key,
|
|
12
|
+
currentCluster: { id: 'local' },
|
|
13
|
+
},
|
|
14
|
+
dispatch: jest.fn(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const mocks = {
|
|
18
|
+
$store: mockStore,
|
|
19
|
+
$fetchState: { pending: false },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const defaultNodeValue = {
|
|
23
|
+
metadata: { name: 'test-node' },
|
|
24
|
+
status: {
|
|
25
|
+
nodeInfo: {}, images: [], conditions: []
|
|
26
|
+
},
|
|
27
|
+
spec: { taints: [] },
|
|
28
|
+
pods: [],
|
|
29
|
+
cpuCapacity: 4,
|
|
30
|
+
cpuUsage: 2,
|
|
31
|
+
ramReserved: 8000,
|
|
32
|
+
ramUsage: 4000,
|
|
33
|
+
podCapacity: 110,
|
|
34
|
+
podConsumed: 5,
|
|
35
|
+
isPidPressureOk: true,
|
|
36
|
+
isDiskPressureOk: true,
|
|
37
|
+
isMemoryPressureOk: true,
|
|
38
|
+
isKubeletOk: true,
|
|
39
|
+
internalIp: '10.0.0.1',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function createWrapper(nodeOverrides = {}) {
|
|
43
|
+
return shallowMount(node, {
|
|
44
|
+
props: { value: { ...defaultNodeValue, ...nodeOverrides } },
|
|
45
|
+
global: { mocks },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findGaugeByResource(wrapper: ReturnType<typeof createWrapper>, resourceKey: string) {
|
|
50
|
+
const gauges = wrapper.findAllComponents(ConsumptionGauge);
|
|
51
|
+
|
|
52
|
+
return gauges.find(
|
|
53
|
+
(g) => g.attributes('resourcename')?.includes(resourceKey)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
it('should pass the "running" translation key as usedlabel to the pods ConsumptionGauge', () => {
|
|
58
|
+
const wrapper = createWrapper();
|
|
59
|
+
|
|
60
|
+
const podsGauge = findGaugeByResource(wrapper, 'consumptionGauge.pods');
|
|
61
|
+
|
|
62
|
+
expect(podsGauge).toBeDefined();
|
|
63
|
+
expect(podsGauge!.attributes('usedlabel')).toContain('consumptionGauge.running');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should NOT pass a usedlabel to the CPU ConsumptionGauge', () => {
|
|
67
|
+
const wrapper = createWrapper();
|
|
68
|
+
|
|
69
|
+
const cpuGauge = findGaugeByResource(wrapper, 'consumptionGauge.cpu');
|
|
70
|
+
|
|
71
|
+
expect(cpuGauge).toBeDefined();
|
|
72
|
+
expect(cpuGauge!.attributes('usedlabel')).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should NOT pass a usedlabel to the Memory ConsumptionGauge', () => {
|
|
76
|
+
const wrapper = createWrapper();
|
|
77
|
+
|
|
78
|
+
const memoryGauge = findGaugeByResource(wrapper, 'consumptionGauge.memory');
|
|
79
|
+
|
|
80
|
+
expect(memoryGauge).toBeDefined();
|
|
81
|
+
expect(memoryGauge!.attributes('usedlabel')).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -9,6 +9,7 @@ import DateComponent from '@shell/components/formatter/Date.vue';
|
|
|
9
9
|
import { RcItemCard } from '@components/RcItemCard';
|
|
10
10
|
import ActionMenu, { type ActionMenuSelection } from '@shell/components/ActionMenuShell.vue';
|
|
11
11
|
import { Banner } from '@components/Banner';
|
|
12
|
+
import keySvg from '~shell/assets/images/key.svg';
|
|
12
13
|
|
|
13
14
|
type SecretActionType = 'create-secret' | 'regen-secret' | 'remove-secret'
|
|
14
15
|
interface ClientSecretData { createdAt: string, lastUsedAt: string, lastFiveCharacters: string }
|
|
@@ -228,7 +229,7 @@ export default defineComponent({
|
|
|
228
229
|
clientSecrets.push({
|
|
229
230
|
id: oidcSecretDataKey,
|
|
230
231
|
header: { title: { text: oidcSecretDataKey } },
|
|
231
|
-
image: { src:
|
|
232
|
+
image: { src: keySvg },
|
|
232
233
|
createdAt,
|
|
233
234
|
lastFiveCharacters: oidcSecretData.lastFiveCharacters,
|
|
234
235
|
lastUsedAt,
|
package/detail/node.vue
CHANGED
|
@@ -234,6 +234,7 @@ export default {
|
|
|
234
234
|
:resource-name="t('node.detail.glance.consumptionGauge.pods')"
|
|
235
235
|
:capacity="value.podCapacity"
|
|
236
236
|
:used="value.podConsumed"
|
|
237
|
+
:used-label="t('node.detail.glance.consumptionGauge.running')"
|
|
237
238
|
/>
|
|
238
239
|
</div>
|
|
239
240
|
<div class="spacer" />
|