@rancher/shell 3.0.10 → 3.0.12-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.
- package/assets/styles/base/_mixins.scss +31 -0
- package/assets/styles/base/_variables.scss +2 -0
- package/assets/styles/themes/_modern.scss +6 -5
- package/assets/translations/en-us.yaml +12 -9
- package/assets/translations/zh-hans.yaml +0 -3
- 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/EmptyProductPage.vue +76 -0
- package/components/ExplorerProjectsNamespaces.vue +1 -4
- package/components/LazyImage.vue +2 -1
- package/components/Resource/Detail/Card/Scaler.vue +4 -4
- package/components/Resource/Detail/CopyToClipboard.vue +1 -2
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
- package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
- package/components/Resource/Detail/TitleBar/index.vue +1 -1
- package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
- package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
- package/components/Resource/Detail/ViewOptions/index.vue +2 -1
- package/components/ResourceList/Masthead.vue +25 -2
- package/components/SideNav.vue +13 -0
- package/components/Tabbed/index.vue +6 -0
- package/components/__tests__/ConsumptionGauge.test.ts +31 -0
- package/components/__tests__/PromptModal.test.ts +2 -0
- package/components/fleet/FleetClusters.vue +1 -0
- package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
- package/components/form/NodeScheduling.vue +17 -3
- package/components/form/PrivateRegistry.vue +69 -0
- package/components/form/ProjectMemberEditor.vue +0 -10
- package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
- package/components/formatter/WorkloadHealthScale.vue +3 -1
- package/components/nav/Group.vue +26 -3
- package/components/nav/Header.vue +32 -7
- package/components/nav/TopLevelMenu.helper.ts +7 -79
- package/components/nav/TopLevelMenu.vue +15 -1
- package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
- package/config/pagination-table-headers.js +8 -1
- package/config/private-label.js +2 -1
- package/config/product/apps.js +3 -1
- package/config/product/auth.js +1 -0
- package/config/product/backup.js +1 -0
- package/config/product/compliance.js +1 -1
- package/config/product/explorer.js +25 -6
- package/config/product/fleet.js +1 -0
- package/config/product/gatekeeper.js +1 -0
- package/config/product/istio.js +1 -0
- package/config/product/logging.js +1 -0
- package/config/product/longhorn.js +2 -1
- package/config/product/manager.js +1 -0
- package/config/product/monitoring.js +1 -0
- package/config/product/navlinks.js +1 -0
- package/config/product/neuvector.js +2 -1
- package/config/product/settings.js +1 -0
- package/config/product/uiplugins.js +1 -0
- package/core/__tests__/extension-manager-impl.test.js +187 -2
- package/core/__tests__/plugin-products-helpers.test.ts +454 -0
- package/core/__tests__/plugin-products.test.ts +3219 -0
- package/core/extension-manager-impl.js +34 -3
- package/core/plugin-helpers.ts +31 -0
- package/core/plugin-products-base.ts +375 -0
- package/core/plugin-products-extending.ts +44 -0
- package/core/plugin-products-helpers.ts +262 -0
- package/core/plugin-products-top-level.ts +66 -0
- package/core/plugin-products-type-guards.ts +33 -0
- package/core/plugin-products.ts +50 -0
- package/core/plugin-types.ts +222 -0
- package/core/plugin.ts +45 -10
- package/core/productDebugger.js +48 -0
- package/core/types.ts +95 -11
- package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
- package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
- package/detail/__tests__/node.test.ts +83 -0
- package/detail/fleet.cattle.io.bundle.vue +21 -34
- package/detail/management.cattle.io.oidcclient.vue +2 -1
- package/detail/node.vue +1 -0
- package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
- package/dialog/InstallExtensionDialog.vue +6 -27
- package/dialog/UninstallExistingExtensionDialog.vue +141 -0
- package/dialog/UninstallExtensionDialog.vue +4 -26
- package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +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/__tests__/Ingress.test.ts +176 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +5 -4
- package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
- package/edit/provisioning.cattle.io.cluster/shared.ts +4 -2
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -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 -50
- package/list/workload.vue +11 -4
- package/mixins/brand.js +2 -1
- package/mixins/resource-fetch.js +12 -3
- 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/pod.js +18 -0
- package/models/provisioning.cattle.io.cluster.js +2 -2
- package/models/workload.js +20 -2
- package/package.json +5 -6
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
- 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/brand.vue +4 -4
- package/pages/c/_cluster/settings/performance.vue +0 -5
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +231 -13
- package/pages/c/_cluster/uiplugins/index.vue +145 -38
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
- package/plugins/dashboard-store/actions.js +3 -2
- package/plugins/dashboard-store/resource-class.js +62 -6
- package/plugins/plugin.js +16 -0
- package/plugins/steve/steve-pagination-utils.ts +8 -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/scripts/typegen.sh +13 -1
- package/store/__tests__/type-map.test.ts +84 -24
- package/store/type-map.js +42 -3
- package/tsconfig.paths.json +1 -0
- package/types/resources/pod.ts +18 -0
- package/types/shell/index.d.ts +8506 -2908
- package/types/store/dashboard-store.types.ts +5 -0
- package/types/store/pagination.types.ts +6 -0
- package/utils/__tests__/require-asset.test.ts +98 -0
- package/utils/async.ts +1 -5
- package/utils/axios.js +1 -4
- package/utils/brand.ts +3 -1
- package/utils/dynamic-importer.js +3 -2
- package/utils/favicon.js +4 -3
- package/utils/pagination-utils.ts +1 -1
- package/utils/require-asset.ts +95 -0
- package/utils/uiplugins.ts +12 -16
- package/utils/validators/__tests__/private-registry.test.ts +76 -0
- package/utils/validators/private-registry.ts +28 -0
- package/vue.config.js +4 -3
- package/components/HarvesterServiceAddOnConfig.vue +0 -207
package/config/product/apps.js
CHANGED
|
@@ -29,7 +29,8 @@ export function init(store) {
|
|
|
29
29
|
weight: 97,
|
|
30
30
|
ifHaveGroup: 'catalog.cattle.io',
|
|
31
31
|
icon: 'marketplace',
|
|
32
|
-
showNamespaceFilter: true
|
|
32
|
+
showNamespaceFilter: true,
|
|
33
|
+
extendable: true,
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
virtualType({
|
|
@@ -53,6 +54,7 @@ export function init(store) {
|
|
|
53
54
|
|
|
54
55
|
configureType(CATALOG.APP, { isCreatable: false, isEditable: false });
|
|
55
56
|
configureType(CATALOG.OPERATION, { isCreatable: false, isEditable: false });
|
|
57
|
+
configureType(CATALOG.CLUSTER_REPO, { listCreateButtonLabelKey: 'catalog.repo.add' });
|
|
56
58
|
|
|
57
59
|
const repoType = {
|
|
58
60
|
name: 'type',
|
package/config/product/auth.js
CHANGED
package/config/product/backup.js
CHANGED
|
@@ -14,7 +14,7 @@ export function init(store) {
|
|
|
14
14
|
headers
|
|
15
15
|
} = DSL(store, NAME);
|
|
16
16
|
|
|
17
|
-
product({ ifHaveGroup: /^(.*\.)*compliance\.cattle\.io
|
|
17
|
+
product({ ifHaveGroup: /^(.*\.)*compliance\.cattle\.io$/, extendable: true });
|
|
18
18
|
|
|
19
19
|
weightType(COMPLIANCE.CLUSTER_SCAN, 3, true);
|
|
20
20
|
weightType(COMPLIANCE.CLUSTER_SCAN_PROFILE, 2, true);
|
|
@@ -27,13 +27,15 @@ import {
|
|
|
27
27
|
|
|
28
28
|
import { DSL } from '@shell/store/type-map';
|
|
29
29
|
import {
|
|
30
|
-
STEVE_AGE_COL, STEVE_EVENT_FIRST_SEEN, STEVE_EVENT_LAST_SEEN, STEVE_EVENT_OBJECT, STEVE_EVENT_TYPE, STEVE_LIST_GROUPS, STEVE_NAMESPACE_COL, STEVE_NAME_COL, STEVE_STATE_COL
|
|
30
|
+
STEVE_AGE_COL, STEVE_EVENT_FIRST_SEEN, STEVE_EVENT_LAST_SEEN, STEVE_EVENT_OBJECT, STEVE_EVENT_TYPE, STEVE_LIST_GROUPS, STEVE_NAMESPACE_COL, STEVE_NAME_COL, STEVE_STATE_COL,
|
|
31
|
+
STEVE_WORKLOAD_HEALTH_SCALE
|
|
31
32
|
} from '@shell/config/pagination-table-headers';
|
|
32
33
|
|
|
33
34
|
import { COLUMN_BREAKPOINTS } from '@shell/types/store/type-map';
|
|
34
35
|
import { STEVE_CACHE } from '@shell/store/features';
|
|
35
36
|
import { configureConditionalDepaginate } from '@shell/store/type-map.utils';
|
|
36
37
|
import { CATTLE_PUBLIC_ENDPOINTS, STORAGE } from '@shell/config/labels-annotations';
|
|
38
|
+
import { POD_LAST_RESTART_FIELD as POD_RESTARTS_LAST_FIELD, POD_RESTART_FIELD as POD_RESTARTS_COUNT_FIELD } from '@shell/types/resources/pod';
|
|
37
39
|
|
|
38
40
|
export const NAME = 'explorer';
|
|
39
41
|
|
|
@@ -58,6 +60,7 @@ export function init(store) {
|
|
|
58
60
|
weight: 3,
|
|
59
61
|
showNamespaceFilter: true,
|
|
60
62
|
icon: 'compass',
|
|
63
|
+
extendable: true,
|
|
61
64
|
typeStoreMap: {
|
|
62
65
|
[MANAGEMENT.PROJECT]: 'management',
|
|
63
66
|
[MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING]: 'management',
|
|
@@ -386,11 +389,11 @@ export function init(store) {
|
|
|
386
389
|
headers(WORKLOAD, [STATE, NAME_COL, NAMESPACE_COL, TYPE, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE]);
|
|
387
390
|
headers(WORKLOAD_TYPES.DEPLOYMENT,
|
|
388
391
|
[STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Up-to-date', 'Available', POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE],
|
|
389
|
-
[STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(6), STEVE_WORKLOAD_ENDPOINTS, 'Ready', 'Up-to-date', 'Available', STEVE_AGE_COL],
|
|
392
|
+
[STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(6), STEVE_WORKLOAD_ENDPOINTS, 'Ready', 'Up-to-date', 'Available', STEVE_AGE_COL, STEVE_WORKLOAD_HEALTH_SCALE],
|
|
390
393
|
);
|
|
391
394
|
headers(WORKLOAD_TYPES.DAEMON_SET,
|
|
392
395
|
[STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE],
|
|
393
|
-
[STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(9), STEVE_WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', STEVE_AGE_COL]
|
|
396
|
+
[STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(9), STEVE_WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', STEVE_AGE_COL, STEVE_WORKLOAD_HEALTH_SCALE]
|
|
394
397
|
);
|
|
395
398
|
headers(WORKLOAD_TYPES.REPLICA_SET,
|
|
396
399
|
[STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE],
|
|
@@ -398,7 +401,7 @@ export function init(store) {
|
|
|
398
401
|
);
|
|
399
402
|
headers(WORKLOAD_TYPES.STATEFUL_SET,
|
|
400
403
|
[STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE],
|
|
401
|
-
[STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(4), STEVE_WORKLOAD_ENDPOINTS, 'Ready', STEVE_AGE_COL],
|
|
404
|
+
[STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(4), STEVE_WORKLOAD_ENDPOINTS, 'Ready', STEVE_AGE_COL, STEVE_WORKLOAD_HEALTH_SCALE],
|
|
402
405
|
);
|
|
403
406
|
headers(WORKLOAD_TYPES.JOB,
|
|
404
407
|
[STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Completions', DURATION, POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE],
|
|
@@ -408,7 +411,7 @@ export function init(store) {
|
|
|
408
411
|
sort: 'metadata.fields.3',
|
|
409
412
|
search: 'metadata.fields.3',
|
|
410
413
|
formatter: undefined, // Now that sort/search is remote we're not doing weird things with start time (see `duration` in model)
|
|
411
|
-
}, STEVE_AGE_COL],
|
|
414
|
+
}, STEVE_AGE_COL, STEVE_WORKLOAD_HEALTH_SCALE],
|
|
412
415
|
);
|
|
413
416
|
headers(WORKLOAD_TYPES.CRON_JOB,
|
|
414
417
|
[STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Schedule', 'Last Schedule', POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE],
|
|
@@ -428,7 +431,23 @@ export function init(store) {
|
|
|
428
431
|
...POD_IMAGES,
|
|
429
432
|
sort: false,
|
|
430
433
|
search: 'spec.containers.image'
|
|
431
|
-
},
|
|
434
|
+
},
|
|
435
|
+
'Ready',
|
|
436
|
+
{
|
|
437
|
+
name: 'pod-restart',
|
|
438
|
+
labelKey: 'tableHeaders.podRestarts',
|
|
439
|
+
search: false,
|
|
440
|
+
sort: [POD_RESTARTS_COUNT_FIELD, POD_RESTARTS_LAST_FIELD, 'metadata.name'],
|
|
441
|
+
value: 'restartsCount',
|
|
442
|
+
}, {
|
|
443
|
+
name: 'pod-last-restart',
|
|
444
|
+
labelKey: 'tableHeaders.podLastRestart',
|
|
445
|
+
value: 'restartsLaster',
|
|
446
|
+
search: false,
|
|
447
|
+
sort: [POD_RESTARTS_LAST_FIELD, POD_RESTARTS_COUNT_FIELD, 'metadata.name'],
|
|
448
|
+
},
|
|
449
|
+
'IP',
|
|
450
|
+
{
|
|
432
451
|
...NODE_COL,
|
|
433
452
|
search: 'spec.nodeName'
|
|
434
453
|
},
|
package/config/product/fleet.js
CHANGED
package/config/product/istio.js
CHANGED
|
@@ -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
|
});
|