@rancher/shell 3.0.12-rc.1 → 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/translations/en-us.yaml +19 -17
- package/assets/translations/zh-hans.yaml +4 -8
- 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/SelectIconGrid.vue +5 -0
- package/components/__tests__/CountBox.test.ts +72 -0
- package/components/__tests__/DetailText.test.ts +113 -0
- package/components/fleet/FleetClusterTargets/index.vue +18 -1
- package/components/form/InputWithSelect.vue +18 -10
- package/components/form/KeyValue.vue +17 -1
- package/components/form/LabeledSelect.vue +82 -24
- 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/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
- package/components/nav/Group.vue +7 -6
- package/components/nav/Header.vue +24 -3
- 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/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/query-params.js +1 -0
- package/config/router/routes.js +0 -8
- package/core/__tests__/plugin-products.test.ts +616 -25
- package/core/plugin-products-base.ts +31 -14
- package/core/plugin-products-helpers.ts +5 -4
- package/core/plugin-types.ts +18 -3
- package/core/types.ts +3 -1
- package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
- package/detail/management.cattle.io.fleetworkspace.vue +49 -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/index.vue +70 -99
- 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/machine-config/amazonec2.vue +1 -0
- package/mixins/chart.js +40 -9
- 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/package.json +13 -12
- 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/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 +15 -10
- package/pages/c/_cluster/uiplugins/index.vue +23 -25
- 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/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/auth.js +0 -3
- package/store/catalog.js +60 -8
- package/types/shell/index.d.ts +26 -22
- 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/object.js +33 -2
- package/utils/time.ts +5 -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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Chart from '@shell/pages/c/_cluster/apps/charts/chart.vue';
|
|
2
|
+
import { APP_UPGRADE_STATUS } from '@shell/store/catalog';
|
|
2
3
|
|
|
3
4
|
jest.mock('clipboard-polyfill', () => ({ writeText: () => {} }));
|
|
4
5
|
|
|
@@ -132,4 +133,192 @@ describe('page: Chart Detail', () => {
|
|
|
132
133
|
});
|
|
133
134
|
});
|
|
134
135
|
});
|
|
136
|
+
|
|
137
|
+
describe('methods: computeSelectedAppStatuses', () => {
|
|
138
|
+
const defaultStatuses = [
|
|
139
|
+
{
|
|
140
|
+
icon: 'icon-alert-alt', color: 'error', tooltip: { key: 'generic.deprecated' }
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
icon: 'icon-upgrade-alt', color: 'info', tooltip: { key: 'generic.upgradeable' }
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
icon: 'icon-confirmation-alt', color: 'success', tooltip: { text: 'generic.installed (1.0.0)' }
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
it('returns only deprecated status when no app is selected', () => {
|
|
151
|
+
const thisContext = {
|
|
152
|
+
selectedInstalledApp: null,
|
|
153
|
+
t: (key: string) => key
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const result = (Chart.methods!.computeSelectedAppStatuses as (statuses: any[]) => any[]).call(thisContext, defaultStatuses);
|
|
157
|
+
|
|
158
|
+
expect(result).toHaveLength(1);
|
|
159
|
+
expect(result[0].icon).toBe('icon-alert-alt');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('returns empty array when no app is selected and chart is not deprecated', () => {
|
|
163
|
+
const thisContext = {
|
|
164
|
+
selectedInstalledApp: null,
|
|
165
|
+
t: (key: string) => key
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const statusesWithoutDeprecated = [
|
|
169
|
+
{
|
|
170
|
+
icon: 'icon-upgrade-alt', color: 'info', tooltip: { key: 'generic.upgradeable' }
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
icon: 'icon-confirmation-alt', color: 'success', tooltip: { text: 'generic.installed (1.0.0)' }
|
|
174
|
+
}
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const result = (Chart.methods!.computeSelectedAppStatuses as (statuses: any[]) => any[]).call(thisContext, statusesWithoutDeprecated);
|
|
178
|
+
|
|
179
|
+
expect(result).toHaveLength(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('returns installed status with version for selected app', () => {
|
|
183
|
+
const thisContext = {
|
|
184
|
+
selectedInstalledApp: {
|
|
185
|
+
upgradeAvailable: APP_UPGRADE_STATUS.NO_UPGRADE,
|
|
186
|
+
spec: { chart: { metadata: { version: '2.0.0' } } }
|
|
187
|
+
},
|
|
188
|
+
t: (key: string) => key
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const result = (Chart.methods!.computeSelectedAppStatuses as (statuses: any[]) => any[]).call(thisContext, defaultStatuses);
|
|
192
|
+
|
|
193
|
+
const installedStatus = result.find((s: any) => s.icon === 'icon-confirmation-alt');
|
|
194
|
+
|
|
195
|
+
expect(installedStatus).toBeDefined();
|
|
196
|
+
expect(installedStatus.tooltip.text).toContain('2.0.0');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('returns upgradeable status when selected app is upgradeable', () => {
|
|
200
|
+
const thisContext = {
|
|
201
|
+
selectedInstalledApp: {
|
|
202
|
+
upgradeAvailable: APP_UPGRADE_STATUS.SINGLE_UPGRADE,
|
|
203
|
+
spec: { chart: { metadata: { version: '1.5.0' } } }
|
|
204
|
+
},
|
|
205
|
+
t: (key: string) => key
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const result = (Chart.methods!.computeSelectedAppStatuses as (statuses: any[]) => any[]).call(thisContext, defaultStatuses);
|
|
209
|
+
|
|
210
|
+
const upgradeableStatus = result.find((s: any) => s.icon === 'icon-upgrade-alt');
|
|
211
|
+
|
|
212
|
+
expect(upgradeableStatus).toBeDefined();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('does not include upgradeable status when selected app is not upgradeable', () => {
|
|
216
|
+
const thisContext = {
|
|
217
|
+
selectedInstalledApp: {
|
|
218
|
+
upgradeAvailable: APP_UPGRADE_STATUS.NO_UPGRADE,
|
|
219
|
+
spec: { chart: { metadata: { version: '1.5.0' } } }
|
|
220
|
+
},
|
|
221
|
+
t: (key: string) => key
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const result = (Chart.methods!.computeSelectedAppStatuses as (statuses: any[]) => any[]).call(thisContext, defaultStatuses);
|
|
225
|
+
|
|
226
|
+
const upgradeableStatus = result.find((s: any) => s.icon === 'icon-upgrade-alt');
|
|
227
|
+
|
|
228
|
+
expect(upgradeableStatus).toBeUndefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('includes all relevant statuses: deprecated, upgradeable, and installed', () => {
|
|
232
|
+
const thisContext = {
|
|
233
|
+
selectedInstalledApp: {
|
|
234
|
+
upgradeAvailable: APP_UPGRADE_STATUS.SINGLE_UPGRADE,
|
|
235
|
+
spec: { chart: { metadata: { version: '1.5.0' } } }
|
|
236
|
+
},
|
|
237
|
+
t: (key: string) => key
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const result = (Chart.methods!.computeSelectedAppStatuses as (statuses: any[]) => any[]).call(thisContext, defaultStatuses);
|
|
241
|
+
|
|
242
|
+
expect(result).toHaveLength(3);
|
|
243
|
+
|
|
244
|
+
const hasDeprecated = result.some((s: any) => s.icon === 'icon-alert-alt');
|
|
245
|
+
const hasUpgradeable = result.some((s: any) => s.icon === 'icon-upgrade-alt');
|
|
246
|
+
const hasInstalled = result.some((s: any) => s.icon === 'icon-confirmation-alt');
|
|
247
|
+
|
|
248
|
+
expect(hasDeprecated).toBe(true);
|
|
249
|
+
expect(hasUpgradeable).toBe(true);
|
|
250
|
+
expect(hasInstalled).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('computed: installedAppOptions', () => {
|
|
255
|
+
const makeApp = (namespace: string, name: string, upgradeStatus: string) => ({
|
|
256
|
+
id: `${ namespace }/${ name }`,
|
|
257
|
+
metadata: { namespace, name },
|
|
258
|
+
upgradeAvailable: upgradeStatus
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('returns empty array when no installed instances', () => {
|
|
262
|
+
const thisContext = {
|
|
263
|
+
installedInstances: [],
|
|
264
|
+
t: (key: string) => key
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const result = (Chart.computed!.installedAppOptions as () => any[]).call(thisContext);
|
|
268
|
+
|
|
269
|
+
expect(result).toStrictEqual([]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('returns options without upgradeable suffix for non-upgradeable apps', () => {
|
|
273
|
+
const thisContext = {
|
|
274
|
+
installedInstances: [
|
|
275
|
+
makeApp('default', 'my-app', APP_UPGRADE_STATUS.NO_UPGRADE)
|
|
276
|
+
],
|
|
277
|
+
t: (key: string) => key
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const result = (Chart.computed!.installedAppOptions as () => any[]).call(thisContext);
|
|
281
|
+
|
|
282
|
+
expect(result).toHaveLength(1);
|
|
283
|
+
expect(result[0]).toStrictEqual({
|
|
284
|
+
value: 'default/my-app',
|
|
285
|
+
label: 'default/my-app'
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('adds upgradeable suffix for apps with available upgrade', () => {
|
|
290
|
+
const thisContext = {
|
|
291
|
+
installedInstances: [
|
|
292
|
+
makeApp('cattle-system', 'rancher-app', APP_UPGRADE_STATUS.SINGLE_UPGRADE)
|
|
293
|
+
],
|
|
294
|
+
t: (key: string) => key
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const result = (Chart.computed!.installedAppOptions as () => any[]).call(thisContext);
|
|
298
|
+
|
|
299
|
+
expect(result).toHaveLength(1);
|
|
300
|
+
expect(result[0]).toStrictEqual({
|
|
301
|
+
value: 'cattle-system/rancher-app',
|
|
302
|
+
label: 'cattle-system/rancher-app (generic.upgradeable)'
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('correctly labels mixed upgradeable and non-upgradeable apps', () => {
|
|
307
|
+
const thisContext = {
|
|
308
|
+
installedInstances: [
|
|
309
|
+
makeApp('default', 'app-one', APP_UPGRADE_STATUS.NO_UPGRADE),
|
|
310
|
+
makeApp('kube-system', 'app-two', APP_UPGRADE_STATUS.SINGLE_UPGRADE),
|
|
311
|
+
makeApp('monitoring', 'app-three', APP_UPGRADE_STATUS.NOT_APPLICABLE)
|
|
312
|
+
],
|
|
313
|
+
t: (key: string) => key
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const result = (Chart.computed!.installedAppOptions as () => any[]).call(thisContext);
|
|
317
|
+
|
|
318
|
+
expect(result).toHaveLength(3);
|
|
319
|
+
expect(result[0].label).toBe('default/app-one');
|
|
320
|
+
expect(result[1].label).toBe('kube-system/app-two (generic.upgradeable)');
|
|
321
|
+
expect(result[2].label).toBe('monitoring/app-three');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
135
324
|
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import Charts from '@shell/pages/c/_cluster/apps/charts/index.vue';
|
|
2
|
+
import { UI_PLUGIN_ANNOTATION } from '@shell/config/uiplugins';
|
|
3
|
+
|
|
4
|
+
describe('page: Charts Index', () => {
|
|
5
|
+
describe('computed: tagOptions', () => {
|
|
6
|
+
it('should gather tags exclusively from enabledCharts, omitting tags from hidden or extension charts present in allCharts (e.g., primeOnly)', () => {
|
|
7
|
+
const thisContext = {
|
|
8
|
+
// allCharts contains a UI plugin/extension chart with the 'primeOnly' tag.
|
|
9
|
+
allCharts: [
|
|
10
|
+
{ tags: ['appTag1'] },
|
|
11
|
+
{ tags: ['primeOnly'], annotations: { [UI_PLUGIN_ANNOTATION.NAME]: UI_PLUGIN_ANNOTATION.VALUE } },
|
|
12
|
+
],
|
|
13
|
+
// enabledCharts has already filtered out the extension chart.
|
|
14
|
+
enabledCharts: [
|
|
15
|
+
{ tags: ['appTag1'] },
|
|
16
|
+
]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const result = (Charts.computed!.tagOptions as () => any[]).call(thisContext);
|
|
20
|
+
|
|
21
|
+
expect(result).toStrictEqual([
|
|
22
|
+
{ value: 'apptag1', label: 'appTag1' },
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const hasPrimeOnly = result.some((tag) => tag.value === 'primeonly');
|
|
26
|
+
|
|
27
|
+
expect(hasPrimeOnly).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('computed: categoryOptions', () => {
|
|
32
|
+
it('should gather categories exclusively from enabledCharts, omitting categories from hidden or extension charts present in allCharts', () => {
|
|
33
|
+
const thisContext = {
|
|
34
|
+
allCharts: [
|
|
35
|
+
{ categories: ['category1'] },
|
|
36
|
+
{ categories: ['categoryFromExtension1'], annotations: { [UI_PLUGIN_ANNOTATION.NAME]: UI_PLUGIN_ANNOTATION.VALUE } },
|
|
37
|
+
],
|
|
38
|
+
enabledCharts: [
|
|
39
|
+
{ categories: ['category1'] },
|
|
40
|
+
],
|
|
41
|
+
$store: { getters: { 'i18n/withFallback': (key: string, fallback: any, c: string) => c } }
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const result = (Charts.computed!.categoryOptions as () => any[]).call(thisContext);
|
|
45
|
+
|
|
46
|
+
expect(result).toStrictEqual([
|
|
47
|
+
{ value: 'category1', label: 'category1' },
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const hasExtensionCategory = result.some((cat) => cat.value === 'categoryFromExtension1');
|
|
51
|
+
|
|
52
|
+
expect(hasExtensionCategory).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -91,4 +91,57 @@ describe('page: Install', () => {
|
|
|
91
91
|
expect(wrapper.vm.forceNamespace).toBe('custom-ns');
|
|
92
92
|
expect(wrapper.vm.value.metadata.name).toBe('custom-name');
|
|
93
93
|
});
|
|
94
|
+
|
|
95
|
+
describe('cancel()', () => {
|
|
96
|
+
it('should route to appLocation if chart is not defined and specific query flags are absent', () => {
|
|
97
|
+
const mockReplace = jest.fn();
|
|
98
|
+
const expectedLocation = { name: 'app-location' };
|
|
99
|
+
|
|
100
|
+
const wrapper = mount(Install, {
|
|
101
|
+
global: {
|
|
102
|
+
mocks: {
|
|
103
|
+
$store: {
|
|
104
|
+
getters: {
|
|
105
|
+
'i18n/t': (key: string) => key,
|
|
106
|
+
'cluster/id': 'cluster-id',
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
$route: { query: {} },
|
|
110
|
+
$router: { replace: mockReplace },
|
|
111
|
+
$fetchState: { pending: false },
|
|
112
|
+
},
|
|
113
|
+
stubs: {
|
|
114
|
+
Loading: true,
|
|
115
|
+
Wizard: true,
|
|
116
|
+
Banner: true,
|
|
117
|
+
Checkbox: true,
|
|
118
|
+
LabeledInput: true,
|
|
119
|
+
LabeledSelect: true,
|
|
120
|
+
NameNsDescription: true,
|
|
121
|
+
Tabbed: true,
|
|
122
|
+
Questions: true,
|
|
123
|
+
YamlEditor: true,
|
|
124
|
+
ResourceCancelModal: true,
|
|
125
|
+
UnitInput: true,
|
|
126
|
+
TypeDescription: true,
|
|
127
|
+
LazyImage: true,
|
|
128
|
+
ChartReadme: true,
|
|
129
|
+
ButtonGroup: true,
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
data() {
|
|
133
|
+
return {
|
|
134
|
+
existing: false,
|
|
135
|
+
chart: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
jest.spyOn((wrapper.vm as any), 'appLocation').mockReturnValue(expectedLocation);
|
|
141
|
+
|
|
142
|
+
(wrapper.vm as any).cancel();
|
|
143
|
+
|
|
144
|
+
expect(mockReplace).toHaveBeenCalledWith(expectedLocation);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
94
147
|
});
|
|
@@ -4,19 +4,23 @@ import ChartMixin from '@shell/mixins/chart';
|
|
|
4
4
|
import { Banner } from '@components/Banner';
|
|
5
5
|
import ChartReadme from '@shell/components/ChartReadme';
|
|
6
6
|
import LazyImage from '@shell/components/LazyImage';
|
|
7
|
+
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
7
8
|
import isEqual from 'lodash/isEqual';
|
|
8
9
|
import {
|
|
9
|
-
CHART, REPO, REPO_TYPE, VERSION, SEARCH_QUERY, CATEGORY, TAG, DEPRECATED
|
|
10
|
+
CHART, REPO, REPO_TYPE, VERSION, SEARCH_QUERY, CATEGORY, TAG, DEPRECATED, NAMESPACE, NAME, NEW_APP_INSTANCE, _FLAGGED
|
|
10
11
|
} from '@shell/config/query-params';
|
|
11
12
|
import { DATE_FORMAT } from '@shell/store/prefs';
|
|
12
|
-
import {
|
|
13
|
+
import { isMissingDate } from '@shell/utils/time';
|
|
13
14
|
import { escapeHtml } from '@shell/utils/string';
|
|
14
15
|
import { mapGetters } from 'vuex';
|
|
15
|
-
import { compatibleVersionsFor } from '@shell/store/catalog';
|
|
16
|
+
import { APP_UPGRADE_STATUS, compatibleVersionsFor } from '@shell/store/catalog';
|
|
17
|
+
import { compareChartVersions } from '@shell/utils/chart';
|
|
16
18
|
import AppChartCardSubHeader from '@shell/pages/c/_cluster/apps/charts/AppChartCardSubHeader';
|
|
17
19
|
import AppChartCardFooter from '@shell/pages/c/_cluster/apps/charts/AppChartCardFooter';
|
|
18
20
|
import day from 'dayjs';
|
|
19
21
|
import { RcButton } from '@components/RcButton';
|
|
22
|
+
import { RcButtonSplit } from '@components/RcButtonSplit';
|
|
23
|
+
import { RcDropdownItem } from '@components/RcDropdown';
|
|
20
24
|
|
|
21
25
|
export default {
|
|
22
26
|
components: {
|
|
@@ -24,9 +28,12 @@ export default {
|
|
|
24
28
|
ChartReadme,
|
|
25
29
|
LazyImage,
|
|
26
30
|
Loading,
|
|
31
|
+
LabeledSelect,
|
|
27
32
|
AppChartCardSubHeader,
|
|
28
33
|
AppChartCardFooter,
|
|
29
|
-
RcButton
|
|
34
|
+
RcButton,
|
|
35
|
+
RcButtonSplit,
|
|
36
|
+
RcDropdownItem
|
|
30
37
|
},
|
|
31
38
|
|
|
32
39
|
mixins: [
|
|
@@ -40,9 +47,9 @@ export default {
|
|
|
40
47
|
data() {
|
|
41
48
|
return {
|
|
42
49
|
SEARCH_QUERY,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
showLastVersions: 7,
|
|
51
|
+
showMoreVersions: false,
|
|
52
|
+
selectedInstalledAppId: null,
|
|
46
53
|
};
|
|
47
54
|
},
|
|
48
55
|
|
|
@@ -50,9 +57,15 @@ export default {
|
|
|
50
57
|
...mapGetters(['currentCluster']),
|
|
51
58
|
|
|
52
59
|
headerContent() {
|
|
60
|
+
const cardContent = this.chart.cardContent;
|
|
61
|
+
|
|
62
|
+
// Override statuses based on selected installed app for the detail page
|
|
63
|
+
const statuses = this.computeSelectedAppStatuses(cardContent.statuses);
|
|
64
|
+
|
|
53
65
|
return {
|
|
54
|
-
...
|
|
55
|
-
|
|
66
|
+
...cardContent,
|
|
67
|
+
statuses,
|
|
68
|
+
subHeaderItems: cardContent.subHeaderItems.map((item, i) => i === 0 ? ({
|
|
56
69
|
icon: 'icon-version-alt',
|
|
57
70
|
iconTooltip: { key: 'tableHeaders.version' },
|
|
58
71
|
label: this.query.versionName
|
|
@@ -138,6 +151,57 @@ export default {
|
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
return '';
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Returns the currently selected installed app.
|
|
158
|
+
* Falls back to this.existing (set by the mixin for targeted charts or URL query params).
|
|
159
|
+
*/
|
|
160
|
+
selectedInstalledApp() {
|
|
161
|
+
if (this.selectedInstalledAppId) {
|
|
162
|
+
return this.installedInstances?.find((app) => app.id === this.selectedInstalledAppId) || null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this.existing || null;
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Returns the action for the current state based on the selected app and target version.
|
|
170
|
+
*/
|
|
171
|
+
currentAction() {
|
|
172
|
+
const app = this.selectedInstalledApp;
|
|
173
|
+
|
|
174
|
+
if (!app) {
|
|
175
|
+
return { tKey: 'install', icon: 'plus' };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const installedVersion = app.spec?.chart?.metadata?.version;
|
|
179
|
+
|
|
180
|
+
if (installedVersion === this.targetVersion) {
|
|
181
|
+
return { tKey: 'edit', icon: 'edit' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (compareChartVersions(installedVersion, this.targetVersion) < 0) {
|
|
185
|
+
return { tKey: 'upgrade', icon: 'upgrade-alt' };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { tKey: 'downgrade', icon: 'downgrade-alt' };
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Returns options for the installed apps selector dropdown.
|
|
193
|
+
* Adds "(Upgradeable)" suffix to apps that have an upgrade available.
|
|
194
|
+
*/
|
|
195
|
+
installedAppOptions() {
|
|
196
|
+
return (this.installedInstances || []).map((app) => {
|
|
197
|
+
const isUpgradeable = app.upgradeAvailable === APP_UPGRADE_STATUS.SINGLE_UPGRADE;
|
|
198
|
+
const baseName = `${ app?.metadata?.namespace }/${ app?.metadata?.name }`;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
value: app.id,
|
|
202
|
+
label: isUpgradeable ? `${ baseName } (${ this.t('generic.upgradeable').toLowerCase() })` : baseName
|
|
203
|
+
};
|
|
204
|
+
});
|
|
141
205
|
}
|
|
142
206
|
|
|
143
207
|
},
|
|
@@ -153,7 +217,85 @@ export default {
|
|
|
153
217
|
},
|
|
154
218
|
|
|
155
219
|
methods: {
|
|
156
|
-
|
|
220
|
+
isMissingDate,
|
|
221
|
+
/**
|
|
222
|
+
* Computes statuses for the chart detail page based on the selected installed app.
|
|
223
|
+
* When an app is selected, shows instance-specific installed/upgradeable status.
|
|
224
|
+
* When no app is selected (fresh install), returns only deprecated status if applicable.
|
|
225
|
+
*
|
|
226
|
+
* @param {Array} defaultStatuses - The default statuses from chart.cardContent
|
|
227
|
+
* @returns {Array} Statuses array for the selected app context
|
|
228
|
+
*/
|
|
229
|
+
computeSelectedAppStatuses(defaultStatuses) {
|
|
230
|
+
const statuses = [];
|
|
231
|
+
|
|
232
|
+
// Always include deprecated status if present
|
|
233
|
+
const deprecatedStatus = defaultStatuses.find((s) => s.icon === 'icon-alert-alt');
|
|
234
|
+
|
|
235
|
+
if (deprecatedStatus) {
|
|
236
|
+
statuses.push(deprecatedStatus);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const selectedApp = this.selectedInstalledApp;
|
|
240
|
+
|
|
241
|
+
if (!selectedApp) {
|
|
242
|
+
// No app selected - fresh install, no installed/upgradeable status
|
|
243
|
+
return statuses;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check if the selected app is upgradeable
|
|
247
|
+
const isUpgradeable = selectedApp.upgradeAvailable === APP_UPGRADE_STATUS.SINGLE_UPGRADE;
|
|
248
|
+
|
|
249
|
+
if (isUpgradeable) {
|
|
250
|
+
statuses.push({
|
|
251
|
+
icon: 'icon-upgrade-alt', color: 'info', tooltip: { key: 'generic.upgradeable' }
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Add installed status with version for the selected app
|
|
256
|
+
const installedVersion = selectedApp.spec?.chart?.metadata?.version;
|
|
257
|
+
|
|
258
|
+
statuses.push({
|
|
259
|
+
icon: 'icon-confirmation-alt', color: 'success', tooltip: { text: `${ this.t('generic.installed') } (${ installedVersion })` }
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return statuses;
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Navigate to install page for the currently selected installed app (edit/upgrade/downgrade).
|
|
267
|
+
*/
|
|
268
|
+
executeAction() {
|
|
269
|
+
const app = this.selectedInstalledApp;
|
|
270
|
+
const query = {
|
|
271
|
+
[REPO_TYPE]: this.query.repoType,
|
|
272
|
+
[REPO]: this.query.repoName,
|
|
273
|
+
[CHART]: this.query.chartName,
|
|
274
|
+
[VERSION]: this.query.versionName,
|
|
275
|
+
[DEPRECATED]: this.query.deprecated,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// If editing an existing app, include namespace and name in query
|
|
279
|
+
if (app) {
|
|
280
|
+
query[NAMESPACE] = app.metadata.namespace;
|
|
281
|
+
query[NAME] = app.metadata.name;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.$router.push({
|
|
285
|
+
name: 'c-cluster-apps-charts-install',
|
|
286
|
+
params: {
|
|
287
|
+
cluster: this.$route.params.cluster,
|
|
288
|
+
product: this.$store.getters['productId'],
|
|
289
|
+
},
|
|
290
|
+
query
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Navigate to install page to create a new instance of this chart.
|
|
296
|
+
* Uses NEW_APP_INSTANCE flag to enable version selection in the install wizard.
|
|
297
|
+
*/
|
|
298
|
+
installNewInstance() {
|
|
157
299
|
this.$router.push({
|
|
158
300
|
name: 'c-cluster-apps-charts-install',
|
|
159
301
|
params: {
|
|
@@ -161,14 +303,24 @@ export default {
|
|
|
161
303
|
product: this.$store.getters['productId'],
|
|
162
304
|
},
|
|
163
305
|
query: {
|
|
164
|
-
[REPO_TYPE]:
|
|
165
|
-
[REPO]:
|
|
166
|
-
[CHART]:
|
|
167
|
-
[
|
|
168
|
-
[
|
|
306
|
+
[REPO_TYPE]: this.query.repoType,
|
|
307
|
+
[REPO]: this.query.repoName,
|
|
308
|
+
[CHART]: this.query.chartName,
|
|
309
|
+
[DEPRECATED]: this.query.deprecated,
|
|
310
|
+
[NEW_APP_INSTANCE]: _FLAGGED,
|
|
169
311
|
}
|
|
170
312
|
});
|
|
171
313
|
},
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Handle installed app selection from the dropdown.
|
|
317
|
+
* Updates selectedInstalledAppId and this.existing so the page reflects the current installed app and displays the correct information for the selected instance.
|
|
318
|
+
*/
|
|
319
|
+
handleInstalledAppSelect(id) {
|
|
320
|
+
this.selectedInstalledAppId = id;
|
|
321
|
+
this.existing = this.installedInstances?.find((app) => app.id === id) ?? null;
|
|
322
|
+
},
|
|
323
|
+
|
|
172
324
|
handleHeaderItemClick(type, value) {
|
|
173
325
|
const params = {
|
|
174
326
|
cluster: this.$route.params.cluster,
|
|
@@ -194,17 +346,9 @@ export default {
|
|
|
194
346
|
}
|
|
195
347
|
},
|
|
196
348
|
formatVersionDate(date) {
|
|
197
|
-
if (date === ZERO_TIME) {
|
|
198
|
-
return this.t('generic.na');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
349
|
return day(date).format('MMM D, YYYY');
|
|
202
350
|
},
|
|
203
351
|
getVersionDateTooltip(date) {
|
|
204
|
-
if (date === ZERO_TIME) {
|
|
205
|
-
return this.t('catalog.chart.info.chartVersions.missingVersionDate');
|
|
206
|
-
}
|
|
207
|
-
|
|
208
352
|
const dateFormat = escapeHtml(this.$store.getters['prefs/get'](DATE_FORMAT));
|
|
209
353
|
|
|
210
354
|
return day(date).format(dateFormat);
|
|
@@ -297,17 +441,49 @@ export default {
|
|
|
297
441
|
/>
|
|
298
442
|
</div>
|
|
299
443
|
</div>
|
|
300
|
-
<
|
|
444
|
+
<div
|
|
301
445
|
v-if="!requires.length"
|
|
302
|
-
|
|
303
|
-
class="btn role-primary"
|
|
304
|
-
@click.prevent="install"
|
|
446
|
+
class="header-actions"
|
|
305
447
|
>
|
|
306
|
-
<
|
|
307
|
-
|
|
448
|
+
<LabeledSelect
|
|
449
|
+
v-if="installedInstances.length > 1 && canInstallNew"
|
|
450
|
+
:value="selectedInstalledApp?.id"
|
|
451
|
+
:options="installedAppOptions"
|
|
452
|
+
:clearable="false"
|
|
453
|
+
:aria-label="t('catalog.chart.installedAppsSelector.ariaLabel')"
|
|
454
|
+
class="installed-apps-selector"
|
|
455
|
+
data-testid="installed-apps-selector"
|
|
456
|
+
@update:value="handleInstalledAppSelect"
|
|
308
457
|
/>
|
|
309
|
-
|
|
310
|
-
|
|
458
|
+
<!-- Split button: shown when chart is installed AND can be re-installed -->
|
|
459
|
+
<RcButtonSplit
|
|
460
|
+
v-if="selectedInstalledApp && canInstallNew"
|
|
461
|
+
data-testid="btn-chart-install"
|
|
462
|
+
size="large"
|
|
463
|
+
:left-icon="currentAction.icon"
|
|
464
|
+
@click="executeAction"
|
|
465
|
+
>
|
|
466
|
+
{{ t(`catalog.chart.chartButton.action.${currentAction.tKey}` ) }}
|
|
467
|
+
<template #dropdownCollection>
|
|
468
|
+
<RcDropdownItem @click="installNewInstance">
|
|
469
|
+
<template #before>
|
|
470
|
+
<i class="icon icon-plus" />
|
|
471
|
+
</template>
|
|
472
|
+
{{ t('catalog.chart.chartButton.action.installNew') }}
|
|
473
|
+
</RcDropdownItem>
|
|
474
|
+
</template>
|
|
475
|
+
</RcButtonSplit>
|
|
476
|
+
<!-- Simple button: shown when chart is not installed OR cannot be re-installed -->
|
|
477
|
+
<RcButton
|
|
478
|
+
v-else
|
|
479
|
+
data-testid="btn-chart-install"
|
|
480
|
+
size="large"
|
|
481
|
+
:left-icon="currentAction.icon"
|
|
482
|
+
@click.prevent="executeAction"
|
|
483
|
+
>
|
|
484
|
+
{{ t(`catalog.chart.chartButton.action.${currentAction.tKey}` ) }}
|
|
485
|
+
</RcButton>
|
|
486
|
+
</div>
|
|
311
487
|
</div>
|
|
312
488
|
|
|
313
489
|
<div class="dashed-spacer" />
|
|
@@ -418,6 +594,7 @@ export default {
|
|
|
418
594
|
/>
|
|
419
595
|
</div>
|
|
420
596
|
<p
|
|
597
|
+
v-if="!isMissingDate(vers.created)"
|
|
421
598
|
v-clean-tooltip="{ content: getVersionDateTooltip(vers.created), placement: 'left'}"
|
|
422
599
|
class="version-date"
|
|
423
600
|
>
|
|
@@ -624,9 +801,16 @@ export default {
|
|
|
624
801
|
}
|
|
625
802
|
}
|
|
626
803
|
|
|
627
|
-
.
|
|
804
|
+
.header-actions {
|
|
628
805
|
margin-left: auto;
|
|
629
|
-
|
|
806
|
+
display: flex;
|
|
807
|
+
align-items: flex-start;
|
|
808
|
+
flex-shrink: 0;
|
|
809
|
+
gap: 8px;
|
|
810
|
+
|
|
811
|
+
.installed-apps-selector {
|
|
812
|
+
width: 340px;
|
|
813
|
+
}
|
|
630
814
|
}
|
|
631
815
|
|
|
632
816
|
.description {
|
|
@@ -200,7 +200,7 @@ export default {
|
|
|
200
200
|
tagOptions() {
|
|
201
201
|
const outSet = new Set();
|
|
202
202
|
|
|
203
|
-
this.
|
|
203
|
+
this.enabledCharts.forEach((chart) => {
|
|
204
204
|
if (Array.isArray(chart.tags)) {
|
|
205
205
|
chart.tags.forEach((tag) => outSet.add(tag));
|
|
206
206
|
}
|
|
@@ -270,7 +270,7 @@ export default {
|
|
|
270
270
|
categoryOptions() {
|
|
271
271
|
const map = {};
|
|
272
272
|
|
|
273
|
-
for ( const chart of this.
|
|
273
|
+
for ( const chart of this.enabledCharts ) {
|
|
274
274
|
for ( const c of chart.categories ) {
|
|
275
275
|
if ( !map[c] ) {
|
|
276
276
|
const labelKey = `catalog.charts.categories.${ lcFirst(c) }`;
|