@rancher/shell 3.0.11 → 3.0.12-rc.2
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/images/providers/entraid-black.svg +4 -0
- package/assets/images/providers/entraid.svg +9 -0
- package/assets/images/vendor/entraid.svg +9 -0
- package/assets/styles/app.scss +0 -1
- 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 +24 -21
- package/assets/translations/zh-hans.yaml +4 -11
- package/chart/__tests__/S3.test.ts +10 -3
- package/components/CountBox.vue +20 -0
- package/components/CreateDriver.vue +0 -12
- package/components/DetailText.vue +12 -3
- package/components/EmptyProductPage.vue +76 -0
- 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/SelectIconGrid.vue +5 -0
- package/components/SideNav.vue +13 -0
- package/components/__tests__/CountBox.test.ts +72 -0
- package/components/__tests__/DetailText.test.ts +113 -0
- package/components/__tests__/PromptModal.test.ts +2 -0
- package/components/fleet/FleetClusterTargets/index.vue +18 -1
- package/components/fleet/FleetClusters.vue +1 -0
- package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
- package/components/form/InputWithSelect.vue +18 -10
- package/components/form/KeyValue.vue +17 -1
- package/components/form/LabeledSelect.vue +82 -24
- package/components/form/NodeScheduling.vue +17 -3
- package/components/form/PrivateRegistry.vue +69 -0
- package/components/form/Select.vue +73 -56
- package/components/form/ServiceNameSelect.vue +13 -11
- package/components/form/__tests__/KeyValue.test.ts +66 -0
- package/components/form/__tests__/NodeScheduling.test.ts +9 -0
- package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
- package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
- package/components/formatter/WorkloadHealthScale.vue +3 -1
- package/components/nav/Group.vue +33 -9
- package/components/nav/Header.vue +56 -10
- package/components/nav/NotificationCenter/Notification.vue +4 -1
- package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
- package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
- package/components/nav/TopLevelMenu.vue +15 -1
- package/components/nav/Type.vue +8 -7
- package/components/nav/WindowManager/index.vue +2 -1
- package/components/nav/WorkspaceSwitcher.vue +13 -0
- package/components/nav/__tests__/Group.test.ts +67 -0
- package/components/nav/__tests__/Header.test.ts +235 -0
- package/components/nav/__tests__/Type.test.ts +20 -3
- package/components/templates/default.vue +34 -4
- package/components/templates/home.vue +12 -25
- package/components/templates/plain.vue +13 -26
- package/composables/useLabeledFormElement.ts +10 -2
- package/composables/useLabeledSelect.ts +60 -0
- package/composables/useUserRetentionValidation.ts +1 -49
- package/config/cookies.js +0 -1
- package/config/labels-annotations.js +1 -0
- package/config/pagination-table-headers.js +8 -1
- package/config/product/apps.js +2 -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/config/query-params.js +1 -0
- package/config/router/routes.js +0 -8
- package/core/__tests__/plugin-products-helpers.test.ts +454 -0
- package/core/__tests__/plugin-products.test.ts +3810 -0
- package/core/extension-manager-impl.js +30 -1
- package/core/plugin-products-base.ts +392 -0
- package/core/plugin-products-extending.ts +44 -0
- package/core/plugin-products-helpers.ts +263 -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 +237 -0
- package/core/plugin.ts +45 -10
- package/core/productDebugger.js +48 -0
- package/core/types.ts +97 -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__/management.cattle.io.fleetworkspace.test.ts +128 -0
- package/detail/fleet.cattle.io.bundle.vue +21 -34
- package/detail/management.cattle.io.fleetworkspace.vue +49 -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/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
- package/edit/__tests__/kontainerDriver.test.ts +0 -13
- package/edit/__tests__/nodeDriver.test.ts +5 -11
- package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
- package/edit/auth/__tests__/oidc.test.ts +54 -0
- package/edit/auth/azuread.vue +1 -1
- package/edit/auth/oidc.vue +8 -0
- package/edit/kontainerDriver.vue +1 -2
- package/edit/nodeDriver.vue +0 -2
- package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +70 -99
- package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
- package/initialize/App.vue +29 -2
- package/initialize/install-plugins.js +0 -2
- package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
- package/list/catalog.cattle.io.app.vue +25 -5
- package/list/management.cattle.io.feature.vue +1 -1
- package/list/management.cattle.io.fleetworkspace.vue +8 -0
- package/list/provisioning.cattle.io.cluster.vue +0 -1
- package/list/workload.vue +11 -4
- package/machine-config/amazonec2.vue +1 -0
- package/mixins/chart.js +40 -9
- package/mixins/resource-fetch.js +12 -3
- package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
- package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
- package/models/__tests__/chart.test.ts +99 -6
- package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
- package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
- package/models/catalog.cattle.io.app.js +21 -17
- package/models/catalog.cattle.io.clusterrepo.js +39 -11
- package/models/chart.js +33 -19
- package/models/fleet-application.js +1 -1
- package/models/fleet.cattle.io.bundle.js +1 -1
- package/models/kontainerdriver.js +11 -0
- package/models/management.cattle.io.authconfig.js +5 -1
- package/models/management.cattle.io.cluster.js +0 -53
- package/models/management.cattle.io.feature.js +3 -3
- package/models/management.cattle.io.kontainerdriver.js +1 -26
- package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
- package/models/nodedriver.js +7 -0
- package/models/pod.js +18 -0
- package/models/workload.js +20 -2
- package/package.json +13 -13
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
- package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +189 -0
- package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
- package/pages/c/_cluster/apps/charts/chart.vue +217 -33
- package/pages/c/_cluster/apps/charts/index.vue +2 -2
- package/pages/c/_cluster/apps/charts/install.vue +8 -3
- package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
- package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
- package/pages/c/_cluster/settings/brand.vue +4 -4
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +39 -2
- package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +246 -23
- package/pages/c/_cluster/uiplugins/index.vue +166 -62
- 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 +7 -0
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +205 -1
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +82 -4
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
- package/scripts/test-plugins-build.sh +5 -2
- package/scripts/typegen.sh +13 -1
- package/server/server-middleware.js +2 -2
- package/static/humans.txt +1 -0
- package/static/robots.txt +34 -0
- package/static/welcome-cow.svg +18 -0
- package/store/__tests__/catalog.test.ts +161 -11
- package/store/__tests__/type-map.test.ts +84 -24
- package/store/auth.js +0 -3
- package/store/catalog.js +60 -8
- 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 +8539 -2938
- package/types/store/dashboard-store.types.ts +5 -0
- package/types/store/pagination.types.ts +6 -0
- package/utils/__tests__/git.test.ts +270 -0
- package/utils/__tests__/inactivity.test.ts +316 -0
- package/utils/__tests__/object.test.ts +77 -0
- package/utils/__tests__/time.test.ts +14 -1
- package/utils/__tests__/url.test.ts +246 -0
- package/utils/axios.js +1 -4
- package/utils/dynamic-importer.js +3 -2
- package/utils/object.js +33 -2
- package/utils/pagination-utils.ts +1 -1
- package/utils/time.ts +5 -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 +0 -9
- package/assets/images/providers/azuread-black.svg +0 -22
- package/assets/images/providers/azuread.svg +0 -25
- package/assets/images/vendor/azuread.svg +0 -18
- package/assets/styles/fonts/_dots.scss +0 -18
- package/components/EmberPage.vue +0 -622
- package/components/EmberPageView.vue +0 -39
- package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
- package/mixins/labeled-form-element.ts +0 -225
- package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
- package/pages/c/_cluster/manager/pages/_page.vue +0 -22
- package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
- package/plugins/ember-cookie.js +0 -17
- package/utils/ember-page.js +0 -30
package/mixins/chart.js
CHANGED
|
@@ -28,6 +28,17 @@ export default {
|
|
|
28
28
|
ignoreWarning: false,
|
|
29
29
|
|
|
30
30
|
chart: null,
|
|
31
|
+
|
|
32
|
+
// Whether installing a new instance is allowed.
|
|
33
|
+
// This is false when the chart is targeted (has fixed namespace/name annotations)
|
|
34
|
+
// or when the URL query specifies a specific app to edit.
|
|
35
|
+
canInstallNew: true,
|
|
36
|
+
|
|
37
|
+
// Installed instances of this chart that can be selected for edit/upgrade
|
|
38
|
+
// on the chart detail page. When instances exist, `existing` is set to the
|
|
39
|
+
// first one by default, but the user can select a different instance or
|
|
40
|
+
// choose to install a new one.
|
|
41
|
+
installedInstances: [],
|
|
31
42
|
};
|
|
32
43
|
},
|
|
33
44
|
|
|
@@ -375,6 +386,9 @@ export default {
|
|
|
375
386
|
// Use those values to check for a catalog app resource.
|
|
376
387
|
// If found, set the form to edit mode. If not, set the
|
|
377
388
|
// form to create mode.
|
|
389
|
+
// This is a hard blocker - installing a new instance is NOT allowed.
|
|
390
|
+
|
|
391
|
+
this.canInstallNew = false;
|
|
378
392
|
|
|
379
393
|
try {
|
|
380
394
|
this.existing = await this.$store.dispatch('cluster/find', {
|
|
@@ -399,6 +413,9 @@ export default {
|
|
|
399
413
|
|
|
400
414
|
// Ask to install a special chart with fixed namespace/name
|
|
401
415
|
// or edit it if there's an existing install.
|
|
416
|
+
// This is a hard blocker - installing a new instance is NOT allowed.
|
|
417
|
+
|
|
418
|
+
this.canInstallNew = false;
|
|
402
419
|
|
|
403
420
|
try {
|
|
404
421
|
this.existing = await this.$store.dispatch('cluster/find', {
|
|
@@ -410,19 +427,33 @@ export default {
|
|
|
410
427
|
this.mode = _CREATE;
|
|
411
428
|
this.existing = null;
|
|
412
429
|
}
|
|
413
|
-
} else
|
|
414
|
-
|
|
430
|
+
} else {
|
|
431
|
+
// Regular chart (not targeted) - check if there are installed instances.
|
|
432
|
+
// Installing new instances IS allowed (canInstallNew remains true).
|
|
433
|
+
const isChartDetailPage = this.$route.name === 'c-cluster-apps-charts-chart';
|
|
415
434
|
|
|
416
|
-
if (
|
|
417
|
-
this.
|
|
418
|
-
|
|
435
|
+
if (isChartDetailPage) {
|
|
436
|
+
const matching = this.chart?.matchingInstalledApps || [];
|
|
437
|
+
|
|
438
|
+
// Always refresh the available instances so stale values are removed.
|
|
439
|
+
this.installedInstances = [];
|
|
440
|
+
|
|
441
|
+
if (matching.length >= 1) {
|
|
442
|
+
// Populate the instance selector and preserve the current selection
|
|
443
|
+
// when it is still one of the matching installed apps.
|
|
444
|
+
this.installedInstances = matching;
|
|
445
|
+
const hasExistingMatch = this.existing?.id && matching.some((instance) => instance.id === this.existing.id);
|
|
446
|
+
|
|
447
|
+
if (!hasExistingMatch) {
|
|
448
|
+
this.existing = matching[0];
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
this.existing = null;
|
|
452
|
+
}
|
|
419
453
|
} else {
|
|
454
|
+
// Regular create
|
|
420
455
|
this.mode = _CREATE;
|
|
421
456
|
}
|
|
422
|
-
} else {
|
|
423
|
-
// Regular create
|
|
424
|
-
|
|
425
|
-
this.mode = _CREATE;
|
|
426
457
|
}
|
|
427
458
|
}
|
|
428
459
|
}, // End of fetchChart
|
package/mixins/resource-fetch.js
CHANGED
|
@@ -87,6 +87,14 @@ export default {
|
|
|
87
87
|
type: Function,
|
|
88
88
|
default: null,
|
|
89
89
|
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* When making a supporting HTTP request include associated resource data
|
|
93
|
+
*/
|
|
94
|
+
includeAssociatedData: {
|
|
95
|
+
type: Boolean,
|
|
96
|
+
default: false,
|
|
97
|
+
},
|
|
90
98
|
},
|
|
91
99
|
|
|
92
100
|
computed: {
|
|
@@ -185,9 +193,10 @@ export default {
|
|
|
185
193
|
return;
|
|
186
194
|
}
|
|
187
195
|
const opt = {
|
|
188
|
-
hasManualRefresh:
|
|
189
|
-
pagination:
|
|
190
|
-
force:
|
|
196
|
+
hasManualRefresh: this.hasManualRefresh,
|
|
197
|
+
pagination: { ...this.pagination },
|
|
198
|
+
force: this.paginating !== null, // Fix for manual refresh (before ripped out).
|
|
199
|
+
includeAssociatedData: this.includeAssociatedData,
|
|
191
200
|
};
|
|
192
201
|
|
|
193
202
|
if (this.apiFilter) {
|
|
@@ -87,6 +87,19 @@ const certManagerOfficialMatchingChart2 = {
|
|
|
87
87
|
}]
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
+
// Simulates the chart data fetched from the repository where the home URL has changed in newer versions
|
|
91
|
+
// AND the older installed version has been dropped/replaced from the repository index.
|
|
92
|
+
const certManagerOfficialMatchingChartNewHomeOnly = {
|
|
93
|
+
name: chartName,
|
|
94
|
+
repoName: certManagerOfficial.repoName,
|
|
95
|
+
versions: [{
|
|
96
|
+
version: latestVersion,
|
|
97
|
+
home: certManagerOfficial.home,
|
|
98
|
+
repoName: certManagerOfficial.repoName,
|
|
99
|
+
annotations: {},
|
|
100
|
+
}]
|
|
101
|
+
};
|
|
102
|
+
|
|
90
103
|
const installedCertManagerAppCoFromRancherUI = {
|
|
91
104
|
metadata: {
|
|
92
105
|
annotations: { [CATALOG_ANNOTATIONS.SOURCE_REPO_NAME]: appCo.repoName },
|
|
@@ -130,7 +143,8 @@ describe('class CatalogApp', () => {
|
|
|
130
143
|
[installedCertManagerOfficialFromRancherUI, [], APP_UPGRADE_STATUS.NO_UPGRADE],
|
|
131
144
|
[installedCertManagerOfficialFromRancherUI, [certManagerOfficialMatchingChart1], APP_UPGRADE_STATUS.SINGLE_UPGRADE],
|
|
132
145
|
[installedCertManagerOfficialFromRancherUI, [certManagerOfficialMatchingChart1, appCoMatchingChart1], APP_UPGRADE_STATUS.SINGLE_UPGRADE],
|
|
133
|
-
[installedCertManagerOfficialFromRancherUI, [certManagerOfficialMatchingChart1, certManagerOfficialMatchingChart2], APP_UPGRADE_STATUS.MULTIPLE_UPGRADES]
|
|
146
|
+
[installedCertManagerOfficialFromRancherUI, [certManagerOfficialMatchingChart1, certManagerOfficialMatchingChart2], APP_UPGRADE_STATUS.MULTIPLE_UPGRADES],
|
|
147
|
+
[installedCertManagerOfficialFromRancherUI, [certManagerOfficialMatchingChartNewHomeOnly], APP_UPGRADE_STATUS.SINGLE_UPGRADE]
|
|
134
148
|
];
|
|
135
149
|
|
|
136
150
|
it.each(testCases)('should return the correct upgrade status', (installedChart: Object, matchingCharts: any, expected: any) => {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import ClusterRepo from '../catalog.cattle.io.clusterrepo';
|
|
2
|
+
|
|
3
|
+
describe('clusterRepo', () => {
|
|
4
|
+
let model: any;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
model = new ClusterRepo({
|
|
8
|
+
metadata: { name: 'test-repo' },
|
|
9
|
+
spec: { url: 'https://test-url.com' }
|
|
10
|
+
}, {
|
|
11
|
+
getters: {},
|
|
12
|
+
dispatch: jest.fn(),
|
|
13
|
+
rootGetters: {}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
jest.spyOn(model, '_key', 'get').mockReturnValue('test-key');
|
|
17
|
+
jest.spyOn(model, 'save').mockImplementation().mockResolvedValue(true);
|
|
18
|
+
jest.spyOn(model, 'waitForState').mockImplementation().mockResolvedValue(true);
|
|
19
|
+
jest.spyOn(model, '$dispatch', 'get').mockReturnValue(jest.fn());
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('refresh', () => {
|
|
23
|
+
it('updates forceUpdate, saves, waits for active state, and dispatches load by default', async() => {
|
|
24
|
+
// Mock Date to ensure deterministic forceUpdate value
|
|
25
|
+
const mockDate = new Date('2023-01-01T12:00:00.000Z');
|
|
26
|
+
const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any);
|
|
27
|
+
|
|
28
|
+
await model.refresh();
|
|
29
|
+
|
|
30
|
+
expect(model.spec.forceUpdate).toBe('2023-01-01T12:00:00Z');
|
|
31
|
+
expect(model.save).toHaveBeenCalledWith();
|
|
32
|
+
expect(model.waitForState).toHaveBeenCalledWith('active', 10000, 1000);
|
|
33
|
+
expect(model.$dispatch).toHaveBeenCalledWith('catalog/load', { force: true, repoKeys: [model._key] }, { root: true });
|
|
34
|
+
|
|
35
|
+
spy.mockRestore();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('updates forceUpdate, saves, waits for active state, but DOES NOT dispatch load if dispatchLoad is false', async() => {
|
|
39
|
+
await model.refresh(false);
|
|
40
|
+
|
|
41
|
+
expect(model.save).toHaveBeenCalledWith();
|
|
42
|
+
expect(model.waitForState).toHaveBeenCalledWith('active', 10000, 1000);
|
|
43
|
+
expect(model.$dispatch).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('dispatches error to growl if save or waitForState fails', async() => {
|
|
47
|
+
const error = new Error('waitForState timeout');
|
|
48
|
+
|
|
49
|
+
model.waitForState.mockRejectedValue(error);
|
|
50
|
+
jest.spyOn(model, 't', 'get').mockReturnValue(jest.fn().mockReturnValue('Error refreshing repository'));
|
|
51
|
+
jest.spyOn(model, 'nameDisplay', 'get').mockReturnValue('Test Repo');
|
|
52
|
+
|
|
53
|
+
await model.refresh();
|
|
54
|
+
|
|
55
|
+
expect(model.$dispatch).toHaveBeenCalledWith('growl/fromError', {
|
|
56
|
+
title: 'Error refreshing repository',
|
|
57
|
+
err: error
|
|
58
|
+
}, { root: true });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('refreshBulk', () => {
|
|
63
|
+
it('calls refresh(false) on all items and then dispatches a single catalog/load with all repoKeys', async() => {
|
|
64
|
+
const mockItem1 = {
|
|
65
|
+
_key: 'repo-1',
|
|
66
|
+
refresh: jest.fn().mockResolvedValue(true)
|
|
67
|
+
};
|
|
68
|
+
const mockItem2 = {
|
|
69
|
+
_key: 'repo-2',
|
|
70
|
+
refresh: jest.fn().mockResolvedValue(true)
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
await model.refreshBulk([mockItem1, mockItem2]);
|
|
74
|
+
|
|
75
|
+
expect(mockItem1.refresh).toHaveBeenCalledWith(false);
|
|
76
|
+
expect(mockItem2.refresh).toHaveBeenCalledWith(false);
|
|
77
|
+
|
|
78
|
+
expect(model.$dispatch).toHaveBeenCalledWith('catalog/load', {
|
|
79
|
+
force: true,
|
|
80
|
+
repoKeys: ['repo-1', 'repo-2']
|
|
81
|
+
}, { root: true });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -17,7 +17,7 @@ type MockChartContext = {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
interface CardContent {
|
|
20
|
-
subHeaderItems: { label: string, labelTooltip?: string}[];
|
|
20
|
+
subHeaderItems: { label: string, labelTooltip?: string, icon?: string, iconTooltip?: string }[];
|
|
21
21
|
footerItems: { labels: string[]; icon?: string }[];
|
|
22
22
|
statuses: { tooltip: { key?: string; text?: string }; color: string }[];
|
|
23
23
|
}
|
|
@@ -166,7 +166,7 @@ describe('class Chart', () => {
|
|
|
166
166
|
expect(chart.isInstalled).toBe(false);
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
-
it('is
|
|
169
|
+
it('is true when multiple apps match', () => {
|
|
170
170
|
const app = makeInstalledApp();
|
|
171
171
|
|
|
172
172
|
app.spec.chart.metadata.version = '1.2.3';
|
|
@@ -174,7 +174,7 @@ describe('class Chart', () => {
|
|
|
174
174
|
|
|
175
175
|
const chart = new Chart(base, ctx);
|
|
176
176
|
|
|
177
|
-
expect(chart.isInstalled).toBe(
|
|
177
|
+
expect(chart.isInstalled).toBe(true);
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
180
|
|
|
@@ -206,6 +206,58 @@ describe('class Chart', () => {
|
|
|
206
206
|
|
|
207
207
|
expect(chart.upgradeable).toBe(false);
|
|
208
208
|
});
|
|
209
|
+
|
|
210
|
+
it('is true when at least one of multiple installed apps is upgradeable', () => {
|
|
211
|
+
const app1 = makeInstalledApp(APP_UPGRADE_STATUS.NO_UPGRADE);
|
|
212
|
+
const app2 = makeInstalledApp(APP_UPGRADE_STATUS.SINGLE_UPGRADE);
|
|
213
|
+
|
|
214
|
+
ctx.rootGetters['cluster/all'] = () => [app1, app2];
|
|
215
|
+
|
|
216
|
+
const chart = new Chart(base, ctx);
|
|
217
|
+
|
|
218
|
+
expect(chart.upgradeable).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('is false when none of multiple installed apps are upgradeable', () => {
|
|
222
|
+
const app1 = makeInstalledApp(APP_UPGRADE_STATUS.NO_UPGRADE);
|
|
223
|
+
const app2 = makeInstalledApp(APP_UPGRADE_STATUS.NO_UPGRADE);
|
|
224
|
+
|
|
225
|
+
ctx.rootGetters['cluster/all'] = () => [app1, app2];
|
|
226
|
+
|
|
227
|
+
const chart = new Chart(base, ctx);
|
|
228
|
+
|
|
229
|
+
expect(chart.upgradeable).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('installedCount', () => {
|
|
234
|
+
it('returns 0 when no apps are installed', () => {
|
|
235
|
+
const chart = new Chart(base, ctx);
|
|
236
|
+
|
|
237
|
+
expect(chart.installedCount).toBe(0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('returns 1 when one app is installed', () => {
|
|
241
|
+
const installedApp = makeInstalledApp();
|
|
242
|
+
|
|
243
|
+
ctx.rootGetters['cluster/all'] = () => [installedApp];
|
|
244
|
+
|
|
245
|
+
const chart = new Chart(base, ctx);
|
|
246
|
+
|
|
247
|
+
expect(chart.installedCount).toBe(1);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('returns correct count when multiple apps are installed', () => {
|
|
251
|
+
const app1 = makeInstalledApp();
|
|
252
|
+
const app2 = makeInstalledApp();
|
|
253
|
+
const app3 = makeInstalledApp();
|
|
254
|
+
|
|
255
|
+
ctx.rootGetters['cluster/all'] = () => [app1, app2, app3];
|
|
256
|
+
|
|
257
|
+
const chart = new Chart(base, ctx);
|
|
258
|
+
|
|
259
|
+
expect(chart.installedCount).toBe(3);
|
|
260
|
+
});
|
|
209
261
|
});
|
|
210
262
|
|
|
211
263
|
describe('cardContent', () => {
|
|
@@ -274,6 +326,25 @@ describe('class Chart', () => {
|
|
|
274
326
|
expect(installedStatus?.tooltip?.text).toContain(installedApp.spec.chart.metadata.version);
|
|
275
327
|
});
|
|
276
328
|
|
|
329
|
+
it('does not include version in installed tooltip when multiple instances exist', () => {
|
|
330
|
+
const app1 = makeInstalledApp();
|
|
331
|
+
const app2 = makeInstalledApp();
|
|
332
|
+
|
|
333
|
+
app2.spec.chart.metadata.version = '1.2.0';
|
|
334
|
+
ctx.rootGetters['cluster/all'] = () => [app1, app2];
|
|
335
|
+
|
|
336
|
+
const chart = new Chart(base, ctx);
|
|
337
|
+
|
|
338
|
+
const result = chart.cardContent as CardContent;
|
|
339
|
+
|
|
340
|
+
const installedStatus = result.statuses.find((s) => s.tooltip?.text?.startsWith('generic.installedMultiple'));
|
|
341
|
+
|
|
342
|
+
expect(installedStatus).toBeDefined();
|
|
343
|
+
expect(installedStatus?.color).toBe('success');
|
|
344
|
+
// Should not contain version number when multiple instances
|
|
345
|
+
expect(installedStatus?.tooltip?.text).toBe('generic.installedMultiple');
|
|
346
|
+
});
|
|
347
|
+
|
|
277
348
|
it('includes upgradeable status when upgrade is available', () => {
|
|
278
349
|
const installedApp = makeInstalledApp(APP_UPGRADE_STATUS.SINGLE_UPGRADE);
|
|
279
350
|
|
|
@@ -332,10 +403,32 @@ describe('class Chart', () => {
|
|
|
332
403
|
});
|
|
333
404
|
|
|
334
405
|
const result = chart.cardContent as CardContent;
|
|
335
|
-
const lastUpdatedItem = result.subHeaderItems[1];
|
|
336
406
|
|
|
337
|
-
expect(
|
|
338
|
-
expect(
|
|
407
|
+
expect(result.subHeaderItems).toHaveLength(1);
|
|
408
|
+
expect(result.subHeaderItems[0].icon).toBe('icon-version-alt');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('handles falsy time for last updated date', () => {
|
|
412
|
+
const chartWithFalsyTime = {
|
|
413
|
+
...base,
|
|
414
|
+
versions: [{
|
|
415
|
+
...base.versions[0],
|
|
416
|
+
created: '',
|
|
417
|
+
}]
|
|
418
|
+
};
|
|
419
|
+
const chart = new Chart(chartWithFalsyTime, {
|
|
420
|
+
rootGetters: {
|
|
421
|
+
'cluster/all': () => [],
|
|
422
|
+
'i18n/t': (key: string) => key,
|
|
423
|
+
currentCluster: { workerOSs: [] },
|
|
424
|
+
'prefs/get': () => false,
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const result = chart.cardContent as CardContent;
|
|
429
|
+
|
|
430
|
+
expect(result.subHeaderItems).toHaveLength(1);
|
|
431
|
+
expect(result.subHeaderItems[0].icon).toBe('icon-version-alt');
|
|
339
432
|
});
|
|
340
433
|
});
|
|
341
434
|
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import Feature from '@shell/models/management.cattle.io.feature.js';
|
|
2
|
+
import Resource from '@shell/plugins/dashboard-store/resource-class';
|
|
3
|
+
|
|
4
|
+
describe('class Feature', () => {
|
|
5
|
+
const ctx = {
|
|
6
|
+
dispatch: jest.fn(),
|
|
7
|
+
rootGetters: { 'i18n/t': (key: string) => key },
|
|
8
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// The parent Resource._availableActions getter depends on runtime config we don't have
|
|
12
|
+
// in tests — stub it out so we can assert on Feature's own additions.
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.spyOn(Resource.prototype, '_availableActions', 'get').mockReturnValue([]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
jest.restoreAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('enabled getter', () => {
|
|
22
|
+
it.each([
|
|
23
|
+
[true, true],
|
|
24
|
+
[false, false],
|
|
25
|
+
])('should return lockedValue (%s) when status.lockedValue is not null', (lockedValue, expected) => {
|
|
26
|
+
const feature = new Feature({
|
|
27
|
+
spec: { value: false },
|
|
28
|
+
status: { lockedValue, default: false }
|
|
29
|
+
}, ctx);
|
|
30
|
+
|
|
31
|
+
expect(feature.enabled).toBe(expected);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return spec.value when lockedValue is null and spec.value is set', () => {
|
|
35
|
+
const feature = new Feature({
|
|
36
|
+
spec: { value: true },
|
|
37
|
+
status: { lockedValue: null, default: false }
|
|
38
|
+
}, ctx);
|
|
39
|
+
|
|
40
|
+
expect(feature.enabled).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should fall back to status.default when lockedValue is null and spec.value is null', () => {
|
|
44
|
+
const feature = new Feature({
|
|
45
|
+
spec: { value: null },
|
|
46
|
+
status: { lockedValue: null, default: true }
|
|
47
|
+
}, ctx);
|
|
48
|
+
|
|
49
|
+
expect(feature.enabled).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should not throw when status is missing (malformed feature flag)', () => {
|
|
53
|
+
const feature = new Feature({ spec: { value: true } }, ctx);
|
|
54
|
+
|
|
55
|
+
expect(() => feature.enabled).not.toThrow();
|
|
56
|
+
expect(feature.enabled).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('restartRequired getter', () => {
|
|
61
|
+
it('should return false when status.dynamic is true', () => {
|
|
62
|
+
const feature = new Feature({ spec: {}, status: { dynamic: true, lockedValue: null } }, ctx);
|
|
63
|
+
|
|
64
|
+
expect(feature.restartRequired).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return true when status.dynamic is false', () => {
|
|
68
|
+
const feature = new Feature({ spec: {}, status: { dynamic: false, lockedValue: null } }, ctx);
|
|
69
|
+
|
|
70
|
+
expect(feature.restartRequired).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should return true when status is missing (malformed feature flag)', () => {
|
|
74
|
+
const feature = new Feature({ spec: {} }, ctx);
|
|
75
|
+
|
|
76
|
+
expect(() => feature.restartRequired).not.toThrow();
|
|
77
|
+
expect(feature.restartRequired).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('_availableActions getter', () => {
|
|
82
|
+
it('should disable the toggle action when lockedValue is not null', () => {
|
|
83
|
+
const feature = new Feature({
|
|
84
|
+
spec: { value: false },
|
|
85
|
+
status: {
|
|
86
|
+
lockedValue: true, default: false, dynamic: true
|
|
87
|
+
},
|
|
88
|
+
}, ctx);
|
|
89
|
+
|
|
90
|
+
jest.spyOn(feature, 'canUpdate', 'get').mockReturnValue(true);
|
|
91
|
+
|
|
92
|
+
const actions = feature._availableActions;
|
|
93
|
+
|
|
94
|
+
expect(actions[0].action).toBe('toggleFeatureFlag');
|
|
95
|
+
expect(actions[0].enabled).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should enable the toggle action when lockedValue is null and user canUpdate', () => {
|
|
99
|
+
const feature = new Feature({
|
|
100
|
+
spec: { value: false },
|
|
101
|
+
status: {
|
|
102
|
+
lockedValue: null, default: false, dynamic: true
|
|
103
|
+
},
|
|
104
|
+
id: 'some-feature',
|
|
105
|
+
}, ctx);
|
|
106
|
+
|
|
107
|
+
jest.spyOn(feature, 'canUpdate', 'get').mockReturnValue(true);
|
|
108
|
+
|
|
109
|
+
const actions = feature._availableActions;
|
|
110
|
+
|
|
111
|
+
expect(actions[0].action).toBe('toggleFeatureFlag');
|
|
112
|
+
expect(actions[0].enabled).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should not throw and should disable the toggle action when status is missing (malformed feature flag)', () => {
|
|
116
|
+
const feature = new Feature({
|
|
117
|
+
spec: { value: false },
|
|
118
|
+
id: 'some-feature',
|
|
119
|
+
}, ctx);
|
|
120
|
+
|
|
121
|
+
jest.spyOn(feature, 'canUpdate', 'get').mockReturnValue(true);
|
|
122
|
+
|
|
123
|
+
expect(() => feature._availableActions).not.toThrow();
|
|
124
|
+
|
|
125
|
+
const actions = feature._availableActions;
|
|
126
|
+
|
|
127
|
+
expect(actions[0].action).toBe('toggleFeatureFlag');
|
|
128
|
+
expect(actions[0].enabled).toBeFalsy();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import AlertmanagerConfig from '@shell/models/monitoring.coreos.com.alertmanagerconfig';
|
|
2
|
+
|
|
3
|
+
const base = {
|
|
4
|
+
apiVersion: 'monitoring.coreos.com/v1alpha1',
|
|
5
|
+
kind: 'AlertmanagerConfig',
|
|
6
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const build = (data: Record<string, any>) => new AlertmanagerConfig(data) as any;
|
|
10
|
+
|
|
11
|
+
describe('class AlertmanagerConfig', () => {
|
|
12
|
+
describe('applyDefaults', () => {
|
|
13
|
+
it('on a fresh resource, seeds the route with defaults and no match/matchRe', () => {
|
|
14
|
+
const amc = build({ ...base });
|
|
15
|
+
|
|
16
|
+
amc.applyDefaults();
|
|
17
|
+
|
|
18
|
+
expect(amc.spec.receivers).toStrictEqual([]);
|
|
19
|
+
expect(amc.spec.route).toStrictEqual({
|
|
20
|
+
groupBy: [],
|
|
21
|
+
groupWait: '30s',
|
|
22
|
+
groupInterval: '5m',
|
|
23
|
+
repeatInterval: '4h',
|
|
24
|
+
matchers: [],
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('backfills route defaults on a resource loaded without a route', () => {
|
|
29
|
+
const amc = build({
|
|
30
|
+
...base,
|
|
31
|
+
spec: { receivers: [{ name: 'existing' }] },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
amc.applyDefaults();
|
|
35
|
+
|
|
36
|
+
expect(amc.spec.route).toBeDefined();
|
|
37
|
+
expect(amc.spec.route.receiver).toBeUndefined();
|
|
38
|
+
expect(amc.spec.route.matchers).toStrictEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('preserves existing matchers on load', () => {
|
|
42
|
+
const matchers = [{
|
|
43
|
+
name: 'severity', value: 'warning', matchType: '='
|
|
44
|
+
}];
|
|
45
|
+
const amc = build({
|
|
46
|
+
...base,
|
|
47
|
+
spec: {
|
|
48
|
+
receivers: [{ name: 'existing' }],
|
|
49
|
+
route: { receiver: 'existing', matchers },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
amc.applyDefaults();
|
|
54
|
+
|
|
55
|
+
expect(amc.spec.route.matchers).toStrictEqual(matchers);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('cleanForSave', () => {
|
|
60
|
+
it('drops spec.route when no receiver is set — this is what fixes #17347 on 109+ charts', () => {
|
|
61
|
+
const amc = build({ ...base });
|
|
62
|
+
|
|
63
|
+
const out = amc.cleanForSave({
|
|
64
|
+
...base,
|
|
65
|
+
spec: {
|
|
66
|
+
receivers: [],
|
|
67
|
+
route: {
|
|
68
|
+
groupBy: [],
|
|
69
|
+
groupWait: '30s',
|
|
70
|
+
groupInterval: '5m',
|
|
71
|
+
repeatInterval: '4h',
|
|
72
|
+
matchers: [],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
}, true);
|
|
76
|
+
|
|
77
|
+
expect(out.spec.route).toBeUndefined();
|
|
78
|
+
expect(out.spec.receivers).toStrictEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('keeps spec.route when a receiver is set', () => {
|
|
82
|
+
const amc = build({ ...base });
|
|
83
|
+
|
|
84
|
+
const out = amc.cleanForSave({
|
|
85
|
+
...base,
|
|
86
|
+
spec: {
|
|
87
|
+
receivers: [{ name: 'existing' }],
|
|
88
|
+
route: {
|
|
89
|
+
receiver: 'existing', groupBy: [], matchers: []
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
}, false);
|
|
93
|
+
|
|
94
|
+
expect(out.spec.route).toBeDefined();
|
|
95
|
+
expect(out.spec.route.receiver).toBe('existing');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -78,28 +78,32 @@ export default class CatalogApp extends SteveModel {
|
|
|
78
78
|
return [];
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
if (!repoName || matchingCharts.length > 1) {
|
|
82
|
+
// Filtering matches by verifying if the current version is in the matched chart's available versions, and that the home value matches as well
|
|
83
|
+
const thisHome = chart?.metadata?.home;
|
|
84
|
+
const bestMatches = matchingCharts.filter(({ versions }) => {
|
|
85
|
+
// First checking if the latest version has the same home value
|
|
86
|
+
if (thisHome === versions[0]?.home) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
for (let i = 1; i < versions.length; i++) {
|
|
91
|
+
const { version, home } = versions[i];
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
// Finding the exact version, if the version is not there, then most likely it's not a match
|
|
94
|
+
// if the exact version is found, then we can compare the home value
|
|
95
|
+
if (version === this.currentVersion && (home === thisHome)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
96
98
|
}
|
|
97
|
-
}
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
return false;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return bestMatches;
|
|
104
|
+
}
|
|
101
105
|
|
|
102
|
-
return
|
|
106
|
+
return matchingCharts;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
get currentVersion() {
|