@rancher/shell 3.0.12-rc.5 → 3.0.12-rc.7

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.
Files changed (116) hide show
  1. package/apis/intf/resources-api/resource-base.ts +42 -2
  2. package/apis/intf/resources-api/resource-instance.ts +101 -0
  3. package/apis/intf/resources-api/resources-api.ts +177 -23
  4. package/apis/intf/resources.ts +8 -6
  5. package/apis/intf/shell-api/slide-in.ts +46 -17
  6. package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
  7. package/apis/resources/resources-api-class.ts +232 -43
  8. package/apis/shell/__tests__/slide-in.test.ts +83 -2
  9. package/apis/shell/slide-in.ts +20 -0
  10. package/assets/images/providers/traefik.png +0 -0
  11. package/assets/translations/en-us.yaml +10 -4
  12. package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
  13. package/chart/rancher-monitoring-dashboards/index.vue +155 -0
  14. package/cloud-credential/__tests__/generic.test.ts +132 -0
  15. package/cloud-credential/generic.vue +33 -3
  16. package/components/EtcdInfoBanner.vue +2 -12
  17. package/components/ExplorerProjectsNamespaces.vue +2 -2
  18. package/components/GrafanaDashboard.vue +2 -8
  19. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
  20. package/components/Resource/Detail/Masthead/index.vue +8 -3
  21. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +9 -0
  22. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +5 -1
  23. package/components/SlideInPanelManager.vue +103 -25
  24. package/components/__tests__/SlideInPanelManager.spec.ts +153 -10
  25. package/components/auth/AuthBanner.vue +1 -1
  26. package/components/form/Security.vue +1 -1
  27. package/components/formatter/Translate.vue +22 -1
  28. package/components/nav/Group.vue +5 -10
  29. package/components/nav/TopLevelMenu.vue +1 -30
  30. package/components/nav/__tests__/Group.test.ts +61 -0
  31. package/config/labels-annotations.js +1 -0
  32. package/config/table-headers.js +5 -2
  33. package/core/__tests__/plugin-products-apply.test.ts +787 -0
  34. package/core/__tests__/plugin-products-extend.test.ts +189 -0
  35. package/core/__tests__/plugin-products-helpers.test.ts +81 -78
  36. package/core/__tests__/plugin-products-new.test.ts +389 -0
  37. package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
  38. package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
  39. package/core/plugin-products-base.ts +201 -93
  40. package/core/plugin-products-extending.ts +1 -1
  41. package/core/plugin-products-external.ts +380 -0
  42. package/core/plugin-products-helpers.ts +28 -24
  43. package/core/plugin-products-internal.ts +207 -0
  44. package/core/plugin-products-top-level.ts +10 -5
  45. package/core/plugin-products-type-guards.ts +17 -6
  46. package/core/plugin-products.ts +5 -7
  47. package/core/plugin-types.ts +1 -389
  48. package/core/plugin.ts +10 -11
  49. package/core/plugins-loader.js +2 -0
  50. package/core/types.ts +6 -202
  51. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
  52. package/detail/pod.vue +1 -1
  53. package/detail/provisioning.cattle.io.cluster.vue +0 -2
  54. package/edit/auth/AuthProviderWarningBanners.vue +12 -0
  55. package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
  56. package/edit/auth/__tests__/azuread.test.ts +1 -0
  57. package/edit/auth/__tests__/github.test.ts +1 -0
  58. package/edit/auth/__tests__/oidc.test.ts +1 -0
  59. package/edit/auth/__tests__/saml.test.ts +1 -0
  60. package/edit/auth/ldap/__tests__/index.test.ts +1 -0
  61. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  62. package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -1
  63. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
  64. package/list/management.cattle.io.user.vue +24 -5
  65. package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
  66. package/machine-config/__tests__/generic.test.ts +163 -0
  67. package/machine-config/components/EC2Networking.vue +31 -7
  68. package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
  69. package/machine-config/generic.vue +32 -5
  70. package/mixins/resource-fetch.js +1 -1
  71. package/models/__tests__/namespace.test.ts +133 -8
  72. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +57 -1
  73. package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
  74. package/models/namespace.js +34 -2
  75. package/models/pod.js +12 -10
  76. package/models/provisioning.cattle.io.cluster.js +20 -13
  77. package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
  78. package/package.json +1 -1
  79. package/pages/auth/login.vue +1 -1
  80. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +110 -0
  81. package/pages/c/_cluster/apps/charts/install.vue +29 -0
  82. package/pages/c/_cluster/auth/config/index.vue +11 -4
  83. package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +136 -1
  84. package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +69 -1
  85. package/pages/c/_cluster/fleet/index.vue +5 -9
  86. package/pages/c/_cluster/monitoring/index.vue +6 -1
  87. package/pkg/vue.config.js +4 -3
  88. package/plugins/codemirror.js +2 -0
  89. package/plugins/dashboard-store/resource-class.js +13 -9
  90. package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
  91. package/plugins/steve/steve-class.js +43 -0
  92. package/plugins/steve/subscribe.js +11 -0
  93. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
  94. package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
  95. package/store/__tests__/action-menu.test.ts +622 -0
  96. package/store/__tests__/auth.test.ts +18 -0
  97. package/store/__tests__/notifications.test.ts +466 -1
  98. package/store/__tests__/slideInPanel.test.ts +143 -43
  99. package/store/__tests__/ui-context.test.ts +255 -0
  100. package/store/__tests__/wm.test.ts +503 -0
  101. package/store/auth.js +11 -1
  102. package/store/aws.js +19 -4
  103. package/store/features.js +1 -0
  104. package/store/plugins.js +6 -0
  105. package/store/slideInPanel.ts +15 -3
  106. package/types/rancher/steve.api.ts +2 -2
  107. package/types/shell/index.d.ts +84 -2
  108. package/types/store/dashboard-store.types.ts +2 -2
  109. package/types/store/type-map.ts +262 -1
  110. package/utils/__tests__/fleet-appco.test.ts +23 -0
  111. package/utils/async.ts +2 -0
  112. package/utils/fleet-appco.ts +6 -1
  113. package/utils/grafana.js +2 -17
  114. package/utils/monitoring.js +38 -1
  115. package/utils/sort.js +1 -1
  116. package/core/__tests__/plugin-products.test.ts +0 -4694
@@ -0,0 +1,22 @@
1
+ import VmwarevsphereMachineTemplate from '@shell/models/rke-machine.cattle.io.vmwarevspheremachinetemplate';
2
+
3
+ describe('class VmwarevsphereMachineTemplate', () => {
4
+ describe('providerLocation', () => {
5
+ it.each([
6
+ ['/DC1/host/ClusterA/Resources', 'ClusterA'],
7
+ ['/DC1/host/ClusterA/Resources/PoolX', 'ClusterA'],
8
+ ['/DC1/host/ClusterA/Resources/', 'ClusterA'],
9
+ ['/DC1/host/esxi01/Resources', 'esxi01'],
10
+ ['/DC1/vm/FolderA', null],
11
+ ['/DC1/host/foo/ResourcesPool/bar', null],
12
+ ['/DC1/host//Resources/PoolX', null],
13
+ ['', null],
14
+ [null, null],
15
+ [undefined, null],
16
+ ])('returns %p for pool %p', (pool: string | null | undefined, expected: string | null) => {
17
+ const machineTemplate = new VmwarevsphereMachineTemplate({ spec: { template: { spec: { pool } } } });
18
+
19
+ expect(machineTemplate.providerLocation).toStrictEqual(expected);
20
+ });
21
+ });
22
+ });
@@ -2,12 +2,15 @@ import SYSTEM_NAMESPACES from '@shell/config/system-namespaces';
2
2
  import {
3
3
  PROJECT, SYSTEM_NAMESPACE, ISTIO as ISTIO_LABELS, FLEET, RESOURCE_QUOTA
4
4
  } from '@shell/config/labels-annotations';
5
- import { ISTIO, MANAGEMENT } from '@shell/config/types';
5
+ import { ISTIO, MANAGEMENT, LOCAL_CLUSTER } from '@shell/config/types';
6
6
 
7
7
  import { get, set } from '@shell/utils/object';
8
8
  import { insertAt, isArray } from '@shell/utils/array';
9
9
  import SteveModel from '@shell/plugins/steve/steve-class';
10
10
  import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
11
+ import { NAME as MANAGER } from '@shell/config/product/manager';
12
+ import { NAME as EXPLORER } from '@shell/config/product/explorer';
13
+ import sideNavService from '@shell/components/nav/TopLevelMenu.helper';
11
14
  import { hasPSALabels, getPSATooltipsDescription, getPSALabels } from '@shell/utils/pod-security-admission';
12
15
  import { PSAIconsDisplay, PSALabelsNamespaceVersion } from '@shell/config/pod-security-admission';
13
16
 
@@ -188,7 +191,14 @@ export default class Namespace extends SteveModel {
188
191
  }
189
192
 
190
193
  get listLocation() {
191
- const listLocation = { name: this.$rootGetters['isRancher'] ? 'c-cluster-product-projectsnamespaces' : 'c-cluster-product-resource' };
194
+ const productId = this.$rootGetters['productId'];
195
+ const isManagerProduct = productId === MANAGER;
196
+ const cluster = isManagerProduct ? LOCAL_CLUSTER : this.$rootGetters['clusterId'];
197
+
198
+ const listLocation = {
199
+ name: this.$rootGetters['isRancher'] ? 'c-cluster-product-projectsnamespaces' : 'c-cluster-product-resource',
200
+ params: { cluster, product: EXPLORER },
201
+ };
192
202
 
193
203
  // Harvester uses these resource directly... but has different routes. listLocation covers routes leading back to route
194
204
  if (this.$rootGetters['currentProduct'].inStore === HARVESTER) {
@@ -202,6 +212,12 @@ export default class Namespace extends SteveModel {
202
212
  get _detailLocation() {
203
213
  const _detailLocation = super._detailLocation;
204
214
 
215
+ // Namespace detail pages belong to the local cluster's explorer, not Cluster Management
216
+ if (this.$rootGetters['productId'] === MANAGER) {
217
+ _detailLocation.params.cluster = LOCAL_CLUSTER;
218
+ _detailLocation.params.product = EXPLORER;
219
+ }
220
+
205
221
  return _detailLocation;
206
222
  }
207
223
 
@@ -285,6 +301,22 @@ export default class Namespace extends SteveModel {
285
301
  glance.splice(namespaceIndex, 1, this.projectGlance);
286
302
  }
287
303
 
304
+ // In Cluster Management, the type link points to the local cluster's explorer.
305
+ // If the user cannot access the local cluster, show type as plain text.
306
+ const productId = this.$rootGetters['productId'];
307
+
308
+ if (productId === MANAGER) {
309
+ const hasLocalCluster = sideNavService.helper.clustersPinned.some((c) => c.id === LOCAL_CLUSTER) ||
310
+ sideNavService.helper.clustersOthers.some((c) => c.id === LOCAL_CLUSTER);
311
+ const typeIndex = glance.findIndex((item) => item.name === 'type');
312
+
313
+ if (!hasLocalCluster && typeIndex > -1) {
314
+ glance[typeIndex] = {
315
+ ...glance[typeIndex], formatter: undefined, formatterOpts: undefined
316
+ };
317
+ }
318
+ }
319
+
288
320
  // projectGlance could be undefined
289
321
  return glance.filter(Boolean);
290
322
  }
package/models/pod.js CHANGED
@@ -309,19 +309,21 @@ export default class Pod extends WorkloadService {
309
309
  save() {
310
310
  const prev = { ...this };
311
311
 
312
- const { metadata, spec } = this.spec.template;
312
+ if (this.spec?.template) {
313
+ const { metadata, spec } = this.spec.template;
313
314
 
314
- this.spec = {
315
- ...this.spec,
316
- ...spec
317
- };
315
+ this.spec = {
316
+ ...this.spec,
317
+ ...spec
318
+ };
318
319
 
319
- this.metadata = {
320
- ...this.metadata,
321
- ...metadata
322
- };
320
+ this.metadata = {
321
+ ...this.metadata,
322
+ ...metadata
323
+ };
323
324
 
324
- delete this.spec.template;
325
+ delete this.spec.template;
326
+ }
325
327
 
326
328
  // IF there is an error POD world model get overwritten
327
329
  // For the workloads this need be reset back
@@ -3,6 +3,8 @@ import {
3
3
  CONFIG_MAP, AUTOSCALER_CONFIG_MAP_ID,
4
4
  EVENT, OPERATION
5
5
  } from '@shell/config/types';
6
+ import { NAME as EXPLORER } from '@shell/config/product/explorer';
7
+ import sideNavService from '@shell/components/nav/TopLevelMenu.helper';
6
8
  import SteveModel from '@shell/plugins/steve/steve-class';
7
9
  import { findBy } from '@shell/utils/array';
8
10
  import { get, set } from '@shell/utils/object';
@@ -788,21 +790,26 @@ export default class ProvCluster extends SteveModel {
788
790
  }
789
791
 
790
792
  get namespaceLocation() {
791
- const localCluster = this.$rootGetters['management/byId'](MANAGEMENT.CLUSTER, LOCAL_CLUSTER);
792
-
793
- if (localCluster) {
794
- return {
795
- name: 'c-cluster-product-resource-id',
796
- params: {
797
- cluster: localCluster.id,
798
- product: this.$rootGetters['productId'],
799
- resource: NAMESPACE,
800
- id: this.namespace
801
- }
802
- };
793
+ // Check side nav for local cluster access — `management/byId` only reflects what's
794
+ // currently cached, which on a downstream detail page is just the downstream cluster
795
+ // (so admins would falsely look like they have no local access). The side nav reflects
796
+ // permission, not cache state, so it's accurate for both admins and downstream-only users.
797
+ const hasLocalCluster = sideNavService.helper.clustersPinned.some((c) => c.id === LOCAL_CLUSTER) ||
798
+ sideNavService.helper.clustersOthers.some((c) => c.id === LOCAL_CLUSTER);
799
+
800
+ if (!hasLocalCluster) {
801
+ return null;
803
802
  }
804
803
 
805
- return null;
804
+ return {
805
+ name: 'c-cluster-product-resource-id',
806
+ params: {
807
+ cluster: LOCAL_CLUSTER,
808
+ product: EXPLORER,
809
+ resource: NAMESPACE,
810
+ id: this.namespace
811
+ }
812
+ };
806
813
  }
807
814
 
808
815
  /**
@@ -1,11 +1,20 @@
1
1
  import { formatSi } from '@shell/utils/units';
2
2
  import MachineTemplate from './rke-machine.cattle.io.machinetemplate';
3
3
 
4
+ const POOL_LOCATION_MATCHER = /\/host\/([^/]+)\/Resources(?:\/|$)/;
5
+
4
6
  export default class VmwarevsphereMachineTemplate extends MachineTemplate {
5
7
  get provider() {
6
8
  return 'vmwarevsphere';
7
9
  }
8
10
 
11
+ get providerLocation() {
12
+ const pool = this.spec?.template?.spec?.pool;
13
+ const match = pool?.match(POOL_LOCATION_MATCHER);
14
+
15
+ return match?.[1] || null;
16
+ }
17
+
9
18
  get providerSize() {
10
19
  const size = formatSi(this.spec.template.spec.memorySize * 1048576, 1024, 'iB');
11
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "3.0.12-rc.5",
3
+ "version": "3.0.12-rc.7",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancher/dashboard",
6
6
  "license": "Apache-2.0",
@@ -177,7 +177,7 @@ export default {
177
177
  this.vendor = getVendor();
178
178
  this.providers = providers;
179
179
  this.hasLocal = hasLocal;
180
- this.showLocal = !hasOthers || (this.$route.query[LOCAL] === _FLAGGED);
180
+ this.showLocal = hasLocal && (!hasOthers || (this.$route.query[LOCAL] === _FLAGGED));
181
181
  this.customLoginError = JSON.parse(value).loginError;
182
182
  this.firstLogin = firstLoginSetting?.value === 'true';
183
183
  this.username = this.firstLogin ? 'admin' : this.username;
@@ -522,4 +522,114 @@ describe('page: Install', () => {
522
522
  expect(wrapper.vm.registryPullSecret).toBeNull();
523
523
  });
524
524
  });
525
+
526
+ describe('computed properties: monitoring banners', () => {
527
+ const setupComponent = (existing: any, releaseName: string, componentName: string, chartName: string, installedApps: any[] = [], certified = 'rancher') => {
528
+ const mockStore = {
529
+ getters: {
530
+ 'i18n/withFallback': () => '',
531
+ 'catalog/inStore': 'cluster',
532
+ 'features/get': () => false,
533
+ 'type-map/hasCustomChart': () => false,
534
+ 'wm/byId': () => null,
535
+ 'i18n/t': (k: string) => k,
536
+ 'prefs/get': () => {},
537
+ 'management/all': () => [],
538
+ 'cluster/all': () => [],
539
+ 'cluster/byId': (type: string, id: string) => {
540
+ if (type === 'catalog.cattle.io.app') {
541
+ return installedApps.find((app) => app.id === id);
542
+ }
543
+
544
+ return null;
545
+ },
546
+ 'catalog/charts': [],
547
+ }
548
+ };
549
+
550
+ return mount(Install as any, {
551
+ global: {
552
+ mocks: {
553
+ $store: mockStore,
554
+ $route: { query: {} },
555
+ $fetchState: { pending: false },
556
+ t: (k: string) => k,
557
+ },
558
+ stubs: {
559
+ Loading: true,
560
+ Wizard: true,
561
+ Banner: true,
562
+ Checkbox: true,
563
+ LabeledInput: true,
564
+ LabeledSelect: true,
565
+ NameNsDescription: true,
566
+ Tabbed: true,
567
+ Questions: true,
568
+ YamlEditor: true,
569
+ ResourceCancelModal: true,
570
+ UnitInput: true,
571
+ TypeDescription: true,
572
+ LazyImage: true,
573
+ ChartReadme: true,
574
+ ButtonGroup: true,
575
+ }
576
+ },
577
+ data() {
578
+ return {
579
+ existing,
580
+ version: {
581
+ annotations: {
582
+ [CATALOG_ANNOTATIONS.RELEASE_NAME]: releaseName,
583
+ [CATALOG_ANNOTATIONS.COMPONENT]: componentName,
584
+ [CATALOG_ANNOTATIONS.CERTIFIED]: certified,
585
+ }
586
+ },
587
+ chart: {
588
+ chartName,
589
+ versions: []
590
+ },
591
+ query: { versionName: '1.0.0' }
592
+ };
593
+ }
594
+ });
595
+ };
596
+
597
+ it('showMonitoringBanner should return translation key if releaseName matches rancher-monitoring (install or edit)', () => {
598
+ const wrapper1 = setupComponent(true, 'rancher-monitoring', '', '');
599
+
600
+ expect((wrapper1.vm as any).showMonitoringBanner).toBe('catalog.install.steps.basics.oldMonitoringChartWarning');
601
+
602
+ const wrapper2 = setupComponent(true, '', 'rancher-monitoring', 'rancher-monitoring');
603
+
604
+ expect((wrapper2.vm as any).showMonitoringBanner).toBeNull();
605
+
606
+ const wrapper3 = setupComponent(false, 'rancher-monitoring', '', '');
607
+
608
+ expect((wrapper3.vm as any).showMonitoringBanner).toBe('catalog.install.steps.basics.oldMonitoringChartWarning');
609
+ });
610
+
611
+ it('showMonitoringBanner should return translation key if existing is false and releaseName matches rancher-monitoring-dashboards', () => {
612
+ const wrapper1 = setupComponent(false, 'rancher-monitoring-dashboards', '', '');
613
+
614
+ expect((wrapper1.vm as any).showMonitoringBanner).toBe('catalog.install.steps.basics.newMonitoringChartWarning');
615
+
616
+ const wrapper2 = setupComponent(false, '', 'rancher-monitoring-dashboards', 'rancher-monitoring-dashboards');
617
+
618
+ expect((wrapper2.vm as any).showMonitoringBanner).toBeNull();
619
+
620
+ const wrapper3 = setupComponent(true, 'rancher-monitoring-dashboards', '', '');
621
+
622
+ expect((wrapper3.vm as any).showMonitoringBanner).toBeNull();
623
+ });
624
+
625
+ it('showMonitoringBanner should return null when the chart is not Rancher-certified, even if the release name matches', () => {
626
+ const oldChartThirdParty = setupComponent(false, 'rancher-monitoring', '', '', [], 'partner');
627
+
628
+ expect((oldChartThirdParty.vm as any).showMonitoringBanner).toBeNull();
629
+
630
+ const newChartThirdParty = setupComponent(false, 'rancher-monitoring-dashboards', '', '', [], '');
631
+
632
+ expect((newChartThirdParty.vm as any).showMonitoringBanner).toBeNull();
633
+ });
634
+ });
525
635
  });
@@ -515,6 +515,29 @@ export default {
515
515
  ...mapGetters({ inStore: 'catalog/inStore', features: 'features/get' }),
516
516
  mcm: mapFeature(MULTI_CLUSTER),
517
517
 
518
+ showMonitoringBanner() {
519
+ const annotations = this.version?.annotations || {};
520
+ const releaseName = annotations[CATALOG_ANNOTATIONS.RELEASE_NAME];
521
+ const certified = annotations[CATALOG_ANNOTATIONS.CERTIFIED];
522
+
523
+ // Only show monitoring banners for the Rancher-certified charts —
524
+ // a third-party chart that happens to reuse the release name should not
525
+ // trigger our migration prompts.
526
+ if (certified !== 'rancher') {
527
+ return null;
528
+ }
529
+
530
+ if (releaseName === 'rancher-monitoring') {
531
+ return this.t('catalog.install.steps.basics.oldMonitoringChartWarning');
532
+ }
533
+
534
+ if (!this.existing && releaseName === 'rancher-monitoring-dashboards') {
535
+ return this.t('catalog.install.steps.basics.newMonitoringChartWarning');
536
+ }
537
+
538
+ return null;
539
+ },
540
+
518
541
  /**
519
542
  * Return list of variables to filter chart questions
520
543
  */
@@ -1678,6 +1701,12 @@ export default {
1678
1701
  </template>
1679
1702
  <template #basics>
1680
1703
  <div class="step__basic">
1704
+ <Banner
1705
+ v-if="showMonitoringBanner"
1706
+ color="warning"
1707
+ >
1708
+ {{ showMonitoringBanner }}
1709
+ </Banner>
1681
1710
  <Banner
1682
1711
  v-if="step1Description"
1683
1712
  color="info"
@@ -6,6 +6,7 @@ import { MODE, _EDIT } from '@shell/config/query-params';
6
6
  import { authProvidersInfo } from '@shell/utils/auth';
7
7
  import { Banner } from '@components/Banner';
8
8
  import Loading from '@shell/components/Loading';
9
+ import { HIDE_LOCAL_AUTH_PROVIDER } from '@shell/store/features';
9
10
 
10
11
  const resource = MANAGEMENT.AUTH_CONFIG;
11
12
 
@@ -28,8 +29,9 @@ export default {
28
29
  data() {
29
30
  return {
30
31
  // Provided by fetch later
31
- enabled: false,
32
- nonLocal: null,
32
+ enabled: false,
33
+ nonLocal: null,
34
+ disableLocalAuth: this.$store.getters['features/get'](HIDE_LOCAL_AUTH_PROVIDER)
33
35
  };
34
36
  },
35
37
 
@@ -100,8 +102,13 @@ export default {
100
102
  <router-link :to="localUsersRoute">
101
103
  {{ t('authConfig.manageLocal') }}
102
104
  </router-link>
103
- <br>
104
- {{ t('authConfig.noneEnabled') }}
105
+ <br><br>
106
+ <template v-if="disableLocalAuth">
107
+ {{ t('authConfig.bannerEnableAuthProvider', null, true) }}
108
+ </template>
109
+ <template v-else>
110
+ {{ t('authConfig.noneEnabled') }}
111
+ </template>
105
112
  </div>
106
113
  </Banner>
107
114
  <SelectIconGrid
@@ -1,11 +1,15 @@
1
1
  import { useWorkloadDashboard } from '@shell/pages/c/_cluster/explorer/workload-dashboard/composable';
2
2
  import { WORKLOAD_RESOURCE_TYPES } from '@shell/pages/c/_cluster/explorer/workload-dashboard/types';
3
+ import { WORKLOAD_TYPES } from '@shell/config/types';
3
4
  import { defineComponent, h } from 'vue';
4
5
  import { shallowMount, flushPromises } from '@vue/test-utils';
5
6
 
7
+ const WORKLOAD_DASHBOARD_ROUTE = 'c-cluster-explorer-workload-dashboard';
8
+
6
9
  const mockGetters: Record<string, any> = {};
7
10
  const mockDispatch = jest.fn();
8
11
  const mockRouterPush = jest.fn();
12
+ const mockCurrentRoute = { value: { name: WORKLOAD_DASHBOARD_ROUTE } };
9
13
 
10
14
  jest.mock('vuex', () => ({
11
15
  useStore: () => ({
@@ -18,7 +22,7 @@ jest.mock('vuex', () => ({
18
22
  }),
19
23
  }));
20
24
 
21
- jest.mock('vue-router', () => ({ useRouter: () => ({ push: mockRouterPush }) }));
25
+ jest.mock('vue-router', () => ({ useRouter: () => ({ push: mockRouterPush, currentRoute: mockCurrentRoute }) }));
22
26
 
23
27
  jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => ({ t: (key: string, args?: Record<string, any>) => `%${ key }%${ args ? JSON.stringify(args) : '' }` }) }));
24
28
 
@@ -105,6 +109,7 @@ describe('composable: useWorkloadDashboard', () => {
105
109
  beforeEach(() => {
106
110
  jest.clearAllMocks();
107
111
  setupGetters();
112
+ mockCurrentRoute.value.name = WORKLOAD_DASHBOARD_ROUTE;
108
113
  });
109
114
 
110
115
  describe('namespaceSubtitle', () => {
@@ -558,4 +563,134 @@ describe('composable: useWorkloadDashboard', () => {
558
563
  wrapper.unmount();
559
564
  });
560
565
  });
566
+
567
+ describe('response validation', () => {
568
+ // The composable caches malformed clusters in a module-level singleton, so each
569
+ // invalid-data test uses a unique cluster id to stay independent of the others.
570
+ function deploymentRouteFor(cluster: string) {
571
+ return {
572
+ name: 'c-cluster-product-resource',
573
+ params: {
574
+ cluster,
575
+ product: 'explorer',
576
+ resource: WORKLOAD_TYPES.DEPLOYMENT,
577
+ },
578
+ };
579
+ }
580
+
581
+ const malformedSummaryResponse = {
582
+ summary: [{
583
+ property: 'metadata.state.name',
584
+ counts: { running: { namespace: { default: 5 } } },
585
+ }],
586
+ data: [],
587
+ };
588
+
589
+ it('should redirect to the deployment page when a count entry is missing total', async() => {
590
+ const { wrapper } = mountComposable({ clusterId: 'c-missing-total' }, malformedSummaryResponse);
591
+
592
+ await flushPromises();
593
+
594
+ expect(mockRouterPush).toHaveBeenCalledWith(deploymentRouteFor('c-missing-total'));
595
+ wrapper.unmount();
596
+ });
597
+
598
+ it('should redirect when the summary is empty but data is present', async() => {
599
+ const dataWithoutSummaryResponse = { summary: [], data: [{ id: 'pod-1' }] };
600
+
601
+ const { wrapper } = mountComposable({ clusterId: 'c-empty-summary-with-data' }, dataWithoutSummaryResponse);
602
+
603
+ await flushPromises();
604
+
605
+ expect(mockRouterPush).toHaveBeenCalledWith(deploymentRouteFor('c-empty-summary-with-data'));
606
+ wrapper.unmount();
607
+ });
608
+
609
+ it('should redirect when there is no summary but data is present', async() => {
610
+ const dataWithoutSummaryResponse = { data: [{ id: 'pod-1' }] };
611
+
612
+ const { wrapper } = mountComposable({ clusterId: 'c-no-summary-with-data' }, dataWithoutSummaryResponse);
613
+
614
+ await flushPromises();
615
+
616
+ expect(mockRouterPush).toHaveBeenCalledWith(deploymentRouteFor('c-no-summary-with-data'));
617
+ wrapper.unmount();
618
+ });
619
+
620
+ it('should not redirect when every count entry has a total', async() => {
621
+ const { wrapper } = mountComposable();
622
+
623
+ await flushPromises();
624
+
625
+ expect(mockRouterPush).not.toHaveBeenCalled();
626
+ wrapper.unmount();
627
+ });
628
+
629
+ it('should not redirect when the summary is empty and there is no data', async() => {
630
+ const emptyResponse = { summary: [], data: [] };
631
+
632
+ const { wrapper } = mountComposable({}, emptyResponse);
633
+
634
+ await flushPromises();
635
+
636
+ expect(mockRouterPush).not.toHaveBeenCalled();
637
+ wrapper.unmount();
638
+ });
639
+
640
+ it('should not redirect when there is no summary and no data', async() => {
641
+ const noSummaryResponse = { data: [] };
642
+
643
+ const { wrapper } = mountComposable({}, noSummaryResponse);
644
+
645
+ await flushPromises();
646
+
647
+ expect(mockRouterPush).not.toHaveBeenCalled();
648
+ wrapper.unmount();
649
+ });
650
+
651
+ it('should not redirect for entries returned with an error', async() => {
652
+ const errorResponse = { summary: null, error: 'No access' };
653
+
654
+ const { wrapper } = mountComposable({}, errorResponse);
655
+
656
+ await flushPromises();
657
+
658
+ expect(mockRouterPush).not.toHaveBeenCalled();
659
+ wrapper.unmount();
660
+ });
661
+
662
+ it('should not redirect when the user has already navigated away from the dashboard', async() => {
663
+ // The fetch resolves after the user left the workload dashboard.
664
+ mockCurrentRoute.value.name = 'c-cluster-product-resource';
665
+
666
+ const { wrapper } = mountComposable({ clusterId: 'c-navigated-away' }, malformedSummaryResponse);
667
+
668
+ await flushPromises();
669
+
670
+ expect(mockRouterPush).not.toHaveBeenCalled();
671
+ wrapper.unmount();
672
+ });
673
+
674
+ it('should skip fetching and redirect immediately for a cluster already known to be invalid', async() => {
675
+ const cluster = 'c-cached-invalid';
676
+
677
+ // First mount detects the malformed response and caches the cluster.
678
+ const first = mountComposable({ clusterId: cluster }, malformedSummaryResponse);
679
+
680
+ await flushPromises();
681
+ first.wrapper.unmount();
682
+
683
+ jest.clearAllMocks();
684
+
685
+ // Second mount returns a valid response, but the cache should short-circuit the
686
+ // fetch entirely and redirect without ever requesting the summaries again.
687
+ const second = mountComposable({ clusterId: cluster }, summaryResponse);
688
+
689
+ await flushPromises();
690
+
691
+ expect(mockDispatch).not.toHaveBeenCalledWith('cluster/request', expect.anything());
692
+ expect(mockRouterPush).toHaveBeenCalledWith(deploymentRouteFor(cluster));
693
+ second.wrapper.unmount();
694
+ });
695
+ });
561
696
  });