@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
|
@@ -83,6 +83,11 @@ export interface ActionFindPageArgs extends ActionCoreFindArgs {
|
|
|
83
83
|
|
|
84
84
|
saveCountAs?: string,
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* When making a supporting HTTP request include associated resource data
|
|
88
|
+
*/
|
|
89
|
+
includeAssociatedData?: boolean,
|
|
90
|
+
|
|
86
91
|
/**
|
|
87
92
|
* The target minimum revision for the resource.
|
|
88
93
|
*
|
|
@@ -607,6 +607,7 @@ export interface StorePaginationRequest {
|
|
|
607
607
|
* The single namespace to filter results by (as part of url path, not pagination params)
|
|
608
608
|
*/
|
|
609
609
|
namespace?: string,
|
|
610
|
+
|
|
610
611
|
/**
|
|
611
612
|
* The set of pagination args used to create the request
|
|
612
613
|
*/
|
|
@@ -616,6 +617,11 @@ export interface StorePaginationRequest {
|
|
|
616
617
|
* Does this request stem from a list with manual refresh?
|
|
617
618
|
*/
|
|
618
619
|
hasManualRefresh?: boolean,
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* When making a supporting HTTP request include associated resource data
|
|
623
|
+
*/
|
|
624
|
+
includeAssociatedData?: boolean,
|
|
619
625
|
}
|
|
620
626
|
|
|
621
627
|
/**
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { toContextKey, requireAsset, requireJson, _setContexts } from '@shell/utils/require-asset';
|
|
2
|
+
|
|
3
|
+
describe('fx: toContextKey', () => {
|
|
4
|
+
it.each([
|
|
5
|
+
['~shell/assets/images/providers/aws.svg', './images/providers/aws.svg'],
|
|
6
|
+
['@shell/assets/images/providers/aws.svg', './images/providers/aws.svg'],
|
|
7
|
+
['~shell/assets/brand/suse/metadata.json', './brand/suse/metadata.json'],
|
|
8
|
+
['@shell/assets/images/pl/dark/logo.svg', './images/pl/dark/logo.svg'],
|
|
9
|
+
])('should convert %s to %s', (input, expected) => {
|
|
10
|
+
expect(toContextKey(input)).toStrictEqual(expected);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should prepend ./ to paths without shell prefix', () => {
|
|
14
|
+
expect(toContextKey('images/providers/aws.svg')).toStrictEqual('./images/providers/aws.svg');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('fx: requireAsset', () => {
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
_setContexts(null, null);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return the resolved asset URL from the image context', () => {
|
|
24
|
+
const mockImgCtx = jest.fn().mockReturnValue('/static/images/aws.svg');
|
|
25
|
+
|
|
26
|
+
_setContexts(mockImgCtx, null);
|
|
27
|
+
|
|
28
|
+
const result = requireAsset('~shell/assets/images/providers/aws.svg');
|
|
29
|
+
|
|
30
|
+
expect(result).toStrictEqual('/static/images/aws.svg');
|
|
31
|
+
expect(mockImgCtx).toHaveBeenCalledWith('./images/providers/aws.svg');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should throw when the image context is not available', () => {
|
|
35
|
+
_setContexts(null, null);
|
|
36
|
+
|
|
37
|
+
expect(() => requireAsset('~shell/assets/images/foo.svg'))
|
|
38
|
+
.toThrow('Asset context not available for: ~shell/assets/images/foo.svg');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should propagate errors from the context function for missing assets', () => {
|
|
42
|
+
const mockImgCtx = jest.fn().mockImplementation(() => {
|
|
43
|
+
throw new Error('Cannot find module');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
_setContexts(mockImgCtx, null);
|
|
47
|
+
|
|
48
|
+
expect(() => requireAsset('~shell/assets/images/missing.svg'))
|
|
49
|
+
.toThrow('Cannot find module');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('fx: requireJson', () => {
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
_setContexts(null, null);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return mod.default when available', () => {
|
|
59
|
+
const jsonData = { vendor: 'suse' };
|
|
60
|
+
const mockJsonCtx = jest.fn().mockReturnValue({ default: jsonData });
|
|
61
|
+
|
|
62
|
+
_setContexts(null, mockJsonCtx);
|
|
63
|
+
|
|
64
|
+
const result = requireJson('~shell/assets/brand/suse/metadata.json');
|
|
65
|
+
|
|
66
|
+
expect(result).toStrictEqual(jsonData);
|
|
67
|
+
expect(mockJsonCtx).toHaveBeenCalledWith('./brand/suse/metadata.json');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return mod directly when no default export', () => {
|
|
71
|
+
const jsonData = { vendor: 'rancher' };
|
|
72
|
+
const mockJsonCtx = jest.fn().mockReturnValue(jsonData);
|
|
73
|
+
|
|
74
|
+
_setContexts(null, mockJsonCtx);
|
|
75
|
+
|
|
76
|
+
const result = requireJson('~shell/assets/brand/rancher/metadata.json');
|
|
77
|
+
|
|
78
|
+
expect(result).toStrictEqual(jsonData);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should throw when the JSON context is not available', () => {
|
|
82
|
+
_setContexts(null, null);
|
|
83
|
+
|
|
84
|
+
expect(() => requireJson('~shell/assets/brand/suse/metadata.json'))
|
|
85
|
+
.toThrow('JSON context not available for: ~shell/assets/brand/suse/metadata.json');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should propagate errors from the context function for missing files', () => {
|
|
89
|
+
const mockJsonCtx = jest.fn().mockImplementation(() => {
|
|
90
|
+
throw new Error('Cannot find module');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
_setContexts(null, mockJsonCtx);
|
|
94
|
+
|
|
95
|
+
expect(() => requireJson('~shell/assets/brand/missing/metadata.json'))
|
|
96
|
+
.toThrow('Cannot find module');
|
|
97
|
+
});
|
|
98
|
+
});
|
package/utils/async.ts
CHANGED
|
@@ -10,11 +10,7 @@ export const waitFor = (testFn: Function, msg = '', timeoutMs = 3000000, interva
|
|
|
10
10
|
gatedLog('Wait for', msg, 'timed out');
|
|
11
11
|
clearInterval(interval);
|
|
12
12
|
clearTimeout(timeout);
|
|
13
|
-
|
|
14
|
-
reject(new Error(`Failed waiting for: ${ msg }`));
|
|
15
|
-
} else {
|
|
16
|
-
throw new Error(`waitFor timed out after ${ timeoutMs / 1000 } seconds`);
|
|
17
|
-
}
|
|
13
|
+
reject(new Error(msg ? `Failed waiting for: ${ msg }` : `waitFor timed out after ${ timeoutMs / 1000 } seconds`));
|
|
18
14
|
}, timeoutMs);
|
|
19
15
|
const interval = setInterval(() => {
|
|
20
16
|
if ( testFn() ) {
|
package/utils/axios.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Axios from 'axios';
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import axiosRetry from 'axios-retry';
|
|
4
4
|
|
|
5
5
|
// Axios.prototype cannot be modified
|
|
@@ -37,9 +37,6 @@ const axiosExtra = {
|
|
|
37
37
|
onError(fn) {
|
|
38
38
|
this.onRequestError(fn);
|
|
39
39
|
this.onResponseError(fn);
|
|
40
|
-
},
|
|
41
|
-
create(options) {
|
|
42
|
-
return createAxiosInstance(defu(options, this.defaults));
|
|
43
40
|
}
|
|
44
41
|
};
|
|
45
42
|
|
package/utils/brand.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { requireJson } from '@shell/utils/require-asset';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Brand/Theme metadata
|
|
3
5
|
*/
|
|
@@ -21,7 +23,7 @@ export function getBrandMeta(brand: string): BrandMeta {
|
|
|
21
23
|
|
|
22
24
|
if (brand) {
|
|
23
25
|
try {
|
|
24
|
-
brandMeta =
|
|
26
|
+
brandMeta = requireJson(`~shell/assets/brand/${ brand }/metadata.json`) as BrandMeta;
|
|
25
27
|
} catch {}
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -94,8 +94,9 @@ export function loadProduct(name) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export function listProducts() {
|
|
97
|
-
|
|
98
|
-
const
|
|
97
|
+
// We just want the .js or .ts files
|
|
98
|
+
const ctx = require.context('@shell/config/product', true, /\.\/.*\.[js|ts]/);
|
|
99
|
+
const products = ctx.keys().map(path => path.substr(2)).map(path => path.slice(0, -3));
|
|
99
100
|
|
|
100
101
|
return products;
|
|
101
102
|
}
|
package/utils/favicon.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SETTING } from '@shell/config/settings';
|
|
2
2
|
import { MANAGEMENT } from '@shell/config/types';
|
|
3
|
+
import { requireAsset } from '@shell/utils/require-asset';
|
|
3
4
|
|
|
4
5
|
let favIconSet = false;
|
|
5
6
|
|
|
@@ -16,11 +17,11 @@ export function setFavIcon(store) {
|
|
|
16
17
|
let brandImage;
|
|
17
18
|
|
|
18
19
|
if (brandSetting === 'suse') {
|
|
19
|
-
brandImage =
|
|
20
|
+
brandImage = requireAsset('~shell/assets/brand/suse/favicon.png');
|
|
20
21
|
} else if (brandSetting === 'csp') {
|
|
21
|
-
brandImage =
|
|
22
|
+
brandImage = requireAsset('~shell/assets/brand/csp/favicon.png');
|
|
22
23
|
} else if (brandSetting === 'harvester') {
|
|
23
|
-
brandImage =
|
|
24
|
+
brandImage = requireAsset('~shell/assets/brand/harvester/favicon.png');
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
link.href = res?.value || brandImage || defaultFavIcon;
|
|
@@ -299,7 +299,7 @@ class PaginationUtils {
|
|
|
299
299
|
return isEqual(aPrimitiveTypes, bPrimitiveTypes) &&
|
|
300
300
|
this.paginationFiltersEqual(aFilter, bFilter) &&
|
|
301
301
|
this.paginationFiltersEqual(aPN, bPN) &&
|
|
302
|
-
sameArrayObjects<PaginationSort>(aSort, bSort,
|
|
302
|
+
sameArrayObjects<PaginationSort>(aSort, bSort, false);
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility to replace dynamic require() calls for image/asset imports.
|
|
3
|
+
*
|
|
4
|
+
* Uses webpack's require.context to resolve assets at compile time,
|
|
5
|
+
* then provides lookup functions by path.
|
|
6
|
+
*
|
|
7
|
+
* When migrating to Vite, replace the require.context calls below with:
|
|
8
|
+
*
|
|
9
|
+
* const imgCtx = import.meta.glob(
|
|
10
|
+
* '@shell/assets/**\/*.{svg,png,jpg,jpeg,gif,ico,webp}',
|
|
11
|
+
* { eager: true, query: '?url', import: 'default' }
|
|
12
|
+
* );
|
|
13
|
+
*
|
|
14
|
+
* const jsonCtx = import.meta.glob(
|
|
15
|
+
* '@shell/assets/**\/*.json',
|
|
16
|
+
* { eager: true }
|
|
17
|
+
* );
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// --- Webpack: require.context type declarations ---
|
|
21
|
+
|
|
22
|
+
interface WebpackRequireContext {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
(key: string): any;
|
|
25
|
+
keys(): string[];
|
|
26
|
+
resolve(key: string): string;
|
|
27
|
+
id: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- Webpack: require.context (compile-time transform) ---
|
|
31
|
+
|
|
32
|
+
let imgCtx: WebpackRequireContext | null = null;
|
|
33
|
+
let jsonCtx: WebpackRequireContext | null = null;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// @ts-expect-error — require.context is a webpack compile-time transform, not visible to TypeScript
|
|
37
|
+
imgCtx = require.context('@shell/assets', true, /\.(svg|png|jpe?g|gif|ico|webp)$/);
|
|
38
|
+
} catch (e) {}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// @ts-expect-error — require.context is a webpack compile-time transform, not visible to TypeScript
|
|
42
|
+
jsonCtx = require.context('@shell/assets', true, /\.json$/);
|
|
43
|
+
} catch (e) {}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Convert an asset path to a require.context key.
|
|
47
|
+
*
|
|
48
|
+
* Input: '~shell/assets/images/providers/aws.svg' or '@shell/assets/images/providers/aws.svg'
|
|
49
|
+
* Output: './images/providers/aws.svg'
|
|
50
|
+
*/
|
|
51
|
+
export function toContextKey(path: string): string {
|
|
52
|
+
return `./${ path.replace(/^[~@]shell\/assets\//, '') }`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Look up an image asset URL by path, similar to Webpack's require() for images.
|
|
57
|
+
*
|
|
58
|
+
* Accepts paths with ~shell/ or @shell/ prefix.
|
|
59
|
+
* Throws if the asset is not found (matching the original require() behavior),
|
|
60
|
+
* so callers can use try/catch for fallback logic.
|
|
61
|
+
*/
|
|
62
|
+
export function requireAsset(path: string): string {
|
|
63
|
+
if (!imgCtx) {
|
|
64
|
+
// Throw to match original require() behavior — callers rely on try/catch for fallback logic
|
|
65
|
+
throw new Error(`Asset context not available for: ${ path }`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const key = toContextKey(path);
|
|
69
|
+
|
|
70
|
+
return imgCtx(key);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Load a JSON file from @shell/assets.
|
|
75
|
+
*
|
|
76
|
+
* Throws if the JSON file is not found (matching the original require() behavior),
|
|
77
|
+
* so callers can use try/catch for fallback logic.
|
|
78
|
+
*/
|
|
79
|
+
export function requireJson(path: string): object {
|
|
80
|
+
if (!jsonCtx) {
|
|
81
|
+
// Throw to match original require() behavior — callers rely on try/catch for fallback logic
|
|
82
|
+
throw new Error(`JSON context not available for: ${ path }`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const key = toContextKey(path);
|
|
86
|
+
const mod = jsonCtx(key);
|
|
87
|
+
|
|
88
|
+
return mod.default || mod;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Exported for testing — allows injecting mock contexts
|
|
92
|
+
export function _setContexts(img: WebpackRequireContext | null, json: WebpackRequireContext | null): void {
|
|
93
|
+
imgCtx = img;
|
|
94
|
+
jsonCtx = json;
|
|
95
|
+
}
|
package/utils/uiplugins.ts
CHANGED
|
@@ -5,14 +5,15 @@ import { UI_PLUGIN_BASE_URL, isSupportedChartVersion, UI_PLUGIN_LABELS } from '@
|
|
|
5
5
|
import { Plugin, Version } from '@shell/types/uiplugins';
|
|
6
6
|
|
|
7
7
|
const MAX_RETRIES = 10;
|
|
8
|
-
const RETRY_WAIT = 2500;
|
|
8
|
+
const RETRY_WAIT = 2500; // 2.5 seconds
|
|
9
|
+
const ACTIVE_STATUS_TIMEOUT = 200000; // 20 seconds
|
|
9
10
|
|
|
10
11
|
type Action = 'install' | 'upgrade';
|
|
11
12
|
export type HelmRepository = any;
|
|
12
13
|
export type HelmChart = any;
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
+
* Get the latest compatible version of a Helm Chart extension
|
|
16
17
|
* @param store Vue store
|
|
17
18
|
* @param chartName The chartName
|
|
18
19
|
* @param rancherVersion Rancher version
|
|
@@ -68,8 +69,8 @@ export async function waitForUIExtension(store: any, name: string, maxRetries =
|
|
|
68
69
|
return extension;
|
|
69
70
|
}
|
|
70
71
|
} catch (e) {
|
|
72
|
+
console.error('waiting for UI extension to be available: error =', e); // eslint-disable-line no-console
|
|
71
73
|
}
|
|
72
|
-
|
|
73
74
|
tries++;
|
|
74
75
|
|
|
75
76
|
if (tries > maxRetries) {
|
|
@@ -106,7 +107,6 @@ export async function waitForUIPackage(store: any, extension: any, maxRetries =
|
|
|
106
107
|
return true;
|
|
107
108
|
} catch (error) {
|
|
108
109
|
}
|
|
109
|
-
|
|
110
110
|
tries++;
|
|
111
111
|
|
|
112
112
|
if (tries > maxRetries) {
|
|
@@ -199,7 +199,7 @@ export async function getHelmRepositoryMatch(store: any, urlRegexes: string[], c
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
|
-
*
|
|
202
|
+
* Get a Helm Repository matching the given criteria.
|
|
203
203
|
* @param store Vue store
|
|
204
204
|
* @param matchFn Match function for repository's urls
|
|
205
205
|
* @returns HelmRepository
|
|
@@ -225,15 +225,15 @@ export async function refreshHelmRepository(store: any, url: string): Promise<vo
|
|
|
225
225
|
const now = (new Date()).toISOString().replace(/\.\d+Z$/, 'Z');
|
|
226
226
|
|
|
227
227
|
repository.spec.forceUpdate = now;
|
|
228
|
-
|
|
229
228
|
await repository.save();
|
|
230
229
|
|
|
231
|
-
await repository.waitForState('active',
|
|
230
|
+
await repository.waitForState('active', ACTIVE_STATUS_TIMEOUT, RETRY_WAIT);
|
|
232
231
|
|
|
233
232
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
234
233
|
}
|
|
235
234
|
|
|
236
235
|
/**
|
|
236
|
+
* Create a Helm Repository and wait for it to be downloaded
|
|
237
237
|
*
|
|
238
238
|
* @param store Vue store
|
|
239
239
|
* @param name Repository name
|
|
@@ -260,7 +260,7 @@ export async function createHelmRepository(store: any, name: string, url: string
|
|
|
260
260
|
|
|
261
261
|
const helmRepo = await repo.save();
|
|
262
262
|
|
|
263
|
-
// Poll the repository
|
|
263
|
+
// Poll the repository status MAX_RETRIES times until it has been downloaded
|
|
264
264
|
let fetched = false;
|
|
265
265
|
let tries = 0;
|
|
266
266
|
|
|
@@ -275,23 +275,19 @@ export async function createHelmRepository(store: any, name: string, url: string
|
|
|
275
275
|
|
|
276
276
|
const downloaded = repo.status.conditions.find((s: any) => s.type === 'Downloaded');
|
|
277
277
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
278
|
+
console.log(`Waiting for helm repository to be downloaded... try ${ tries } time(s).`); // eslint-disable-line no-console
|
|
279
|
+
|
|
280
|
+
if (downloaded && downloaded.status === 'True') {
|
|
281
|
+
fetched = true;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
if (!fetched) {
|
|
285
|
-
tries++;
|
|
286
|
-
|
|
287
285
|
if (tries > MAX_RETRIES) {
|
|
288
286
|
throw new Error('Failed to add Helm Chart Repository');
|
|
289
287
|
}
|
|
290
288
|
|
|
291
289
|
await new Promise((resolve) => setTimeout(resolve, RETRY_WAIT));
|
|
292
290
|
}
|
|
293
|
-
|
|
294
|
-
fetched = true;
|
|
295
291
|
}
|
|
296
292
|
|
|
297
293
|
// Return the Helm Repository
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { privateRegistryRequired } from '@shell/utils/validators/private-registry';
|
|
2
|
+
|
|
3
|
+
const makeCtx = (overrides: any = {}) => ({
|
|
4
|
+
t: jest.fn((key: string, params?: any) => (params ? `${ key }:${ JSON.stringify(params) }` : key)),
|
|
5
|
+
privateRegistryEnabled: false,
|
|
6
|
+
normanCluster: { importedConfig: { privateRegistryURL: null } },
|
|
7
|
+
isImportedCluster: true,
|
|
8
|
+
...overrides,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('privateRegistryRequired', () => {
|
|
12
|
+
it('should return undefined when the cluster is not imported', () => {
|
|
13
|
+
const ctx = makeCtx({ isImportedCluster: false, privateRegistryEnabled: true });
|
|
14
|
+
const rule = privateRegistryRequired(ctx);
|
|
15
|
+
|
|
16
|
+
expect(rule()).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should default isImportedCluster to true when the property is absent from the context', () => {
|
|
20
|
+
const ctx: any = {
|
|
21
|
+
t: jest.fn((key: string, params?: any) => (params ? `${ key }:${ JSON.stringify(params) }` : key)),
|
|
22
|
+
privateRegistryEnabled: true,
|
|
23
|
+
normanCluster: { importedConfig: { privateRegistryURL: null } },
|
|
24
|
+
};
|
|
25
|
+
const rule = privateRegistryRequired(ctx);
|
|
26
|
+
|
|
27
|
+
expect(rule()).toStrictEqual('validation.required:{"key":"cluster.privateRegistry.label"}');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should return undefined when the registry toggle is off', () => {
|
|
31
|
+
const ctx = makeCtx({ privateRegistryEnabled: false });
|
|
32
|
+
const rule = privateRegistryRequired(ctx);
|
|
33
|
+
|
|
34
|
+
expect(rule()).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return a required error when enabled but the url is empty', () => {
|
|
38
|
+
const ctx = makeCtx({
|
|
39
|
+
privateRegistryEnabled: true,
|
|
40
|
+
normanCluster: { importedConfig: { privateRegistryURL: '' } },
|
|
41
|
+
});
|
|
42
|
+
const rule = privateRegistryRequired(ctx);
|
|
43
|
+
|
|
44
|
+
expect(rule()).toStrictEqual('validation.required:{"key":"cluster.privateRegistry.label"}');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return a required error when normanCluster.importedConfig is missing', () => {
|
|
48
|
+
const ctx = makeCtx({
|
|
49
|
+
privateRegistryEnabled: true,
|
|
50
|
+
normanCluster: {},
|
|
51
|
+
});
|
|
52
|
+
const rule = privateRegistryRequired(ctx);
|
|
53
|
+
|
|
54
|
+
expect(rule()).toStrictEqual('validation.required:{"key":"cluster.privateRegistry.label"}');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return a format error when enabled and the url is malformed', () => {
|
|
58
|
+
const ctx = makeCtx({
|
|
59
|
+
privateRegistryEnabled: true,
|
|
60
|
+
normanCluster: { importedConfig: { privateRegistryURL: 'goober' } },
|
|
61
|
+
});
|
|
62
|
+
const rule = privateRegistryRequired(ctx);
|
|
63
|
+
|
|
64
|
+
expect(rule()).toStrictEqual('cluster.privateRegistry.privateRegistryUrlError');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return undefined when enabled and the url is valid', () => {
|
|
68
|
+
const ctx = makeCtx({
|
|
69
|
+
privateRegistryEnabled: true,
|
|
70
|
+
normanCluster: { importedConfig: { privateRegistryURL: 'registry.io:5000' } },
|
|
71
|
+
});
|
|
72
|
+
const rule = privateRegistryRequired(ctx);
|
|
73
|
+
|
|
74
|
+
expect(rule()).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Translation } from '@shell/types/t';
|
|
2
|
+
import formRulesGenerator from '@shell/utils/validators/formRules';
|
|
3
|
+
|
|
4
|
+
interface PrivateRegistryRuleContext {
|
|
5
|
+
t: Translation;
|
|
6
|
+
privateRegistryEnabled: boolean;
|
|
7
|
+
normanCluster: { importedConfig?: { privateRegistryURL?: string | null } } | null;
|
|
8
|
+
isImportedCluster?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function privateRegistryRequired(ctx: PrivateRegistryRuleContext) {
|
|
12
|
+
return () => {
|
|
13
|
+
// Check existence using `in` rather than direct access to avoid Vue's render-time warning
|
|
14
|
+
const isImported = 'isImportedCluster' in ctx ? !!ctx.isImportedCluster : true;
|
|
15
|
+
|
|
16
|
+
if (!isImported || !ctx.privateRegistryEnabled) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const url = ctx.normanCluster?.importedConfig?.privateRegistryURL;
|
|
20
|
+
|
|
21
|
+
if (!url) {
|
|
22
|
+
return ctx.t('validation.required', { key: ctx.t('cluster.privateRegistry.label') });
|
|
23
|
+
}
|
|
24
|
+
const { registryUrl } = formRulesGenerator(ctx.t, { key: ctx.t('cluster.privateRegistry.label') });
|
|
25
|
+
|
|
26
|
+
return (registryUrl as (val: string) => string | undefined)(url);
|
|
27
|
+
};
|
|
28
|
+
}
|
package/vue.config.js
CHANGED
|
@@ -143,7 +143,7 @@ const instrumentCode = (config) => {
|
|
|
143
143
|
}
|
|
144
144
|
};
|
|
145
145
|
|
|
146
|
-
const getLoaders = (SHELL_ABS) => [
|
|
146
|
+
const getLoaders = (SHELL_ABS, dir) => [
|
|
147
147
|
// no fallback for pre-2013 browsers https://caniuse.com/webworkers
|
|
148
148
|
{
|
|
149
149
|
test: /web-worker.[a-z-]+.js/i,
|
|
@@ -199,7 +199,8 @@ const getLoaders = (SHELL_ABS) => [
|
|
|
199
199
|
appendTsxSuffixTo: [
|
|
200
200
|
'\\.vue$'
|
|
201
201
|
],
|
|
202
|
-
configFile:
|
|
202
|
+
configFile: path.join(SHELL_ABS, 'tsconfig.json'),
|
|
203
|
+
compilerOptions: { rootDir: dir }
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
]
|
|
@@ -580,7 +581,7 @@ module.exports = function(dir, appConfig = {}) {
|
|
|
580
581
|
|
|
581
582
|
config.resolve.symlinks = false;
|
|
582
583
|
processShellFiles(config, SHELL_ABS);
|
|
583
|
-
config.module.rules.push(...getLoaders(SHELL_ABS));
|
|
584
|
+
config.module.rules.push(...getLoaders(SHELL_ABS, dir));
|
|
584
585
|
instrumentCode(config);
|
|
585
586
|
preserveWhitespace(config);
|
|
586
587
|
},
|