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

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 (315) hide show
  1. package/assets/styles/global/_button.scss +1 -1
  2. package/assets/styles/global/_layout.scss +4 -0
  3. package/assets/translations/en-us.yaml +183 -51
  4. package/assets/translations/zh-hans.yaml +1 -7
  5. package/chart/monitoring/ClusterSelector.vue +0 -21
  6. package/chart/monitoring/prometheus/index.vue +6 -3
  7. package/components/ActionDropdownShell.vue +5 -3
  8. package/components/ButtonGroup.vue +26 -1
  9. package/components/CruResource.vue +212 -16
  10. package/components/ExplorerMembers.vue +8 -4
  11. package/components/ExplorerProjectsNamespaces.vue +10 -6
  12. package/components/GrowlManager.vue +4 -0
  13. package/components/MgmtNodeList.vue +184 -0
  14. package/components/PromptRestore.vue +93 -32
  15. package/components/Questions/index.vue +1 -0
  16. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +90 -1
  17. package/components/Resource/Detail/Card/StateCard/composables.ts +57 -87
  18. package/components/Resource/Detail/Card/StatusCard/__tests__/StatusCard.test.ts +61 -0
  19. package/components/Resource/Detail/Card/StatusCard/index.vue +61 -15
  20. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +2 -0
  21. package/components/Resource/Detail/Metadata/KeyValue.vue +5 -2
  22. package/components/Resource/Detail/Metadata/KeyValueRow.vue +2 -6
  23. package/components/ResourceDetail/index.vue +1 -1
  24. package/components/ResourceList/Masthead.vue +7 -1
  25. package/components/ResourceList/index.vue +82 -1
  26. package/components/ResourceTable.vue +1 -0
  27. package/components/RichTranslation.vue +5 -2
  28. package/components/Setting.vue +1 -0
  29. package/components/SortableTable/index.vue +4 -3
  30. package/components/SubtleLink.vue +31 -6
  31. package/components/Tabbed/Tab.vue +29 -3
  32. package/components/Tabbed/index.vue +25 -3
  33. package/components/TableOfContents/TableOfContents.vue +109 -0
  34. package/components/TableOfContents/composables.ts +258 -0
  35. package/components/Window/ContainerShell.vue +21 -11
  36. package/components/Window/__tests__/ContainerShell.test.ts +107 -37
  37. package/components/Wizard.vue +23 -5
  38. package/components/__tests__/ButtonGroup.test.ts +56 -0
  39. package/components/__tests__/PromptRestore.test.ts +169 -19
  40. package/components/fleet/AppCoChartGrid.vue +401 -0
  41. package/components/fleet/AppCoEmptyState.vue +127 -0
  42. package/components/fleet/AppCoPageHeader.vue +119 -0
  43. package/components/fleet/AppCoVersionSelect.vue +70 -0
  44. package/components/fleet/FleetClusterTargets/ClusterSelectionFields.vue +217 -0
  45. package/components/fleet/FleetClusterTargets/TargetsList.vue +123 -35
  46. package/components/fleet/FleetClusterTargets/index.vue +189 -146
  47. package/components/fleet/FleetIntro.vue +7 -3
  48. package/components/fleet/FleetNoWorkspaces.vue +7 -3
  49. package/components/fleet/FleetSecretSelector.vue +5 -3
  50. package/components/fleet/FleetValuesFrom.vue +8 -2
  51. package/components/fleet/GitRepoAdvancedTab.vue +1 -0
  52. package/components/fleet/GitRepoMetadataTab.vue +5 -0
  53. package/components/fleet/GitRepoTargetTab.vue +0 -2
  54. package/components/fleet/HelmOpAdvancedTab.vue +19 -53
  55. package/components/fleet/HelmOpAppCoConfigTab.vue +597 -0
  56. package/components/fleet/HelmOpAppCoResourcesSection.vue +162 -0
  57. package/components/fleet/HelmOpMetadataTab.vue +5 -0
  58. package/components/fleet/HelmOpResourcesSection.vue +82 -0
  59. package/components/fleet/HelmOpTargetOptionsSection.vue +89 -0
  60. package/components/fleet/HelmOpTargetTab.vue +64 -60
  61. package/components/fleet/HelmOpValuesTab.vue +129 -105
  62. package/components/fleet/__tests__/AppCoEmptyState.test.ts +71 -0
  63. package/components/fleet/__tests__/AppCoVersionSelect.test.ts +36 -0
  64. package/components/fleet/__tests__/ClusterSelectionFields.test.ts +62 -0
  65. package/components/fleet/__tests__/FleetClusterTargets.test.ts +253 -0
  66. package/components/fleet/__tests__/FleetSecretSelector.test.ts +16 -0
  67. package/components/fleet/__tests__/FleetValuesFrom.test.ts +44 -0
  68. package/components/fleet/__tests__/HelmOpAppCoConfigTab.test.ts +59 -0
  69. package/components/fleet/__tests__/HelmOpAppCoResourcesSection.test.ts +62 -0
  70. package/components/fleet/__tests__/HelmOpResourcesSection.test.ts +43 -0
  71. package/components/fleet/__tests__/HelmOpTargetOptionsSection.test.ts +34 -0
  72. package/components/fleet/__tests__/HelmOpValuesTab.test.ts +39 -0
  73. package/components/fleet/__tests__/__snapshots__/AppCoEmptyState.test.ts.snap +97 -0
  74. package/components/fleet/__tests__/__snapshots__/AppCoVersionSelect.test.ts.snap +30 -0
  75. package/components/fleet/__tests__/__snapshots__/ClusterSelectionFields.test.ts.snap +209 -0
  76. package/components/fleet/__tests__/__snapshots__/HelmOpTargetOptionsSection.test.ts.snap +140 -0
  77. package/components/fleet/dashboard/Empty.vue +8 -4
  78. package/components/fleet/dashboard/ResourceCard.vue +28 -0
  79. package/components/fleet/dashboard/ResourceDetails.vue +28 -0
  80. package/components/fleet/dashboard/__tests__/ResourceCard.test.ts +87 -0
  81. package/components/form/ArrayList.vue +61 -4
  82. package/components/form/FileSelector.vue +39 -1
  83. package/components/form/KeyValue.vue +23 -2
  84. package/components/form/LabeledSelect.vue +39 -1
  85. package/components/form/Labels.vue +22 -3
  86. package/components/form/NameNsDescription.vue +13 -5
  87. package/components/form/PrivateRegistry.constants.ts +7 -0
  88. package/components/form/PrivateRegistry.vue +253 -18
  89. package/components/form/ResourceTabs/index.vue +1 -0
  90. package/components/form/SelectOrCreateAuthSecret.vue +140 -17
  91. package/components/form/__tests__/FileSelector.test.ts +23 -0
  92. package/components/form/__tests__/NameNsDescription.test.ts +75 -0
  93. package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
  94. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
  95. package/components/formatter/EtcdSnapshotName.vue +73 -0
  96. package/components/formatter/InternalExternalIP.vue +10 -4
  97. package/components/formatter/ServiceTargets.vue +26 -7
  98. package/components/formatter/__tests__/InternalExternalIP.test.ts +132 -0
  99. package/components/formatter/__tests__/ServiceTargets.test.ts +412 -0
  100. package/components/nav/Header.vue +12 -1
  101. package/components/nav/TopLevelMenu.vue +7 -2
  102. package/components/nav/__tests__/Header.test.ts +15 -0
  103. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -2
  104. package/components/templates/default.vue +16 -4
  105. package/components/templates/home.vue +9 -4
  106. package/components/templates/plain.vue +9 -4
  107. package/composables/useHelmOpResources.test.ts +56 -0
  108. package/composables/useHelmOpResources.ts +32 -0
  109. package/composables/useStateColor.test.ts +325 -0
  110. package/composables/useStateColor.ts +128 -0
  111. package/config/features.js +1 -0
  112. package/config/home-links.js +1 -1
  113. package/config/labels-annotations.js +3 -0
  114. package/config/product/explorer.js +17 -4
  115. package/config/product/manager.js +8 -0
  116. package/config/router/index.js +16 -0
  117. package/config/router/navigation-guards/__tests__/authentication.test.ts +130 -0
  118. package/config/router/navigation-guards/authentication.js +10 -4
  119. package/config/router/routes.js +20 -6
  120. package/config/secret.ts +10 -0
  121. package/config/settings.ts +6 -4
  122. package/config/table-headers.js +3 -4
  123. package/config/types.js +16 -0
  124. package/core/plugin-products-base.ts +3 -3
  125. package/core/plugin-types.ts +83 -30
  126. package/core/plugin.ts +3 -0
  127. package/core/types-provisioning.ts +34 -1
  128. package/core/types.ts +15 -2
  129. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +114 -0
  130. package/detail/__tests__/workload.test.ts +3 -152
  131. package/detail/catalog.cattle.io.clusterrepo.vue +1 -1
  132. package/detail/provisioning.cattle.io.cluster.vue +109 -7
  133. package/detail/workload/index.vue +12 -55
  134. package/dialog/RotateEncryptionKeyDialog.vue +33 -9
  135. package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
  136. package/edit/__tests__/catalog.cattle.io.clusterrepo.test.ts +248 -0
  137. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
  138. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +206 -0
  139. package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
  140. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  141. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +1 -0
  142. package/edit/auth/__tests__/azuread.test.ts +34 -9
  143. package/edit/auth/__tests__/github.test.ts +234 -0
  144. package/edit/auth/__tests__/oidc.test.ts +26 -6
  145. package/edit/auth/__tests__/saml.test.ts +196 -0
  146. package/edit/auth/azuread.vue +128 -95
  147. package/edit/auth/github.vue +72 -13
  148. package/edit/auth/ldap/__tests__/index.test.ts +206 -0
  149. package/edit/auth/ldap/config.vue +8 -0
  150. package/edit/auth/ldap/index.vue +75 -1
  151. package/edit/auth/oidc.vue +119 -73
  152. package/edit/auth/saml.vue +76 -12
  153. package/edit/catalog.cattle.io.clusterrepo.vue +140 -32
  154. package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
  155. package/edit/fleet.cattle.io.gitrepo.vue +70 -16
  156. package/edit/fleet.cattle.io.helmop.vue +542 -141
  157. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  158. package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
  159. package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
  160. package/edit/management.cattle.io.user.vue +5 -2
  161. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
  162. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
  163. package/edit/provisioning.cattle.io.cluster/rke2.vue +89 -11
  164. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  165. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
  166. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
  167. package/list/group.principal.vue +5 -4
  168. package/list/harvesterhci.io.management.cluster.vue +8 -9
  169. package/list/management.cattle.io.user.vue +12 -9
  170. package/list/provisioning.cattle.io.cluster.vue +16 -10
  171. package/mixins/__tests__/auth-config.test.ts +90 -0
  172. package/mixins/__tests__/chart.test.ts +94 -0
  173. package/mixins/__tests__/resource-fetch-api-pagination.test.ts +48 -0
  174. package/mixins/auth-config.js +7 -0
  175. package/mixins/chart.js +11 -2
  176. package/mixins/child-hook.js +12 -6
  177. package/mixins/create-edit-view/impl.js +5 -3
  178. package/mixins/resource-fetch-api-pagination.js +21 -1
  179. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +57 -0
  180. package/models/__tests__/compliance.cattle.io.clusterscan.test.ts +144 -0
  181. package/models/__tests__/fleet-application.test.ts +175 -0
  182. package/models/__tests__/fleet.cattle.io.bundle.test.ts +169 -0
  183. package/models/__tests__/fleet.cattle.io.helmop.test.ts +84 -0
  184. package/models/__tests__/management.cattle.io.node.ts +22 -0
  185. package/models/__tests__/namespace.test.ts +36 -0
  186. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +205 -0
  187. package/models/__tests__/secret.test.ts +68 -1
  188. package/models/__tests__/workload.test.ts +401 -26
  189. package/models/catalog.cattle.io.clusterrepo.js +28 -4
  190. package/models/compliance.cattle.io.clusterscan.js +39 -4
  191. package/models/fleet-application.js +4 -0
  192. package/models/fleet.cattle.io.helmop.js +20 -1
  193. package/models/management.cattle.io.cluster.js +39 -5
  194. package/models/management.cattle.io.node.js +44 -3
  195. package/models/namespace.js +1 -1
  196. package/models/pod.js +46 -3
  197. package/models/provisioning.cattle.io.cluster.js +64 -14
  198. package/models/rke.cattle.io.etcdsnapshot.js +17 -9
  199. package/models/secret.js +19 -0
  200. package/models/workload.js +120 -20
  201. package/models/workload.service.js +5 -0
  202. package/package.json +14 -13
  203. package/pages/about.vue +5 -6
  204. package/pages/auth/login.vue +0 -35
  205. package/pages/auth/setup.vue +11 -0
  206. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +2 -2
  207. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +10 -1
  208. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +93 -0
  209. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +485 -107
  210. package/pages/c/_cluster/apps/charts/chart.vue +2 -1
  211. package/pages/c/_cluster/apps/charts/index.vue +48 -10
  212. package/pages/c/_cluster/apps/charts/install.vue +236 -144
  213. package/pages/c/_cluster/auth/roles/index.vue +5 -4
  214. package/pages/c/_cluster/explorer/workload-dashboard/ByNamespaceSection.vue +31 -0
  215. package/pages/c/_cluster/explorer/workload-dashboard/ByStateSection.vue +138 -0
  216. package/pages/c/_cluster/explorer/workload-dashboard/ByTypeSection.vue +30 -0
  217. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadCard.vue +155 -0
  218. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadNamespaceCard.vue +142 -0
  219. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadTypeCard.vue +159 -0
  220. package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +561 -0
  221. package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +440 -0
  222. package/pages/c/_cluster/explorer/workload-dashboard/index.vue +187 -0
  223. package/pages/c/_cluster/explorer/workload-dashboard/types.ts +80 -0
  224. package/pages/c/_cluster/fleet/application/create.vue +187 -136
  225. package/pages/c/_cluster/fleet/application/index.vue +5 -3
  226. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailBody.vue +338 -0
  227. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailHeader.vue +121 -0
  228. package/pages/c/_cluster/fleet/application/suse-app-collection/chart.vue +369 -0
  229. package/pages/c/_cluster/fleet/application/suse-app-collection/charts.vue +248 -0
  230. package/pages/c/_cluster/fleet/application/suse-app-collection/credentials.vue +310 -0
  231. package/pages/c/_cluster/fleet/index.vue +2 -2
  232. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +96 -0
  233. package/pages/c/_cluster/uiplugins/index.vue +15 -0
  234. package/pages/fail-whale.vue +16 -11
  235. package/pages/home.vue +16 -46
  236. package/pkg/require-asset.lib.js +25 -0
  237. package/pkg/vue.config.js +7 -0
  238. package/plugins/clean-html.d.ts +9 -0
  239. package/plugins/dashboard-store/__tests__/resource-class.test.ts +177 -0
  240. package/plugins/dashboard-store/getters.js +0 -1
  241. package/plugins/dashboard-store/resource-class.js +114 -19
  242. package/plugins/steve/__tests__/actions.test.ts +212 -0
  243. package/plugins/steve/actions.js +96 -0
  244. package/plugins/steve/steve-pagination-utils.ts +1 -1
  245. package/rancher-components/Accordion/Accordion.vue +53 -9
  246. package/rancher-components/Form/Checkbox/Checkbox.vue +14 -0
  247. package/rancher-components/Form/Radio/RadioButton.vue +17 -1
  248. package/rancher-components/Form/Radio/RadioGroup.vue +10 -0
  249. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
  250. package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
  251. package/rancher-components/Pill/RcTag/RcTag.vue +3 -2
  252. package/rancher-components/RcButton/RcButton.test.ts +103 -0
  253. package/rancher-components/RcButton/RcButton.vue +94 -15
  254. package/rancher-components/RcButton/index.ts +1 -1
  255. package/rancher-components/RcButton/types.ts +3 -0
  256. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
  257. package/rancher-components/RcItemCard/RcItemCard.test.ts +18 -0
  258. package/rancher-components/RcItemCard/RcItemCard.vue +2 -2
  259. package/rancher-components/RcSection/RcSection.vue +28 -3
  260. package/scripts/extension/helm/package/Dockerfile +1 -1
  261. package/scripts/test-plugins-build.sh +2 -1
  262. package/store/__tests__/features.test.ts +131 -0
  263. package/store/__tests__/growl.test.ts +374 -0
  264. package/store/__tests__/modal.test.ts +131 -0
  265. package/store/__tests__/notifications.test.ts +434 -0
  266. package/store/__tests__/slideInPanel.test.ts +88 -0
  267. package/store/__tests__/type-map.utils.test.ts +433 -0
  268. package/store/catalog.js +57 -0
  269. package/store/features.js +4 -0
  270. package/store/plugins.js +7 -4
  271. package/types/components/buttonGroup.ts +5 -0
  272. package/types/shell/index.d.ts +166 -70
  273. package/utils/__tests__/auth.test.ts +273 -0
  274. package/utils/__tests__/computed.test.ts +193 -0
  275. package/utils/__tests__/cspAdaptor.test.ts +163 -0
  276. package/utils/__tests__/dom.test.ts +81 -0
  277. package/utils/__tests__/duration.test.ts +37 -1
  278. package/utils/__tests__/dynamic-importer.test.ts +102 -0
  279. package/utils/__tests__/fleet-appco.test.ts +312 -0
  280. package/utils/__tests__/monitoring.test.ts +130 -0
  281. package/utils/__tests__/object.test.ts +22 -0
  282. package/utils/__tests__/operation-cr.test.ts +34 -0
  283. package/utils/__tests__/platform.test.ts +91 -0
  284. package/utils/__tests__/position.test.ts +237 -0
  285. package/utils/__tests__/provider.test.ts +51 -1
  286. package/utils/__tests__/queue.test.ts +232 -0
  287. package/utils/__tests__/release-notes.test.ts +221 -0
  288. package/utils/__tests__/router.test.js +254 -1
  289. package/utils/__tests__/select.test.ts +208 -0
  290. package/utils/__tests__/time.test.ts +265 -1
  291. package/utils/__tests__/title.test.ts +47 -0
  292. package/utils/__tests__/width.test.ts +53 -0
  293. package/utils/__tests__/window.test.ts +158 -0
  294. package/utils/__tests__/xccdf.test.ts +126 -6
  295. package/utils/crypto/__tests__/browserHashUtils.test.ts +98 -0
  296. package/utils/crypto/__tests__/index.test.ts +144 -0
  297. package/utils/duration.ts +104 -0
  298. package/utils/dynamic-content/__tests__/notification-handler.test.ts +196 -0
  299. package/utils/dynamic-content/info.ts +2 -1
  300. package/utils/error.js +13 -0
  301. package/utils/fleet-appco.ts +323 -0
  302. package/utils/object.js +22 -2
  303. package/utils/operation-cr.js +19 -0
  304. package/utils/provider.ts +12 -0
  305. package/utils/require-asset.ts +7 -0
  306. package/utils/validators/__tests__/container-images.test.ts +104 -0
  307. package/utils/validators/__tests__/flow-output.test.ts +91 -0
  308. package/utils/validators/__tests__/logging-outputs.test.ts +58 -0
  309. package/utils/validators/__tests__/monitoring-route.test.ts +119 -0
  310. package/utils/validators/__tests__/private-registry.test.ts +27 -15
  311. package/utils/validators/private-registry.ts +15 -4
  312. package/utils/xccdf.ts +39 -42
  313. package/vue.config.js +1 -1
  314. package/pages/support/index.vue +0 -264
  315. package/utils/duration.js +0 -43
@@ -0,0 +1,412 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import ServiceTargets from '@shell/components/formatter/ServiceTargets.vue';
3
+
4
+ describe('component: ServiceTargets', () => {
5
+ const proxyUrl = (scheme: string, port: number) => `https://rancher.test/k8s/clusters/local/api/v1/namespaces/default/services/${ scheme }:test-service:${ port }/proxy`;
6
+
7
+ function createRow(overrides: Record<string, any> = {}) {
8
+ return {
9
+ metadata: { annotations: {}, ...overrides.metadata },
10
+ spec: {
11
+ clusterIP: '10.43.0.100',
12
+ ports: [],
13
+ type: 'ClusterIP',
14
+ ...overrides.spec,
15
+ },
16
+ proxyUrl,
17
+ };
18
+ }
19
+
20
+ function mountComponent(row: Record<string, any>) {
21
+ return shallowMount(ServiceTargets, {
22
+ props: {
23
+ value: null,
24
+ row,
25
+ col: {},
26
+ },
27
+ global: { directives: { 'clean-html': () => {} } },
28
+ });
29
+ }
30
+
31
+ describe('default port handling', () => {
32
+ it.each([
33
+ ['80', 80],
34
+ ['443', 443],
35
+ ['8080', 8080],
36
+ ['8443', 8443],
37
+ ])('should generate a clickable link for TCP port %s', (_label, port) => {
38
+ const row = createRow({
39
+ spec: {
40
+ ports: [{
41
+ port, protocol: 'TCP', targetPort: port
42
+ }]
43
+ }
44
+ });
45
+ const wrapper = mountComponent(row);
46
+ const parsed = (wrapper.vm as any).parsed;
47
+
48
+ expect(parsed).toHaveLength(1);
49
+ expect(parsed[0].label).toContain('<a href=');
50
+ expect(parsed[0].label).toContain(`proxy`);
51
+ });
52
+
53
+ it.each([
54
+ ['3000', 3000],
55
+ ['9090', 9090],
56
+ ['5000', 5000],
57
+ ])('should NOT generate a clickable link for TCP port %s without annotation', (_label, port) => {
58
+ const row = createRow({
59
+ spec: {
60
+ ports: [{
61
+ port, protocol: 'TCP', targetPort: port
62
+ }]
63
+ }
64
+ });
65
+ const wrapper = mountComponent(row);
66
+ const parsed = (wrapper.vm as any).parsed;
67
+
68
+ expect(parsed).toHaveLength(1);
69
+ expect(parsed[0].label).not.toContain('<a href=');
70
+ });
71
+
72
+ it('should NOT generate a clickable link for non-TCP protocol', () => {
73
+ const row = createRow({
74
+ spec: {
75
+ ports: [{
76
+ port: 80, protocol: 'UDP', targetPort: 80
77
+ }]
78
+ }
79
+ });
80
+ const wrapper = mountComponent(row);
81
+ const parsed = (wrapper.vm as any).parsed;
82
+
83
+ expect(parsed).toHaveLength(1);
84
+ expect(parsed[0].label).not.toContain('<a href=');
85
+ });
86
+ });
87
+
88
+ describe('service-links annotation', () => {
89
+ it('should generate a clickable link for a port listed in the annotation', () => {
90
+ const row = createRow({
91
+ metadata: { annotations: { 'ui.rancher/service-links': '3000' } },
92
+ spec: {
93
+ ports: [{
94
+ port: 3000, protocol: 'TCP', targetPort: 3000
95
+ }]
96
+ },
97
+ });
98
+ const wrapper = mountComponent(row);
99
+ const parsed = (wrapper.vm as any).parsed;
100
+
101
+ expect(parsed).toHaveLength(1);
102
+ expect(parsed[0].label).toContain('<a href=');
103
+ expect(parsed[0].label).toContain('http:test-service:3000');
104
+ });
105
+
106
+ it('should generate clickable links for multiple ports in the annotation', () => {
107
+ const row = createRow({
108
+ metadata: { annotations: { 'ui.rancher/service-links': '3000,9090' } },
109
+ spec: {
110
+ ports: [
111
+ {
112
+ port: 3000, protocol: 'TCP', targetPort: 3000
113
+ },
114
+ {
115
+ port: 9090, protocol: 'TCP', targetPort: 9090
116
+ },
117
+ {
118
+ port: 5000, protocol: 'TCP', targetPort: 5000
119
+ },
120
+ ],
121
+ },
122
+ });
123
+ const wrapper = mountComponent(row);
124
+ const parsed = (wrapper.vm as any).parsed;
125
+
126
+ expect(parsed).toHaveLength(3);
127
+ expect(parsed[0].label).toContain('<a href=');
128
+ expect(parsed[1].label).toContain('<a href=');
129
+ expect(parsed[2].label).not.toContain('<a href=');
130
+ });
131
+
132
+ it('should still generate links for default ports (80, 443) even without annotation', () => {
133
+ const row = createRow({
134
+ spec: {
135
+ ports: [
136
+ {
137
+ port: 80, protocol: 'TCP', targetPort: 80
138
+ },
139
+ {
140
+ port: 443, protocol: 'TCP', targetPort: 443
141
+ },
142
+ ],
143
+ },
144
+ });
145
+ const wrapper = mountComponent(row);
146
+ const parsed = (wrapper.vm as any).parsed;
147
+
148
+ expect(parsed).toHaveLength(2);
149
+ expect(parsed[0].label).toContain('<a href=');
150
+ expect(parsed[1].label).toContain('<a href=');
151
+ });
152
+
153
+ it('should handle whitespace in the annotation value', () => {
154
+ const row = createRow({
155
+ metadata: { annotations: { 'ui.rancher/service-links': ' 3000 , 9090 ' } },
156
+ spec: {
157
+ ports: [
158
+ {
159
+ port: 3000, protocol: 'TCP', targetPort: 3000
160
+ },
161
+ {
162
+ port: 9090, protocol: 'TCP', targetPort: 9090
163
+ },
164
+ ],
165
+ },
166
+ });
167
+ const wrapper = mountComponent(row);
168
+ const parsed = (wrapper.vm as any).parsed;
169
+
170
+ expect(parsed).toHaveLength(2);
171
+ expect(parsed[0].label).toContain('<a href=');
172
+ expect(parsed[1].label).toContain('<a href=');
173
+ });
174
+
175
+ it('should handle an empty annotation value gracefully', () => {
176
+ const row = createRow({
177
+ metadata: { annotations: { 'ui.rancher/service-links': '' } },
178
+ spec: {
179
+ ports: [{
180
+ port: 3000, protocol: 'TCP', targetPort: 3000
181
+ }]
182
+ },
183
+ });
184
+ const wrapper = mountComponent(row);
185
+ const parsed = (wrapper.vm as any).parsed;
186
+
187
+ expect(parsed).toHaveLength(1);
188
+ expect(parsed[0].label).not.toContain('<a href=');
189
+ });
190
+
191
+ it('should ignore non-numeric values in the annotation', () => {
192
+ const row = createRow({
193
+ metadata: { annotations: { 'ui.rancher/service-links': '3000,abc,9090' } },
194
+ spec: {
195
+ ports: [
196
+ {
197
+ port: 3000, protocol: 'TCP', targetPort: 3000
198
+ },
199
+ {
200
+ port: 9090, protocol: 'TCP', targetPort: 9090
201
+ },
202
+ ],
203
+ },
204
+ });
205
+ const wrapper = mountComponent(row);
206
+ const parsed = (wrapper.vm as any).parsed;
207
+
208
+ expect(parsed).toHaveLength(2);
209
+ expect(parsed[0].label).toContain('<a href=');
210
+ expect(parsed[1].label).toContain('<a href=');
211
+ });
212
+
213
+ it('should not generate a link for annotated port with non-TCP protocol', () => {
214
+ const row = createRow({
215
+ metadata: { annotations: { 'ui.rancher/service-links': '3000' } },
216
+ spec: {
217
+ ports: [{
218
+ port: 3000, protocol: 'UDP', targetPort: 3000
219
+ }]
220
+ },
221
+ });
222
+ const wrapper = mountComponent(row);
223
+ const parsed = (wrapper.vm as any).parsed;
224
+
225
+ expect(parsed).toHaveLength(1);
226
+ expect(parsed[0].label).not.toContain('<a href=');
227
+ });
228
+
229
+ it('should use https scheme for annotated port that isMaybeSecure considers secure', () => {
230
+ const row = createRow({
231
+ metadata: { annotations: { 'ui.rancher/service-links': '8443' } },
232
+ spec: {
233
+ ports: [{
234
+ port: 8443, protocol: 'TCP', targetPort: 8443
235
+ }]
236
+ },
237
+ });
238
+ const wrapper = mountComponent(row);
239
+ const parsed = (wrapper.vm as any).parsed;
240
+
241
+ expect(parsed).toHaveLength(1);
242
+ expect(parsed[0].label).toContain('https:test-service:8443');
243
+ });
244
+
245
+ it('should use http scheme for annotated port that is not secure', () => {
246
+ const row = createRow({
247
+ metadata: { annotations: { 'ui.rancher/service-links': '3000' } },
248
+ spec: {
249
+ ports: [{
250
+ port: 3000, protocol: 'TCP', targetPort: 3000
251
+ }]
252
+ },
253
+ });
254
+ const wrapper = mountComponent(row);
255
+ const parsed = (wrapper.vm as any).parsed;
256
+
257
+ expect(parsed).toHaveLength(1);
258
+ expect(parsed[0].label).toContain('http:test-service:3000');
259
+ });
260
+
261
+ it('should use explicit https scheme from annotation even for non-secure port', () => {
262
+ const row = createRow({
263
+ metadata: { annotations: { 'ui.rancher/service-links': '3000/https' } },
264
+ spec: {
265
+ ports: [{
266
+ port: 3000, protocol: 'TCP', targetPort: 3000
267
+ }]
268
+ },
269
+ });
270
+ const wrapper = mountComponent(row);
271
+ const parsed = (wrapper.vm as any).parsed;
272
+
273
+ expect(parsed).toHaveLength(1);
274
+ expect(parsed[0].label).toContain('https:test-service:3000');
275
+ });
276
+
277
+ it('should use explicit http scheme from annotation even for secure port', () => {
278
+ const row = createRow({
279
+ metadata: { annotations: { 'ui.rancher/service-links': '8443/http' } },
280
+ spec: {
281
+ ports: [{
282
+ port: 8443, protocol: 'TCP', targetPort: 8443
283
+ }]
284
+ },
285
+ });
286
+ const wrapper = mountComponent(row);
287
+ const parsed = (wrapper.vm as any).parsed;
288
+
289
+ expect(parsed).toHaveLength(1);
290
+ expect(parsed[0].label).toContain('http:test-service:8443');
291
+ });
292
+
293
+ it('should not add port 0 from trailing comma in annotation', () => {
294
+ const row = createRow({
295
+ metadata: { annotations: { 'ui.rancher/service-links': '3000,' } },
296
+ spec: {
297
+ ports: [
298
+ {
299
+ port: 3000, protocol: 'TCP', targetPort: 3000
300
+ },
301
+ {
302
+ port: 0, protocol: 'TCP', targetPort: 0
303
+ },
304
+ ],
305
+ },
306
+ });
307
+ const wrapper = mountComponent(row);
308
+ const parsed = (wrapper.vm as any).parsed;
309
+
310
+ expect(parsed).toHaveLength(2);
311
+ expect(parsed[0].label).toContain('<a href=');
312
+ expect(parsed[1].label).not.toContain('<a href=');
313
+ });
314
+
315
+ it('should fall back to isMaybeSecure for invalid scheme values', () => {
316
+ const row = createRow({
317
+ metadata: { annotations: { 'ui.rancher/service-links': '3000/ftp' } },
318
+ spec: {
319
+ ports: [{
320
+ port: 3000, protocol: 'TCP', targetPort: 3000
321
+ }]
322
+ },
323
+ });
324
+ const wrapper = mountComponent(row);
325
+ const parsed = (wrapper.vm as any).parsed;
326
+
327
+ expect(parsed).toHaveLength(1);
328
+ expect(parsed[0].label).toContain('http:test-service:3000');
329
+ });
330
+
331
+ it('should handle mixed format with and without explicit scheme', () => {
332
+ const row = createRow({
333
+ metadata: { annotations: { 'ui.rancher/service-links': '3000/https,9090' } },
334
+ spec: {
335
+ ports: [
336
+ {
337
+ port: 3000, protocol: 'TCP', targetPort: 3000
338
+ },
339
+ {
340
+ port: 9090, protocol: 'TCP', targetPort: 9090
341
+ },
342
+ ],
343
+ },
344
+ });
345
+ const wrapper = mountComponent(row);
346
+ const parsed = (wrapper.vm as any).parsed;
347
+
348
+ expect(parsed).toHaveLength(2);
349
+ expect(parsed[0].label).toContain('https:test-service:3000');
350
+ expect(parsed[1].label).toContain('http:test-service:9090');
351
+ });
352
+ });
353
+
354
+ describe('empty ports', () => {
355
+ it('should show clusterIP when ports are empty', () => {
356
+ const row = createRow({ spec: { clusterIP: '10.43.0.100', ports: [] } });
357
+ const wrapper = mountComponent(row);
358
+ const parsed = (wrapper.vm as any).parsed;
359
+
360
+ expect(parsed).toHaveLength(1);
361
+ expect(parsed[0].label).toBe('10.43.0.100:');
362
+ });
363
+
364
+ it('should show externalName for ExternalName service type', () => {
365
+ const row = createRow({
366
+ spec: {
367
+ clusterIP: '',
368
+ ports: [],
369
+ type: 'ExternalName',
370
+ externalName: 'my.external.service',
371
+ },
372
+ });
373
+ const wrapper = mountComponent(row);
374
+ const parsed = (wrapper.vm as any).parsed;
375
+
376
+ expect(parsed).toHaveLength(1);
377
+ expect(parsed[0].label).toBe('my.external.service');
378
+ });
379
+ });
380
+
381
+ describe('port label formatting', () => {
382
+ it('should use port name when available in clickable link', () => {
383
+ const row = createRow({
384
+ spec: {
385
+ ports: [{
386
+ port: 80, protocol: 'TCP', targetPort: 80, name: 'http-web',
387
+ }],
388
+ },
389
+ });
390
+ const wrapper = mountComponent(row);
391
+ const parsed = (wrapper.vm as any).parsed;
392
+
393
+ expect(parsed[0].label).toContain('>http-web</a>');
394
+ });
395
+
396
+ it('should include target port and protocol in label', () => {
397
+ const row = createRow({
398
+ spec: {
399
+ ports: [{
400
+ port: 3000, protocol: 'TCP', targetPort: 8080
401
+ }]
402
+ }
403
+ });
404
+ const wrapper = mountComponent(row);
405
+ const parsed = (wrapper.vm as any).parsed;
406
+
407
+ expect(parsed[0].label).toContain('10.43.0.100:3000');
408
+ expect(parsed[0].label).toContain('8080');
409
+ expect(parsed[0].label).toContain('/TCP');
410
+ });
411
+ });
412
+ });
@@ -30,6 +30,7 @@ import {
30
30
  RcDropdownTrigger
31
31
  } from '@components/RcDropdown';
32
32
  import { SLO_AUTH_PROVIDERS } from '@shell/store/auth';
33
+ import { CLUSTER_SHELL } from '@shell/store/features';
33
34
 
34
35
  export default {
35
36
 
@@ -147,10 +148,16 @@ export default {
147
148
  return true;
148
149
  },
149
150
 
151
+ // Does the user have permissions to use the shell
150
152
  shellEnabled() {
151
153
  return !!this.currentCluster?.links?.shell;
152
154
  },
153
155
 
156
+ // Is the feature flag enabled for cluster shell access?
157
+ shellFeatureEnabled() {
158
+ return !!this.$store.getters['features/get'](CLUSTER_SHELL);
159
+ },
160
+
154
161
  showKubeShell() {
155
162
  return !this.rootProduct?.hideKubeShell;
156
163
  },
@@ -210,6 +217,10 @@ export default {
210
217
  return true;
211
218
  }
212
219
 
220
+ if (this.$route?.meta?.disableWorkspaceSwitcher) {
221
+ return true;
222
+ }
223
+
213
224
  return false;
214
225
  },
215
226
 
@@ -625,7 +636,7 @@ export default {
625
636
  </button>
626
637
 
627
638
  <button
628
- v-if="showKubeShell"
639
+ v-if="showKubeShell && shellFeatureEnabled"
629
640
  id="btn-kubectl"
630
641
  v-clean-tooltip="t('nav.shellShortcut', {key: shellShortcut})"
631
642
  v-shortkey="{windows: ['ctrl', '`'], mac: ['meta', '`']}"
@@ -12,6 +12,7 @@ import { KEY } from '@shell/utils/platform';
12
12
  import { getVersionInfo } from '@shell/utils/version';
13
13
  import { SETTING } from '@shell/config/settings';
14
14
  import { getProductFromRoute } from '@shell/utils/router';
15
+ import { NAME as EXPLORER } from '@shell/config/product/explorer';
15
16
  import { isRancherPrime } from '@shell/config/version';
16
17
  import Pinned from '@shell/components/nav/Pinned';
17
18
  import sideNavService from '@shell/components/nav/TopLevelMenu.helper';
@@ -99,7 +100,7 @@ export default {
99
100
  },
100
101
 
101
102
  routeComboActive() {
102
- if (!this.routeCombo) {
103
+ if (!this.routeCombo || !this.isCurrRouteClusterExplorer) {
103
104
  return false;
104
105
  }
105
106
 
@@ -237,7 +238,7 @@ export default {
237
238
  },
238
239
 
239
240
  isCurrRouteClusterExplorer() {
240
- return this.$route?.name?.startsWith('c-cluster');
241
+ return this.$route?.name?.startsWith('c-cluster') && this.productFromRoute === EXPLORER;
241
242
  },
242
243
 
243
244
  productFromRoute() {
@@ -393,6 +394,10 @@ export default {
393
394
  },
394
395
 
395
396
  handleKeyComboClick() {
397
+ if (!this.isCurrRouteClusterExplorer) {
398
+ return;
399
+ }
400
+
396
401
  this.routeCombo = !this.routeCombo;
397
402
  },
398
403
 
@@ -157,6 +157,21 @@ describe('component: Header', () => {
157
157
 
158
158
  expect((wrapper.vm as any).disableWorkspaceSwitcher).toBe(true);
159
159
  });
160
+
161
+ it.each([
162
+ ['c-cluster-fleet-application-appco-credentials', '/c/local/fleet/application/suse-app-collection/credentials'],
163
+ ['c-cluster-fleet-application-appco-charts', '/c/local/fleet/application/suse-app-collection/charts'],
164
+ ['c-cluster-fleet-application-appco-chart', '/c/local/fleet/application/suse-app-collection/chart'],
165
+ ])('should disable Workspace Switcher on AppCo page %s (via route meta)', (name, path) => {
166
+ const wrapper = createWrapper({
167
+ name,
168
+ path,
169
+ params: {},
170
+ meta: { disableWorkspaceSwitcher: true },
171
+ });
172
+
173
+ expect((wrapper.vm as any).disableWorkspaceSwitcher).toBe(true);
174
+ });
160
175
  });
161
176
 
162
177
  describe('showFilter', () => {
@@ -720,7 +720,7 @@ describe('topLevelMenu', () => {
720
720
  const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
721
721
  global: {
722
722
  mocks: {
723
- $route: {},
723
+ $route: { name: 'c-cluster-explorer', params: { cluster: 'local', product: 'explorer' } },
724
724
  $store: {
725
725
  ...generateStore([
726
726
  {
@@ -823,7 +823,7 @@ describe('topLevelMenu', () => {
823
823
  const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
824
824
  global: {
825
825
  mocks: {
826
- $route: {},
826
+ $route: { name: 'c-cluster-explorer', params: { cluster: 'local', product: 'explorer' } },
827
827
  $store: store
828
828
  },
829
829
  stubs: ['BrandImage', 'router-link'],
@@ -836,5 +836,123 @@ describe('topLevelMenu', () => {
836
836
  expect(wrapper.vm.routeComboActive).toBe(true);
837
837
  });
838
838
  });
839
+
840
+ describe('handleKeyComboClick', () => {
841
+ it('should not toggle routeCombo when route is a non-explorer c-cluster route', async() => {
842
+ const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
843
+ global: {
844
+ mocks: {
845
+ $route: { name: 'c-cluster-fleet', params: { cluster: 'local', product: 'fleet' } },
846
+ $router: { push: jest.fn() },
847
+ $store: { ...generateStore([]) }
848
+ },
849
+ stubs: ['BrandImage', 'router-link'],
850
+ }
851
+ });
852
+
853
+ await waitForIt();
854
+
855
+ expect(wrapper.vm.routeCombo).toBe(false);
856
+ wrapper.vm.handleKeyComboClick();
857
+ expect(wrapper.vm.routeCombo).toBe(false);
858
+ });
859
+
860
+ it('should toggle routeCombo when route is cluster explorer', async() => {
861
+ const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
862
+ global: {
863
+ mocks: {
864
+ $route: { name: 'c-cluster-explorer', params: { cluster: 'local', product: 'explorer' } },
865
+ $router: { push: jest.fn() },
866
+ $store: { ...generateStore([]) }
867
+ },
868
+ stubs: ['BrandImage', 'router-link'],
869
+ }
870
+ });
871
+
872
+ await waitForIt();
873
+
874
+ expect(wrapper.vm.routeCombo).toBe(false);
875
+ wrapper.vm.handleKeyComboClick();
876
+ expect(wrapper.vm.routeCombo).toBe(true);
877
+ });
878
+ });
879
+
880
+ describe('clusterMenuClick', () => {
881
+ it('should navigate normally on non-explorer c-cluster route even with routeCombo set', async() => {
882
+ const mockPush = jest.fn();
883
+ const clusterRoute = { name: 'c-cluster-explorer' };
884
+ const clusters = [
885
+ {
886
+ nameDisplay: 'cluster1',
887
+ id: 'an-id1',
888
+ mgmt: { id: 'an-id1' },
889
+ canExplore: true,
890
+ clusterRoute
891
+ },
892
+ {
893
+ nameDisplay: 'cluster2',
894
+ id: 'an-id2',
895
+ mgmt: { id: 'an-id2' },
896
+ canExplore: true,
897
+ clusterRoute
898
+ }
899
+ ];
900
+
901
+ const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
902
+ global: {
903
+ mocks: {
904
+ $route: { name: 'c-cluster-fleet', params: { cluster: 'local', product: 'fleet' } },
905
+ $router: { push: mockPush },
906
+ $store: { ...generateStore(clusters) }
907
+ },
908
+ stubs: ['BrandImage', 'router-link'],
909
+ }
910
+ });
911
+
912
+ await waitForIt();
913
+ await wrapper.setData({ routeCombo: true });
914
+
915
+ expect(wrapper.vm.routeComboActive).toBe(false);
916
+
917
+ const ev = { preventDefault: jest.fn() };
918
+
919
+ wrapper.vm.clusterMenuClick(ev, clusters[1]);
920
+
921
+ expect(mockPush).toHaveBeenCalledWith(clusterRoute);
922
+ });
923
+
924
+ it('should navigate to cluster route when routeComboActive is false', async() => {
925
+ const mockPush = jest.fn();
926
+ const clusterRoute = { name: 'c-cluster-explorer' };
927
+ const clusters = [
928
+ {
929
+ nameDisplay: 'cluster1',
930
+ id: 'an-id1',
931
+ mgmt: { id: 'an-id1' },
932
+ canExplore: true,
933
+ clusterRoute
934
+ }
935
+ ];
936
+
937
+ const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
938
+ global: {
939
+ mocks: {
940
+ $route: { name: 'fleet-management', params: {} },
941
+ $router: { push: mockPush },
942
+ $store: { ...generateStore(clusters) }
943
+ },
944
+ stubs: ['BrandImage', 'router-link'],
945
+ }
946
+ });
947
+
948
+ await waitForIt();
949
+
950
+ const ev = { preventDefault: jest.fn() };
951
+
952
+ wrapper.vm.clusterMenuClick(ev, clusters[0]);
953
+
954
+ expect(mockPush).toHaveBeenCalledWith(clusterRoute);
955
+ });
956
+ });
839
957
  });
840
958
  });