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

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 (258) hide show
  1. package/assets/styles/global/_layout.scss +4 -0
  2. package/assets/translations/en-us.yaml +144 -41
  3. package/assets/translations/zh-hans.yaml +1 -7
  4. package/chart/monitoring/ClusterSelector.vue +0 -21
  5. package/chart/monitoring/prometheus/index.vue +6 -3
  6. package/components/CruResource.vue +161 -14
  7. package/components/ExplorerMembers.vue +8 -4
  8. package/components/ExplorerProjectsNamespaces.vue +10 -6
  9. package/components/GrowlManager.vue +4 -0
  10. package/components/MgmtNodeList.vue +184 -0
  11. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +90 -1
  12. package/components/Resource/Detail/Card/StateCard/composables.ts +57 -87
  13. package/components/Resource/Detail/Card/StatusCard/__tests__/StatusCard.test.ts +61 -0
  14. package/components/Resource/Detail/Card/StatusCard/index.vue +61 -15
  15. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +2 -0
  16. package/components/Resource/Detail/Metadata/KeyValue.vue +5 -2
  17. package/components/Resource/Detail/Metadata/KeyValueRow.vue +2 -6
  18. package/components/ResourceDetail/index.vue +1 -1
  19. package/components/ResourceList/Masthead.vue +7 -1
  20. package/components/ResourceList/index.vue +82 -1
  21. package/components/RichTranslation.vue +5 -2
  22. package/components/Setting.vue +1 -0
  23. package/components/SubtleLink.vue +31 -6
  24. package/components/Tabbed/Tab.vue +29 -3
  25. package/components/Tabbed/index.vue +25 -3
  26. package/components/TableOfContents/TableOfContents.vue +109 -0
  27. package/components/TableOfContents/composables.ts +258 -0
  28. package/components/Window/ContainerShell.vue +21 -11
  29. package/components/Window/__tests__/ContainerShell.test.ts +107 -37
  30. package/components/Wizard.vue +9 -4
  31. package/components/fleet/AppCoChartGrid.vue +401 -0
  32. package/components/fleet/AppCoEmptyState.vue +127 -0
  33. package/components/fleet/AppCoPageHeader.vue +119 -0
  34. package/components/fleet/AppCoVersionSelect.vue +70 -0
  35. package/components/fleet/FleetClusterTargets/ClusterSelectionFields.vue +217 -0
  36. package/components/fleet/FleetClusterTargets/TargetsList.vue +123 -35
  37. package/components/fleet/FleetClusterTargets/index.vue +189 -146
  38. package/components/fleet/FleetIntro.vue +7 -3
  39. package/components/fleet/FleetNoWorkspaces.vue +7 -3
  40. package/components/fleet/FleetSecretSelector.vue +5 -3
  41. package/components/fleet/FleetValuesFrom.vue +8 -2
  42. package/components/fleet/GitRepoTargetTab.vue +0 -2
  43. package/components/fleet/HelmOpAdvancedTab.vue +19 -53
  44. package/components/fleet/HelmOpAppCoConfigTab.vue +593 -0
  45. package/components/fleet/HelmOpAppCoResourcesSection.vue +162 -0
  46. package/components/fleet/HelmOpResourcesSection.vue +82 -0
  47. package/components/fleet/HelmOpTargetOptionsSection.vue +89 -0
  48. package/components/fleet/HelmOpTargetTab.vue +64 -60
  49. package/components/fleet/HelmOpValuesTab.vue +129 -105
  50. package/components/fleet/__tests__/AppCoEmptyState.test.ts +71 -0
  51. package/components/fleet/__tests__/AppCoVersionSelect.test.ts +36 -0
  52. package/components/fleet/__tests__/ClusterSelectionFields.test.ts +62 -0
  53. package/components/fleet/__tests__/FleetClusterTargets.test.ts +253 -0
  54. package/components/fleet/__tests__/FleetSecretSelector.test.ts +16 -0
  55. package/components/fleet/__tests__/FleetValuesFrom.test.ts +44 -0
  56. package/components/fleet/__tests__/HelmOpAppCoConfigTab.test.ts +59 -0
  57. package/components/fleet/__tests__/HelmOpAppCoResourcesSection.test.ts +62 -0
  58. package/components/fleet/__tests__/HelmOpResourcesSection.test.ts +43 -0
  59. package/components/fleet/__tests__/HelmOpTargetOptionsSection.test.ts +34 -0
  60. package/components/fleet/__tests__/HelmOpValuesTab.test.ts +39 -0
  61. package/components/fleet/__tests__/__snapshots__/AppCoEmptyState.test.ts.snap +97 -0
  62. package/components/fleet/__tests__/__snapshots__/AppCoVersionSelect.test.ts.snap +30 -0
  63. package/components/fleet/__tests__/__snapshots__/ClusterSelectionFields.test.ts.snap +209 -0
  64. package/components/fleet/__tests__/__snapshots__/HelmOpTargetOptionsSection.test.ts.snap +140 -0
  65. package/components/fleet/dashboard/Empty.vue +8 -4
  66. package/components/fleet/dashboard/ResourceCard.vue +28 -0
  67. package/components/fleet/dashboard/ResourceDetails.vue +28 -0
  68. package/components/fleet/dashboard/__tests__/ResourceCard.test.ts +87 -0
  69. package/components/form/ArrayList.vue +61 -4
  70. package/components/form/KeyValue.vue +23 -2
  71. package/components/form/LabeledSelect.vue +39 -1
  72. package/components/form/Labels.vue +22 -3
  73. package/components/form/NameNsDescription.vue +13 -5
  74. package/components/form/ResourceTabs/index.vue +1 -0
  75. package/components/form/__tests__/NameNsDescription.test.ts +75 -0
  76. package/components/formatter/InternalExternalIP.vue +10 -4
  77. package/components/formatter/ServiceTargets.vue +26 -7
  78. package/components/formatter/__tests__/InternalExternalIP.test.ts +132 -0
  79. package/components/formatter/__tests__/ServiceTargets.test.ts +412 -0
  80. package/components/nav/Header.vue +4 -0
  81. package/components/nav/TopLevelMenu.vue +7 -2
  82. package/components/nav/__tests__/Header.test.ts +15 -0
  83. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -2
  84. package/components/templates/default.vue +9 -4
  85. package/components/templates/home.vue +9 -4
  86. package/components/templates/plain.vue +9 -4
  87. package/composables/useHelmOpResources.test.ts +56 -0
  88. package/composables/useHelmOpResources.ts +32 -0
  89. package/composables/useStateColor.test.ts +325 -0
  90. package/composables/useStateColor.ts +128 -0
  91. package/config/home-links.js +1 -1
  92. package/config/labels-annotations.js +1 -0
  93. package/config/product/explorer.js +17 -4
  94. package/config/product/manager.js +2 -0
  95. package/config/router/index.js +16 -0
  96. package/config/router/navigation-guards/__tests__/authentication.test.ts +130 -0
  97. package/config/router/navigation-guards/authentication.js +10 -4
  98. package/config/router/routes.js +20 -6
  99. package/config/settings.ts +0 -2
  100. package/config/table-headers.js +3 -4
  101. package/config/types.js +9 -0
  102. package/core/plugin-products-base.ts +3 -3
  103. package/core/plugin-types.ts +83 -30
  104. package/core/plugin.ts +3 -0
  105. package/core/types-provisioning.ts +34 -1
  106. package/core/types.ts +15 -2
  107. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +114 -0
  108. package/detail/__tests__/workload.test.ts +3 -152
  109. package/detail/catalog.cattle.io.clusterrepo.vue +1 -1
  110. package/detail/provisioning.cattle.io.cluster.vue +30 -4
  111. package/detail/workload/index.vue +12 -55
  112. package/edit/__tests__/catalog.cattle.io.clusterrepo.test.ts +248 -0
  113. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +105 -0
  114. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  115. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +1 -0
  116. package/edit/auth/__tests__/azuread.test.ts +34 -9
  117. package/edit/auth/__tests__/github.test.ts +234 -0
  118. package/edit/auth/__tests__/oidc.test.ts +26 -6
  119. package/edit/auth/__tests__/saml.test.ts +196 -0
  120. package/edit/auth/azuread.vue +128 -95
  121. package/edit/auth/github.vue +72 -13
  122. package/edit/auth/ldap/__tests__/index.test.ts +206 -0
  123. package/edit/auth/ldap/config.vue +8 -0
  124. package/edit/auth/ldap/index.vue +75 -1
  125. package/edit/auth/oidc.vue +119 -73
  126. package/edit/auth/saml.vue +76 -12
  127. package/edit/catalog.cattle.io.clusterrepo.vue +140 -32
  128. package/edit/fleet.cattle.io.helmop.vue +491 -136
  129. package/edit/management.cattle.io.user.vue +5 -2
  130. package/edit/provisioning.cattle.io.cluster/rke2.vue +84 -10
  131. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  132. package/list/group.principal.vue +5 -4
  133. package/list/harvesterhci.io.management.cluster.vue +8 -9
  134. package/list/management.cattle.io.user.vue +12 -9
  135. package/list/provisioning.cattle.io.cluster.vue +16 -10
  136. package/mixins/__tests__/auth-config.test.ts +90 -0
  137. package/mixins/__tests__/chart.test.ts +94 -0
  138. package/mixins/__tests__/resource-fetch-api-pagination.test.ts +48 -0
  139. package/mixins/auth-config.js +7 -0
  140. package/mixins/chart.js +11 -2
  141. package/mixins/child-hook.js +12 -6
  142. package/mixins/create-edit-view/impl.js +5 -3
  143. package/mixins/resource-fetch-api-pagination.js +21 -1
  144. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +57 -0
  145. package/models/__tests__/compliance.cattle.io.clusterscan.test.ts +144 -0
  146. package/models/__tests__/fleet-application.test.ts +175 -0
  147. package/models/__tests__/fleet.cattle.io.bundle.test.ts +169 -0
  148. package/models/__tests__/fleet.cattle.io.helmop.test.ts +84 -0
  149. package/models/__tests__/management.cattle.io.node.ts +22 -0
  150. package/models/__tests__/namespace.test.ts +36 -0
  151. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +49 -0
  152. package/models/__tests__/workload.test.ts +401 -26
  153. package/models/catalog.cattle.io.clusterrepo.js +28 -4
  154. package/models/compliance.cattle.io.clusterscan.js +39 -4
  155. package/models/fleet-application.js +4 -0
  156. package/models/fleet.cattle.io.helmop.js +20 -1
  157. package/models/management.cattle.io.cluster.js +18 -2
  158. package/models/management.cattle.io.node.js +44 -3
  159. package/models/namespace.js +1 -1
  160. package/models/pod.js +33 -1
  161. package/models/provisioning.cattle.io.cluster.js +5 -5
  162. package/models/workload.js +108 -13
  163. package/models/workload.service.js +5 -0
  164. package/package.json +14 -13
  165. package/pages/about.vue +5 -6
  166. package/pages/auth/login.vue +0 -35
  167. package/pages/auth/setup.vue +11 -0
  168. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +2 -2
  169. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +10 -1
  170. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +93 -0
  171. package/pages/c/_cluster/apps/charts/chart.vue +2 -1
  172. package/pages/c/_cluster/apps/charts/index.vue +48 -10
  173. package/pages/c/_cluster/apps/charts/install.vue +122 -116
  174. package/pages/c/_cluster/auth/roles/index.vue +5 -4
  175. package/pages/c/_cluster/explorer/workload-dashboard/ByNamespaceSection.vue +31 -0
  176. package/pages/c/_cluster/explorer/workload-dashboard/ByStateSection.vue +138 -0
  177. package/pages/c/_cluster/explorer/workload-dashboard/ByTypeSection.vue +30 -0
  178. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadCard.vue +155 -0
  179. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadNamespaceCard.vue +142 -0
  180. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadTypeCard.vue +159 -0
  181. package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +561 -0
  182. package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +440 -0
  183. package/pages/c/_cluster/explorer/workload-dashboard/index.vue +187 -0
  184. package/pages/c/_cluster/explorer/workload-dashboard/types.ts +80 -0
  185. package/pages/c/_cluster/fleet/application/create.vue +187 -136
  186. package/pages/c/_cluster/fleet/application/index.vue +5 -3
  187. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailBody.vue +338 -0
  188. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailHeader.vue +121 -0
  189. package/pages/c/_cluster/fleet/application/suse-app-collection/chart.vue +369 -0
  190. package/pages/c/_cluster/fleet/application/suse-app-collection/charts.vue +248 -0
  191. package/pages/c/_cluster/fleet/application/suse-app-collection/credentials.vue +310 -0
  192. package/pages/c/_cluster/fleet/index.vue +2 -2
  193. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +96 -0
  194. package/pages/c/_cluster/uiplugins/index.vue +15 -0
  195. package/pages/fail-whale.vue +16 -11
  196. package/pages/home.vue +16 -46
  197. package/plugins/clean-html.d.ts +9 -0
  198. package/plugins/dashboard-store/__tests__/resource-class.test.ts +93 -0
  199. package/plugins/dashboard-store/resource-class.js +62 -7
  200. package/plugins/steve/__tests__/actions.test.ts +212 -0
  201. package/plugins/steve/actions.js +96 -0
  202. package/plugins/steve/steve-pagination-utils.ts +1 -1
  203. package/rancher-components/Accordion/Accordion.vue +53 -9
  204. package/rancher-components/Form/Checkbox/Checkbox.vue +14 -0
  205. package/rancher-components/Form/Radio/RadioButton.vue +17 -1
  206. package/rancher-components/Form/Radio/RadioGroup.vue +10 -0
  207. package/rancher-components/Pill/RcTag/RcTag.vue +3 -2
  208. package/rancher-components/RcButton/RcButton.test.ts +103 -0
  209. package/rancher-components/RcButton/RcButton.vue +94 -15
  210. package/rancher-components/RcButton/types.ts +3 -0
  211. package/rancher-components/RcItemCard/RcItemCard.test.ts +18 -0
  212. package/rancher-components/RcItemCard/RcItemCard.vue +2 -2
  213. package/rancher-components/RcSection/RcSection.vue +28 -3
  214. package/scripts/extension/helm/package/Dockerfile +1 -1
  215. package/scripts/test-plugins-build.sh +2 -1
  216. package/store/__tests__/notifications.test.ts +434 -0
  217. package/store/catalog.js +57 -0
  218. package/store/plugins.js +7 -4
  219. package/types/components/buttonGroup.ts +5 -0
  220. package/types/shell/index.d.ts +104 -70
  221. package/utils/__tests__/auth.test.ts +273 -0
  222. package/utils/__tests__/computed.test.ts +193 -0
  223. package/utils/__tests__/cspAdaptor.test.ts +163 -0
  224. package/utils/__tests__/dom.test.ts +81 -0
  225. package/utils/__tests__/duration.test.ts +37 -1
  226. package/utils/__tests__/dynamic-importer.test.ts +102 -0
  227. package/utils/__tests__/fleet-appco.test.ts +312 -0
  228. package/utils/__tests__/monitoring.test.ts +130 -0
  229. package/utils/__tests__/object.test.ts +22 -0
  230. package/utils/__tests__/platform.test.ts +91 -0
  231. package/utils/__tests__/position.test.ts +237 -0
  232. package/utils/__tests__/provider.test.ts +51 -1
  233. package/utils/__tests__/queue.test.ts +232 -0
  234. package/utils/__tests__/release-notes.test.ts +221 -0
  235. package/utils/__tests__/router.test.js +254 -1
  236. package/utils/__tests__/select.test.ts +208 -0
  237. package/utils/__tests__/time.test.ts +265 -1
  238. package/utils/__tests__/title.test.ts +47 -0
  239. package/utils/__tests__/width.test.ts +53 -0
  240. package/utils/__tests__/window.test.ts +158 -0
  241. package/utils/__tests__/xccdf.test.ts +126 -6
  242. package/utils/crypto/__tests__/browserHashUtils.test.ts +98 -0
  243. package/utils/crypto/__tests__/index.test.ts +144 -0
  244. package/utils/duration.ts +104 -0
  245. package/utils/dynamic-content/__tests__/notification-handler.test.ts +196 -0
  246. package/utils/dynamic-content/info.ts +2 -1
  247. package/utils/error.js +13 -0
  248. package/utils/fleet-appco.ts +323 -0
  249. package/utils/object.js +22 -2
  250. package/utils/provider.ts +12 -0
  251. package/utils/validators/__tests__/container-images.test.ts +104 -0
  252. package/utils/validators/__tests__/flow-output.test.ts +91 -0
  253. package/utils/validators/__tests__/logging-outputs.test.ts +58 -0
  254. package/utils/validators/__tests__/monitoring-route.test.ts +119 -0
  255. package/utils/xccdf.ts +39 -42
  256. package/vue.config.js +1 -1
  257. package/pages/support/index.vue +0 -264
  258. package/utils/duration.js +0 -43
@@ -113,6 +113,39 @@ describe('component: InternalExternalIP', () => {
113
113
  });
114
114
  });
115
115
 
116
+ describe('fallback to singular IPs', () => {
117
+ it('should use singular externalIp and internalIp when arrays are not present', () => {
118
+ const wrapper = mountComponent({ row: { externalIp: '1.1.1.1', internalIp: '2.2.2.2' } });
119
+
120
+ expect(wrapper.vm.filteredExternalIps).toStrictEqual(['1.1.1.1']);
121
+ expect(wrapper.vm.filteredInternalIps).toStrictEqual(['2.2.2.2']);
122
+ expect(wrapper.find('[data-testid="external-ip"]').text()).toStrictEqual('1.1.1.1');
123
+ expect(wrapper.find('[data-testid="internal-ip"]').text()).toStrictEqual('2.2.2.2');
124
+ });
125
+
126
+ it('should ignore generic.none value for externalIp', () => {
127
+ // Assuming the mock translation for 'generic.none' returns 'generic.none'
128
+ const wrapper = mountComponent({ row: { externalIp: 'generic.none', internalIp: '2.2.2.2' } });
129
+
130
+ expect(wrapper.vm.filteredExternalIps).toStrictEqual([]);
131
+ expect(wrapper.vm.filteredInternalIps).toStrictEqual(['2.2.2.2']);
132
+ });
133
+
134
+ it('should prioritize array properties over singular strings', () => {
135
+ const wrapper = mountComponent({
136
+ row: {
137
+ externalIps: ['3.3.3.3'],
138
+ externalIp: '1.1.1.1',
139
+ internalIps: ['4.4.4.4'],
140
+ internalIp: '2.2.2.2'
141
+ }
142
+ });
143
+
144
+ expect(wrapper.vm.filteredExternalIps).toStrictEqual(['3.3.3.3']);
145
+ expect(wrapper.vm.filteredInternalIps).toStrictEqual(['4.4.4.4']);
146
+ });
147
+ });
148
+
116
149
  describe('invalid IPs', () => {
117
150
  it('should filter invalid IPs', () => {
118
151
  const wrapper = mountComponent({ row: { externalIps: ['1.1.1.1', 'not-an-ip'], internalIps: ['2.2.2.2'] } });
@@ -122,6 +155,105 @@ describe('component: InternalExternalIP', () => {
122
155
  });
123
156
  });
124
157
 
158
+ describe('isIp validation', () => {
159
+ describe('valid IPv4 addresses', () => {
160
+ it.each([
161
+ ['0.0.0.0'],
162
+ ['1.1.1.1'],
163
+ ['127.0.0.1'],
164
+ ['192.168.1.1'],
165
+ ['10.0.0.1'],
166
+ ['255.255.255.255'],
167
+ ['172.16.0.1'],
168
+ ])('should accept %s as a valid IP', (ip) => {
169
+ const wrapper = mountComponent({ row: { externalIps: [ip], internalIps: [] } });
170
+
171
+ expect(wrapper.vm.filteredExternalIps).toStrictEqual([ip]);
172
+ expect(wrapper.find('[data-testid="external-ip"]').text()).toStrictEqual(ip);
173
+ });
174
+ });
175
+
176
+ describe('valid IPv6 addresses', () => {
177
+ it.each([
178
+ ['::1'],
179
+ ['::'],
180
+ ['fe80::1'],
181
+ ['2001:0db8:85a3:0000:0000:8a2e:0370:7334'],
182
+ ['2001:db8::1'],
183
+ ['::ffff:192.168.1.1'],
184
+ ])('should accept %s as a valid IP', (ip) => {
185
+ const wrapper = mountComponent({ row: { externalIps: [ip], internalIps: [] } });
186
+
187
+ expect(wrapper.vm.filteredExternalIps).toStrictEqual([ip]);
188
+ expect(wrapper.find('[data-testid="external-ip"]').text()).toStrictEqual(ip);
189
+ });
190
+ });
191
+
192
+ describe('invalid values', () => {
193
+ it.each([
194
+ ['not-an-ip'],
195
+ ['abc.def.ghi.jkl'],
196
+ ['999.999.999.999'],
197
+ ['1.2.3'],
198
+ ['1.2.3.4.5'],
199
+ [''],
200
+ ['hello'],
201
+ ['192.168.1'],
202
+ ['192.168.1.1.1'],
203
+ ['1.2.3.4/24'],
204
+ ])('should reject %s as an invalid IP', (ip) => {
205
+ const wrapper = mountComponent({ row: { externalIps: [ip], internalIps: [] } });
206
+
207
+ expect(wrapper.vm.filteredExternalIps).toStrictEqual([]);
208
+ expect(wrapper.find('[data-testid="external-ip"]').exists()).toBe(false);
209
+ });
210
+ });
211
+
212
+ it('should filter invalid IPs from internal IPs list', () => {
213
+ const wrapper = mountComponent({ row: { externalIps: [], internalIps: ['10.0.0.1', 'bad-ip', '192.168.1.1'] } });
214
+
215
+ expect(wrapper.vm.filteredInternalIps).toStrictEqual(['10.0.0.1', '192.168.1.1']);
216
+ expect(wrapper.find('[data-testid="internal-ip"]').text()).toStrictEqual('10.0.0.1');
217
+ expect(wrapper.vm.remainingIpCount).toStrictEqual(1);
218
+ });
219
+
220
+ it('should filter all invalid IPs leaving no results', () => {
221
+ const wrapper = mountComponent({ row: { externalIps: ['not-valid', 'also-bad'], internalIps: ['nope'] } });
222
+
223
+ expect(wrapper.vm.filteredExternalIps).toStrictEqual([]);
224
+ expect(wrapper.vm.filteredInternalIps).toStrictEqual([]);
225
+ expect(wrapper.find('[data-testid="external-ip"]').exists()).toBe(false);
226
+ expect(wrapper.find('[data-testid="internal-ip"]').exists()).toBe(false);
227
+ });
228
+
229
+ it('should handle mixed valid and invalid IPs preserving only valid ones', () => {
230
+ const wrapper = mountComponent({
231
+ row: {
232
+ externalIps: ['1.1.1.1', 'garbage', '8.8.8.8', '999.0.0.1'],
233
+ internalIps: ['10.0.0.1', '', '172.16.0.1', 'abc']
234
+ }
235
+ });
236
+
237
+ expect(wrapper.vm.filteredExternalIps).toStrictEqual(['1.1.1.1', '8.8.8.8']);
238
+ expect(wrapper.vm.filteredInternalIps).toStrictEqual(['10.0.0.1', '172.16.0.1']);
239
+ expect(wrapper.find('[data-testid="external-ip"]').text()).toStrictEqual('1.1.1.1');
240
+ expect(wrapper.find('[data-testid="internal-ip"]').text()).toStrictEqual('10.0.0.1');
241
+ expect(wrapper.vm.remainingIpCount).toStrictEqual(2);
242
+ });
243
+
244
+ it('should handle undefined externalIps gracefully', () => {
245
+ const wrapper = mountComponent({ row: { internalIps: ['1.1.1.1'] } });
246
+
247
+ expect(wrapper.vm.filteredExternalIps).toStrictEqual([]);
248
+ });
249
+
250
+ it('should handle undefined internalIps gracefully', () => {
251
+ const wrapper = mountComponent({ row: { externalIps: ['1.1.1.1'] } });
252
+
253
+ expect(wrapper.vm.filteredInternalIps).toStrictEqual([]);
254
+ });
255
+ });
256
+
125
257
  describe('tooltip', () => {
126
258
  it('should display the correct tooltip text', () => {
127
259
  const wrapper = mountComponent({ row: { externalIps: ['1.1.1.1', '3.3.3.3'], internalIps: ['2.2.2.2', '4.4.4.4'] } });
@@ -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
+ });
@@ -210,6 +210,10 @@ export default {
210
210
  return true;
211
211
  }
212
212
 
213
+ if (this.$route?.meta?.disableWorkspaceSwitcher) {
214
+ return true;
215
+ }
216
+
213
217
  return false;
214
218
  },
215
219
 
@@ -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', () => {