@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
|
@@ -207,6 +207,68 @@ describe('page: UI plugins/Extensions', () => {
|
|
|
207
207
|
|
|
208
208
|
expect(items).toHaveLength(0);
|
|
209
209
|
});
|
|
210
|
+
|
|
211
|
+
it('should display repository name for non-installed plugins with chart info', () => {
|
|
212
|
+
const plugin = {
|
|
213
|
+
installed: false,
|
|
214
|
+
chart: { repoNameDisplay: 'rancher-charts' },
|
|
215
|
+
certified: true
|
|
216
|
+
};
|
|
217
|
+
const items = wrapper.vm.getFooterItems(plugin);
|
|
218
|
+
|
|
219
|
+
expect(items).toHaveLength(1);
|
|
220
|
+
expect(items[0].icon).toBe('repository-alt');
|
|
221
|
+
expect(items[0].labels).toStrictEqual(['rancher-charts']);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should display repository name for installed plugins when chart info is present (original repo exists)', () => {
|
|
225
|
+
const plugin = {
|
|
226
|
+
installed: true,
|
|
227
|
+
chart: { repoNameDisplay: 'rancher-charts' },
|
|
228
|
+
certified: true
|
|
229
|
+
};
|
|
230
|
+
const items = wrapper.vm.getFooterItems(plugin);
|
|
231
|
+
|
|
232
|
+
expect(items).toHaveLength(1);
|
|
233
|
+
expect(items[0].icon).toBe('repository-alt');
|
|
234
|
+
expect(items[0].labels).toStrictEqual(['rancher-charts']);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should display original repository name for installed plugins when original repo was removed', () => {
|
|
238
|
+
const plugin = {
|
|
239
|
+
installed: true,
|
|
240
|
+
originalRepoNameDisplay: 'removed-repo',
|
|
241
|
+
certified: true
|
|
242
|
+
};
|
|
243
|
+
const items = wrapper.vm.getFooterItems(plugin);
|
|
244
|
+
|
|
245
|
+
expect(items).toHaveLength(1);
|
|
246
|
+
expect(items[0].icon).toBe('repository-alt');
|
|
247
|
+
expect(items[0].labels).toStrictEqual(['removed-repo']);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should prefer chart.repoNameDisplay over originalRepoNameDisplay when both are present', () => {
|
|
251
|
+
const plugin = {
|
|
252
|
+
installed: true,
|
|
253
|
+
chart: { repoNameDisplay: 'current-repo' },
|
|
254
|
+
originalRepoNameDisplay: 'original-repo',
|
|
255
|
+
certified: true
|
|
256
|
+
};
|
|
257
|
+
const items = wrapper.vm.getFooterItems(plugin);
|
|
258
|
+
|
|
259
|
+
expect(items).toHaveLength(1);
|
|
260
|
+
expect(items[0].labels).toStrictEqual(['current-repo']);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should NOT display repository name when neither chart nor originalRepoNameDisplay are present', () => {
|
|
264
|
+
const plugin = {
|
|
265
|
+
installed: true,
|
|
266
|
+
certified: true
|
|
267
|
+
};
|
|
268
|
+
const items = wrapper.vm.getFooterItems(plugin);
|
|
269
|
+
|
|
270
|
+
expect(items).toHaveLength(0);
|
|
271
|
+
});
|
|
210
272
|
});
|
|
211
273
|
|
|
212
274
|
describe('getStatuses', () => {
|
|
@@ -269,6 +331,162 @@ describe('page: UI plugins/Extensions', () => {
|
|
|
269
331
|
});
|
|
270
332
|
});
|
|
271
333
|
|
|
334
|
+
describe('showInstallDialog', () => {
|
|
335
|
+
let dispatchMock: jest.Mock;
|
|
336
|
+
|
|
337
|
+
const createWrapper = (availablePlugins: any[] = []) => {
|
|
338
|
+
dispatchMock = jest.fn().mockResolvedValue(true);
|
|
339
|
+
|
|
340
|
+
const store = {
|
|
341
|
+
getters: {
|
|
342
|
+
'prefs/get': jest.fn(),
|
|
343
|
+
'catalog/rawCharts': {},
|
|
344
|
+
'uiplugins/plugins': [],
|
|
345
|
+
'uiplugins/errors': {},
|
|
346
|
+
'management/all': () => [],
|
|
347
|
+
},
|
|
348
|
+
dispatch: dispatchMock,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const wrapper = shallowMount(UiPluginsPage, {
|
|
352
|
+
global: {
|
|
353
|
+
mocks: {
|
|
354
|
+
$store: store,
|
|
355
|
+
t,
|
|
356
|
+
},
|
|
357
|
+
stubs: { ActionMenu: { template: '<div />' } }
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Mock the available computed property
|
|
362
|
+
Object.defineProperty(wrapper.vm, 'available', { get: () => availablePlugins });
|
|
363
|
+
|
|
364
|
+
return wrapper;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
it('should open UninstallExistingExtensionDialog when installing a plugin that is already installed from a different source', () => {
|
|
368
|
+
const installedPlugin = {
|
|
369
|
+
id: 'other-repo/my-plugin',
|
|
370
|
+
name: 'my-plugin',
|
|
371
|
+
installed: true
|
|
372
|
+
};
|
|
373
|
+
const pluginToInstall = {
|
|
374
|
+
id: 'new-repo/my-plugin',
|
|
375
|
+
name: 'my-plugin'
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const wrapper = createWrapper([installedPlugin, pluginToInstall]);
|
|
379
|
+
|
|
380
|
+
wrapper.vm.showInstallDialog(pluginToInstall, 'install', {});
|
|
381
|
+
|
|
382
|
+
expect(dispatchMock).toHaveBeenCalledWith(
|
|
383
|
+
'management/promptModal',
|
|
384
|
+
expect.objectContaining({ component: 'UninstallExistingExtensionDialog' })
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should NOT open InstallExtensionDialog when plugin is already installed from a different source', () => {
|
|
389
|
+
const installedPlugin = {
|
|
390
|
+
id: 'other-repo/my-plugin',
|
|
391
|
+
name: 'my-plugin',
|
|
392
|
+
installed: true
|
|
393
|
+
};
|
|
394
|
+
const pluginToInstall = {
|
|
395
|
+
id: 'new-repo/my-plugin',
|
|
396
|
+
name: 'my-plugin'
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const wrapper = createWrapper([installedPlugin, pluginToInstall]);
|
|
400
|
+
|
|
401
|
+
wrapper.vm.showInstallDialog(pluginToInstall, 'install', {});
|
|
402
|
+
|
|
403
|
+
expect(dispatchMock).not.toHaveBeenCalledWith(
|
|
404
|
+
'management/promptModal',
|
|
405
|
+
expect.objectContaining({ component: 'InstallExtensionDialog' })
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should pass the installedPlugin to UninstallExistingExtensionDialog', () => {
|
|
410
|
+
const installedPlugin = {
|
|
411
|
+
id: 'other-repo/my-plugin',
|
|
412
|
+
name: 'my-plugin',
|
|
413
|
+
installed: true
|
|
414
|
+
};
|
|
415
|
+
const pluginToInstall = {
|
|
416
|
+
id: 'new-repo/my-plugin',
|
|
417
|
+
name: 'my-plugin'
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const wrapper = createWrapper([installedPlugin, pluginToInstall]);
|
|
421
|
+
|
|
422
|
+
wrapper.vm.showInstallDialog(pluginToInstall, 'install', {});
|
|
423
|
+
|
|
424
|
+
expect(dispatchMock).toHaveBeenCalledWith(
|
|
425
|
+
'management/promptModal',
|
|
426
|
+
expect.objectContaining({
|
|
427
|
+
component: 'UninstallExistingExtensionDialog',
|
|
428
|
+
componentProps: expect.objectContaining({ installedPlugin })
|
|
429
|
+
})
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('should open InstallExtensionDialog when installing a plugin that is NOT installed from another source', () => {
|
|
434
|
+
const pluginToInstall = {
|
|
435
|
+
id: 'repo/my-plugin',
|
|
436
|
+
name: 'my-plugin'
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const wrapper = createWrapper([pluginToInstall]);
|
|
440
|
+
|
|
441
|
+
wrapper.vm.showInstallDialog(pluginToInstall, 'install', {});
|
|
442
|
+
|
|
443
|
+
expect(dispatchMock).toHaveBeenCalledWith(
|
|
444
|
+
'management/promptModal',
|
|
445
|
+
expect.objectContaining({ component: 'InstallExtensionDialog' })
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should open InstallExtensionDialog when the same plugin id is being re-installed (same source)', () => {
|
|
450
|
+
const installedPlugin = {
|
|
451
|
+
id: 'repo/my-plugin',
|
|
452
|
+
name: 'my-plugin',
|
|
453
|
+
installed: true
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const wrapper = createWrapper([installedPlugin]);
|
|
457
|
+
|
|
458
|
+
wrapper.vm.showInstallDialog(installedPlugin, 'install', {});
|
|
459
|
+
|
|
460
|
+
expect(dispatchMock).toHaveBeenCalledWith(
|
|
461
|
+
'management/promptModal',
|
|
462
|
+
expect.objectContaining({ component: 'InstallExtensionDialog' })
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('should open InstallExtensionDialog for upgrade action even if same name exists in another repo', () => {
|
|
467
|
+
const installedPlugin = {
|
|
468
|
+
id: 'repo-a/my-plugin',
|
|
469
|
+
name: 'my-plugin',
|
|
470
|
+
installed: true
|
|
471
|
+
};
|
|
472
|
+
const otherPlugin = {
|
|
473
|
+
id: 'repo-b/my-plugin',
|
|
474
|
+
name: 'my-plugin',
|
|
475
|
+
installed: false
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const wrapper = createWrapper([installedPlugin, otherPlugin]);
|
|
479
|
+
|
|
480
|
+
// Upgrade action should always go to InstallExtensionDialog
|
|
481
|
+
wrapper.vm.showInstallDialog(installedPlugin, 'upgrade', {});
|
|
482
|
+
|
|
483
|
+
expect(dispatchMock).toHaveBeenCalledWith(
|
|
484
|
+
'management/promptModal',
|
|
485
|
+
expect.objectContaining({ component: 'InstallExtensionDialog' })
|
|
486
|
+
);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
272
490
|
describe('watch: helmOps', () => {
|
|
273
491
|
let wrapper: VueWrapper<any>;
|
|
274
492
|
let updatePluginInstallStatusMock: jest.Mock;
|
|
@@ -296,10 +514,10 @@ describe('page: UI plugins/Extensions', () => {
|
|
|
296
514
|
computed: {
|
|
297
515
|
// Override the computed property for this test suite
|
|
298
516
|
available: () => [
|
|
299
|
-
{ name: 'plugin1' },
|
|
300
|
-
{ name: 'plugin2' },
|
|
301
|
-
{ name: 'plugin3' },
|
|
302
|
-
{ name: 'plugin4' },
|
|
517
|
+
{ name: 'plugin1', id: 'repo/plugin1' },
|
|
518
|
+
{ name: 'plugin2', id: 'repo/plugin2' },
|
|
519
|
+
{ name: 'plugin3', id: 'repo/plugin3' },
|
|
520
|
+
{ name: 'plugin4', id: 'repo/plugin4' },
|
|
303
521
|
],
|
|
304
522
|
hasMenuActions: () => true,
|
|
305
523
|
menuActions: () => []
|
|
@@ -311,9 +529,9 @@ describe('page: UI plugins/Extensions', () => {
|
|
|
311
529
|
|
|
312
530
|
// Set the 'installing' status on the component instance
|
|
313
531
|
wrapper.vm.installing = {
|
|
314
|
-
plugin1: 'install',
|
|
315
|
-
plugin2: 'downgrade',
|
|
316
|
-
plugin3: 'uninstall',
|
|
532
|
+
'repo/plugin1': 'install',
|
|
533
|
+
'repo/plugin2': 'downgrade',
|
|
534
|
+
'repo/plugin3': 'uninstall',
|
|
317
535
|
};
|
|
318
536
|
|
|
319
537
|
// Reset errors
|
|
@@ -330,7 +548,7 @@ describe('page: UI plugins/Extensions', () => {
|
|
|
330
548
|
wrapper.vm.helmOps = helmOps;
|
|
331
549
|
await wrapper.vm.$nextTick();
|
|
332
550
|
|
|
333
|
-
expect(updatePluginInstallStatusMock).toHaveBeenCalledWith('plugin1', 'install');
|
|
551
|
+
expect(updatePluginInstallStatusMock).toHaveBeenCalledWith('repo/plugin1', 'install');
|
|
334
552
|
});
|
|
335
553
|
|
|
336
554
|
it('should not update status for an upgrade op when a downgrade was initiated', async() => {
|
|
@@ -344,7 +562,7 @@ describe('page: UI plugins/Extensions', () => {
|
|
|
344
562
|
await wrapper.vm.$nextTick();
|
|
345
563
|
|
|
346
564
|
// It should not be called with 'upgrade' for plugin2
|
|
347
|
-
expect(updatePluginInstallStatusMock).not.toHaveBeenCalledWith('plugin2', 'upgrade');
|
|
565
|
+
expect(updatePluginInstallStatusMock).not.toHaveBeenCalledWith('repo/plugin2', 'upgrade');
|
|
348
566
|
});
|
|
349
567
|
|
|
350
568
|
it('should clear status for a completed uninstall operation', async() => {
|
|
@@ -357,7 +575,7 @@ describe('page: UI plugins/Extensions', () => {
|
|
|
357
575
|
wrapper.vm.helmOps = helmOps;
|
|
358
576
|
await wrapper.vm.$nextTick();
|
|
359
577
|
|
|
360
|
-
expect(updatePluginInstallStatusMock).toHaveBeenCalledWith('plugin3', false);
|
|
578
|
+
expect(updatePluginInstallStatusMock).toHaveBeenCalledWith('repo/plugin3', false);
|
|
361
579
|
});
|
|
362
580
|
|
|
363
581
|
it('should set error and clear status for a failed operation', async() => {
|
|
@@ -370,8 +588,8 @@ describe('page: UI plugins/Extensions', () => {
|
|
|
370
588
|
wrapper.vm.helmOps = helmOps;
|
|
371
589
|
await wrapper.vm.$nextTick();
|
|
372
590
|
|
|
373
|
-
expect(wrapper.vm.errors
|
|
374
|
-
expect(updatePluginInstallStatusMock).toHaveBeenCalledWith('plugin1', false);
|
|
591
|
+
expect(wrapper.vm.errors['repo/plugin1']).toBe(true);
|
|
592
|
+
expect(updatePluginInstallStatusMock).toHaveBeenCalledWith('repo/plugin1', false);
|
|
375
593
|
});
|
|
376
594
|
|
|
377
595
|
it('should clear status for plugins with no active operation', async() => {
|
|
@@ -385,7 +603,7 @@ describe('page: UI plugins/Extensions', () => {
|
|
|
385
603
|
await wrapper.vm.$nextTick();
|
|
386
604
|
|
|
387
605
|
// plugin4 has no op, so its status should be cleared
|
|
388
|
-
expect(updatePluginInstallStatusMock).toHaveBeenCalledWith('plugin4', false);
|
|
606
|
+
expect(updatePluginInstallStatusMock).toHaveBeenCalledWith('repo/plugin4', false);
|
|
389
607
|
});
|
|
390
608
|
});
|
|
391
609
|
});
|
|
@@ -3,6 +3,7 @@ import { mapGetters } from 'vuex';
|
|
|
3
3
|
import day from 'dayjs';
|
|
4
4
|
import { mapPref, PLUGIN_DEVELOPER } from '@shell/store/prefs';
|
|
5
5
|
import { sortBy } from '@shell/utils/sort';
|
|
6
|
+
import genericPluginSvg from '~shell/assets/images/generic-plugin.svg';
|
|
6
7
|
import { allHash } from '@shell/utils/promise';
|
|
7
8
|
import { CATALOG, UI_PLUGIN, MANAGEMENT, ZERO_TIME } from '@shell/config/types';
|
|
8
9
|
import { SETTING } from '@shell/config/settings';
|
|
@@ -73,7 +74,7 @@ export default {
|
|
|
73
74
|
menuTargetEvent: null,
|
|
74
75
|
menuOpen: false,
|
|
75
76
|
hasFeatureFlag: true,
|
|
76
|
-
defaultIcon:
|
|
77
|
+
defaultIcon: genericPluginSvg,
|
|
77
78
|
reloadRequired: false,
|
|
78
79
|
rancherVersion: null
|
|
79
80
|
};
|
|
@@ -100,6 +101,14 @@ export default {
|
|
|
100
101
|
hash.helmOps = this.$store.dispatch('management/findAll', { type: CATALOG.OPERATION });
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
// Load apps in UI_PLUGIN_NAMESPACE to determine which repo an extension was installed from
|
|
105
|
+
if (this.$store.getters['management/schemaFor'](CATALOG.APP)) {
|
|
106
|
+
hash.apps = this.$store.dispatch('management/findAll', {
|
|
107
|
+
type: CATALOG.APP,
|
|
108
|
+
opt: { namespaced: UI_PLUGIN_NAMESPACE }
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
if (this.$store.getters['management/schemaFor'](CATALOG.CLUSTER_REPO)) {
|
|
104
113
|
hash.repos = this.$store.dispatch('management/findAll', { type: CATALOG.CLUSTER_REPO }, { force: true });
|
|
105
114
|
}
|
|
@@ -126,6 +135,12 @@ export default {
|
|
|
126
135
|
...mapGetters({ uiErrors: 'uiplugins/errors' }),
|
|
127
136
|
...mapGetters({ theme: 'prefs/theme' }),
|
|
128
137
|
|
|
138
|
+
// Computed to reactively update when new apps are installed
|
|
139
|
+
apps() {
|
|
140
|
+
return this.$store.getters['management/all'](CATALOG.APP)
|
|
141
|
+
.filter((app) => app.metadata?.namespace === UI_PLUGIN_NAMESPACE);
|
|
142
|
+
},
|
|
143
|
+
|
|
129
144
|
charts() {
|
|
130
145
|
const c = this.$store.getters['catalog/rawCharts'];
|
|
131
146
|
|
|
@@ -312,8 +327,8 @@ export default {
|
|
|
312
327
|
}
|
|
313
328
|
}
|
|
314
329
|
|
|
315
|
-
if (this.installing[item.
|
|
316
|
-
item.installing = this.installing[item.
|
|
330
|
+
if (this.installing[item.id]) {
|
|
331
|
+
item.installing = this.installing[item.id];
|
|
317
332
|
}
|
|
318
333
|
|
|
319
334
|
return item;
|
|
@@ -358,15 +373,23 @@ export default {
|
|
|
358
373
|
|
|
359
374
|
// Go through the CRs for the plugins and wire them into the catalog
|
|
360
375
|
this.plugins.forEach((p) => {
|
|
361
|
-
|
|
376
|
+
let chart;
|
|
377
|
+
const app = this.apps.find((a) => a.metadata.name === p.name && a.metadata.namespace === UI_PLUGIN_NAMESPACE);
|
|
378
|
+
const originalRepoName = app?.spec?.chart?.metadata?.annotations?.[CATALOG_ANNOTATIONS.SOURCE_REPO_NAME] || app?.metadata?.labels?.[CATALOG_ANNOTATIONS.CLUSTER_REPO_NAME];
|
|
379
|
+
|
|
380
|
+
// Find the chart from the original repo to avoid picking a wrong chart with the same name
|
|
381
|
+
if (originalRepoName) {
|
|
382
|
+
chart = all.find((c) => c.name === p.name && c.chart?.repoName === originalRepoName);
|
|
383
|
+
}
|
|
362
384
|
|
|
385
|
+
// If original repo was removed, don't fall back to another repo's chart (would break Available tab)
|
|
363
386
|
if (chart) {
|
|
364
387
|
chart.installed = true;
|
|
365
388
|
chart.uiplugin = p;
|
|
366
389
|
chart.installedVersion = p.version;
|
|
367
390
|
|
|
368
391
|
// Can't do this here
|
|
369
|
-
chart.installing = this.installing[chart.
|
|
392
|
+
chart.installing = this.installing[chart.id];
|
|
370
393
|
|
|
371
394
|
// Check for upgrade
|
|
372
395
|
const latestInstallableVersion = chart.installableVersions?.[0];
|
|
@@ -384,20 +407,34 @@ export default {
|
|
|
384
407
|
chart.upgrade = getPluginChartVersionLabel(latestInstallableVersion);
|
|
385
408
|
}
|
|
386
409
|
} else {
|
|
387
|
-
// No chart
|
|
410
|
+
// No chart available - original repo was removed or developer-loaded plugin
|
|
411
|
+
const appChartMeta = app?.spec?.chart?.metadata;
|
|
412
|
+
const appAnnotations = appChartMeta?.annotations || {};
|
|
413
|
+
let originalRepoDisplayName = null;
|
|
414
|
+
|
|
415
|
+
if (originalRepoName) {
|
|
416
|
+
originalRepoDisplayName = this.$store.getters['i18n/withFallback'](`catalog.repo.name."${ originalRepoName }"`, null, originalRepoName);
|
|
417
|
+
}
|
|
418
|
+
|
|
388
419
|
const item = {
|
|
389
|
-
name:
|
|
390
|
-
label:
|
|
391
|
-
description:
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
420
|
+
name: p.name,
|
|
421
|
+
label: appAnnotations[UI_PLUGIN_CHART_ANNOTATIONS.DISPLAY_NAME] || appChartMeta?.name || p.name,
|
|
422
|
+
description: appChartMeta?.description || p.description || '-',
|
|
423
|
+
icon: appChartMeta?.icon || appAnnotations['catalog.cattle.io/ui-icon'],
|
|
424
|
+
id: `${ p.name }-${ p.version }`,
|
|
425
|
+
versions: [],
|
|
426
|
+
displayVersion: p.version,
|
|
427
|
+
displayVersionLabel: p.version || '-',
|
|
428
|
+
isDeveloper: p.isDeveloper,
|
|
429
|
+
installed: true,
|
|
430
|
+
installedVersion: p.version,
|
|
431
|
+
installing: false,
|
|
432
|
+
builtin: false,
|
|
433
|
+
uiplugin: p,
|
|
434
|
+
primeOnly: appAnnotations[CATALOG_ANNOTATIONS.PRIME_ONLY] === 'true',
|
|
435
|
+
experimental: appAnnotations[CATALOG_ANNOTATIONS.EXPERIMENTAL] === 'true',
|
|
436
|
+
certified: appAnnotations[CATALOG_ANNOTATIONS.CERTIFIED] === CATALOG_ANNOTATIONS._RANCHER,
|
|
437
|
+
originalRepoNameDisplay: originalRepoDisplayName,
|
|
401
438
|
};
|
|
402
439
|
|
|
403
440
|
all.push(item);
|
|
@@ -421,7 +458,7 @@ export default {
|
|
|
421
458
|
|
|
422
459
|
// Merge in the plugin load errors from help ops
|
|
423
460
|
Object.keys(this.errors).forEach((e) => {
|
|
424
|
-
const chart = all.find((c) => c.
|
|
461
|
+
const chart = all.find((c) => c.id === e);
|
|
425
462
|
|
|
426
463
|
if (chart) {
|
|
427
464
|
chart.helmError = !!this.errors[e];
|
|
@@ -455,28 +492,50 @@ export default {
|
|
|
455
492
|
const op = pluginOps.find((o) => o.status?.releaseName === plugin.name);
|
|
456
493
|
|
|
457
494
|
if (op) {
|
|
495
|
+
const allWithSameName = this.available.filter((p) => p.name === plugin.name);
|
|
496
|
+
let targetPluginId;
|
|
497
|
+
|
|
498
|
+
// When multiple plugins share the same name (from different repositories),
|
|
499
|
+
// a single helm operation will trigger updates. We need to correctly identify
|
|
500
|
+
// the specific plugin that is either currently being installed/updated or is already installed
|
|
501
|
+
// so we don't accidentally mark all identically named plugins as "installing".
|
|
502
|
+
const installingPlugin = allWithSameName.find((p) => this.installing[p.id]);
|
|
503
|
+
const installedPlugin = allWithSameName.find((p) => p.installed);
|
|
504
|
+
|
|
505
|
+
if (installingPlugin) {
|
|
506
|
+
targetPluginId = installingPlugin.id;
|
|
507
|
+
} else if (installedPlugin) {
|
|
508
|
+
targetPluginId = installedPlugin.id;
|
|
509
|
+
} else {
|
|
510
|
+
targetPluginId = allWithSameName[0]?.id;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (plugin.id !== targetPluginId) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
458
517
|
const active = op.metadata.state?.transitioning;
|
|
459
518
|
const error = op.metadata.state?.error;
|
|
460
519
|
|
|
461
|
-
this.errors[plugin.
|
|
520
|
+
this.errors[plugin.id] = error;
|
|
462
521
|
|
|
463
522
|
if (active) {
|
|
464
523
|
// Can use the status directly, apart from upgrade, which maps to update
|
|
465
524
|
const status = op.status.action;
|
|
466
525
|
|
|
467
|
-
if (status === 'upgrade' && this.installing[plugin.
|
|
526
|
+
if (status === 'upgrade' && this.installing[plugin.id] === 'downgrade') {
|
|
468
527
|
// Helm op is an upgrade, but we initiated a downgrade, so keep the 'downgrade' status
|
|
469
528
|
} else {
|
|
470
|
-
this.updatePluginInstallStatus(plugin.
|
|
529
|
+
this.updatePluginInstallStatus(plugin.id, status);
|
|
471
530
|
}
|
|
472
531
|
} else if (op.status.action === 'uninstall') {
|
|
473
532
|
// Uninstall has finished
|
|
474
|
-
this.updatePluginInstallStatus(plugin.
|
|
533
|
+
this.updatePluginInstallStatus(plugin.id, false);
|
|
475
534
|
} else if (error) {
|
|
476
|
-
this.updatePluginInstallStatus(plugin.
|
|
535
|
+
this.updatePluginInstallStatus(plugin.id, false);
|
|
477
536
|
}
|
|
478
537
|
} else {
|
|
479
|
-
this.updatePluginInstallStatus(plugin.
|
|
538
|
+
this.updatePluginInstallStatus(plugin.id, false);
|
|
480
539
|
}
|
|
481
540
|
});
|
|
482
541
|
},
|
|
@@ -502,7 +561,11 @@ export default {
|
|
|
502
561
|
changes++;
|
|
503
562
|
}
|
|
504
563
|
|
|
505
|
-
this.
|
|
564
|
+
(this.available || []).forEach((c) => {
|
|
565
|
+
if (c.name === plugin.name) {
|
|
566
|
+
this.updatePluginInstallStatus(c.id, false);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
506
569
|
}
|
|
507
570
|
});
|
|
508
571
|
|
|
@@ -595,6 +658,35 @@ export default {
|
|
|
595
658
|
ev?.preventDefault?.();
|
|
596
659
|
ev?.stopPropagation?.();
|
|
597
660
|
|
|
661
|
+
// Check if a plugin with the same name is already installed from a different repo
|
|
662
|
+
if (action === 'install') {
|
|
663
|
+
const installedPlugin = this.available.find((p) => p.name === plugin.name && p.installed);
|
|
664
|
+
const isInstalledFromDifferentSource = !!installedPlugin && installedPlugin.id !== plugin.id;
|
|
665
|
+
|
|
666
|
+
if (isInstalledFromDifferentSource) {
|
|
667
|
+
// Show a different dialog that prompts the user to uninstall the existing version first
|
|
668
|
+
this.$store.dispatch('management/promptModal', {
|
|
669
|
+
component: 'UninstallExistingExtensionDialog',
|
|
670
|
+
testId: 'uninstall-existing-extension-modal',
|
|
671
|
+
returnFocusSelector: `[data-testid="extension-card-${ action }-btn-${ plugin?.name }"]`,
|
|
672
|
+
returnFocusFirstIterableNodeSelector: '#extensions-main-page',
|
|
673
|
+
componentProps: {
|
|
674
|
+
installedPlugin,
|
|
675
|
+
updateStatus: (pluginId, type) => {
|
|
676
|
+
this.updatePluginInstallStatus(pluginId, type);
|
|
677
|
+
},
|
|
678
|
+
closed: (res) => {
|
|
679
|
+
if (res?.uninstalled) {
|
|
680
|
+
this.didUninstall(res.plugin);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
598
690
|
this.$store.dispatch('management/promptModal', {
|
|
599
691
|
component: 'InstallExtensionDialog',
|
|
600
692
|
testId: 'install-extension-modal',
|
|
@@ -604,8 +696,8 @@ export default {
|
|
|
604
696
|
plugin,
|
|
605
697
|
action,
|
|
606
698
|
initialVersion,
|
|
607
|
-
updateStatus: (
|
|
608
|
-
this.updatePluginInstallStatus(
|
|
699
|
+
updateStatus: (pluginId, type) => {
|
|
700
|
+
this.updatePluginInstallStatus(pluginId, type);
|
|
609
701
|
},
|
|
610
702
|
closed: (res) => {
|
|
611
703
|
this.didInstall(res);
|
|
@@ -626,8 +718,8 @@ export default {
|
|
|
626
718
|
returnFocusFirstIterableNodeSelector: '#extensions-main-page',
|
|
627
719
|
componentProps: {
|
|
628
720
|
plugin,
|
|
629
|
-
updateStatus: (
|
|
630
|
-
this.updatePluginInstallStatus(
|
|
721
|
+
updateStatus: (pluginId, type) => {
|
|
722
|
+
this.updatePluginInstallStatus(pluginId, type);
|
|
631
723
|
},
|
|
632
724
|
closed: (res) => {
|
|
633
725
|
this.didUninstall(res);
|
|
@@ -638,7 +730,7 @@ export default {
|
|
|
638
730
|
|
|
639
731
|
didUninstall(plugin) {
|
|
640
732
|
if (plugin) {
|
|
641
|
-
this.updatePluginInstallStatus(plugin.
|
|
733
|
+
this.updatePluginInstallStatus(plugin.id, 'uninstall');
|
|
642
734
|
|
|
643
735
|
if (plugin.catalog) {
|
|
644
736
|
this.refreshCharts();
|
|
@@ -665,8 +757,8 @@ export default {
|
|
|
665
757
|
this.$refs.infoPanel.show({ ...plugin, tags });
|
|
666
758
|
},
|
|
667
759
|
|
|
668
|
-
updatePluginInstallStatus(
|
|
669
|
-
this.installing[
|
|
760
|
+
updatePluginInstallStatus(id, status) {
|
|
761
|
+
this.installing[id] = status;
|
|
670
762
|
},
|
|
671
763
|
|
|
672
764
|
setMenu(event) {
|
|
@@ -849,6 +941,17 @@ export default {
|
|
|
849
941
|
},
|
|
850
942
|
|
|
851
943
|
getFooterItems(plugin) {
|
|
944
|
+
const footerItems = [];
|
|
945
|
+
const repoNameToDisplay = plugin?.chart?.repoNameDisplay || plugin?.originalRepoNameDisplay;
|
|
946
|
+
|
|
947
|
+
if (repoNameToDisplay) {
|
|
948
|
+
footerItems.push({
|
|
949
|
+
icon: 'repository-alt',
|
|
950
|
+
iconTooltip: { key: 'tableHeaders.repoName' },
|
|
951
|
+
labels: [repoNameToDisplay]
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
|
|
852
955
|
const labels = [];
|
|
853
956
|
|
|
854
957
|
// "developer load" tag
|
|
@@ -872,11 +975,15 @@ export default {
|
|
|
872
975
|
labels.push(this.t('plugins.labels.experimental'));
|
|
873
976
|
}
|
|
874
977
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
978
|
+
if (labels.length) {
|
|
979
|
+
footerItems.push({
|
|
980
|
+
icon: 'tag-alt',
|
|
981
|
+
iconTooltip: { key: 'generic.tags' },
|
|
982
|
+
labels,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return footerItems;
|
|
880
987
|
},
|
|
881
988
|
|
|
882
989
|
getStatuses(plugin) {
|
|
@@ -436,6 +436,7 @@ describe('class: Resource', () => {
|
|
|
436
436
|
}, {
|
|
437
437
|
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
438
438
|
dispatch: jest.fn(),
|
|
439
|
+
rootState: { $extension: { getPlugins: () => ({}) } },
|
|
439
440
|
rootGetters: {
|
|
440
441
|
'i18n/t': (key: string) => key,
|
|
441
442
|
currentCluster: undefined,
|
|
@@ -494,8 +494,9 @@ export default {
|
|
|
494
494
|
// Of type @StorePaginationResult
|
|
495
495
|
const pagination = opt.pagination ? {
|
|
496
496
|
request: {
|
|
497
|
-
namespace:
|
|
498
|
-
pagination:
|
|
497
|
+
namespace: opt.namespaced,
|
|
498
|
+
pagination: opt.pagination,
|
|
499
|
+
includeAssociatedData: opt.includeAssociatedData,
|
|
499
500
|
},
|
|
500
501
|
result: {
|
|
501
502
|
count: out.count,
|