@rancher/shell 3.0.8-rc.8 → 3.0.8

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 (260) hide show
  1. package/apis/impl/apis.ts +61 -0
  2. package/apis/index.ts +40 -0
  3. package/apis/intf/modal.ts +90 -0
  4. package/apis/intf/shell.ts +36 -0
  5. package/apis/intf/slide-in.ts +98 -0
  6. package/apis/intf/system.ts +41 -0
  7. package/apis/shell/__tests__/modal.test.ts +80 -0
  8. package/apis/shell/__tests__/notifications.test.ts +71 -0
  9. package/apis/shell/__tests__/slide-in.test.ts +54 -0
  10. package/apis/shell/__tests__/system.test.ts +129 -0
  11. package/apis/shell/index.ts +38 -0
  12. package/apis/shell/modal.ts +41 -0
  13. package/apis/shell/notifications.ts +65 -0
  14. package/apis/shell/slide-in.ts +33 -0
  15. package/apis/shell/system.ts +65 -0
  16. package/apis/vue-shim.d.ts +11 -0
  17. package/assets/brand/suse/dark/rancher-logo.svg +1 -64
  18. package/assets/styles/global/_tooltip.scss +6 -1
  19. package/assets/translations/en-us.yaml +14 -1
  20. package/components/ActionMenuShell.vue +3 -1
  21. package/components/BackLink.vue +8 -0
  22. package/components/BannerGraphic.vue +1 -5
  23. package/components/BrandImage.vue +17 -6
  24. package/components/Cron/CronExpressionEditor.vue +1 -1
  25. package/components/Cron/CronExpressionEditorModal.vue +1 -1
  26. package/components/CruResource.vue +8 -1
  27. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +1 -0
  28. package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +50 -1
  29. package/components/Drawer/ResourceDetailDrawer/composables.ts +19 -0
  30. package/components/Drawer/ResourceDetailDrawer/index.vue +4 -1
  31. package/components/Drawer/ResourceDetailDrawer/types.ts +2 -1
  32. package/components/LocaleSelector.vue +2 -2
  33. package/components/ModalManager.vue +11 -1
  34. package/components/Questions/__tests__/Yaml.test.ts +1 -1
  35. package/components/Questions/__tests__/index.test.ts +159 -0
  36. package/components/RelatedResources.vue +5 -0
  37. package/components/Resource/Detail/Metadata/Annotations/index.vue +2 -2
  38. package/components/Resource/Detail/Metadata/Labels/index.vue +2 -2
  39. package/components/Resource/Detail/Metadata/index.vue +3 -3
  40. package/components/Resource/Detail/ResourcePopover/index.vue +5 -1
  41. package/components/Resource/Detail/composables.ts +2 -2
  42. package/components/ResourceDetail/Masthead/latest.vue +23 -21
  43. package/components/ResourceDetail/index.vue +3 -0
  44. package/components/ResourceTable.vue +54 -21
  45. package/components/SlideInPanelManager.vue +16 -11
  46. package/components/SortableTable/THead.vue +2 -1
  47. package/components/SortableTable/index.vue +20 -2
  48. package/components/Tabbed/__tests__/index.test.ts +86 -0
  49. package/components/Tabbed/index.vue +37 -2
  50. package/components/__tests__/NamespaceFilter.test.ts +49 -0
  51. package/components/auth/SelectPrincipal.vue +28 -6
  52. package/components/auth/__tests__/SelectPrincipal.test.ts +119 -0
  53. package/components/auth/login/ldap.vue +3 -3
  54. package/components/fleet/FleetSecretSelector.vue +1 -1
  55. package/components/form/KeyValue.vue +1 -1
  56. package/components/form/NameNsDescription.vue +1 -1
  57. package/components/form/NodeScheduling.vue +2 -2
  58. package/components/form/ResourceTabs/composable.ts +2 -2
  59. package/components/form/ResourceTabs/index.vue +0 -2
  60. package/components/form/__tests__/NameNsDescription.test.ts +42 -0
  61. package/components/formatter/InternalExternalIP.vue +4 -1
  62. package/components/formatter/LinkName.vue +5 -0
  63. package/components/formatter/__tests__/InternalExternalIP.test.ts +1 -1
  64. package/components/nav/Group.vue +25 -7
  65. package/components/nav/Header.vue +1 -1
  66. package/components/nav/NamespaceFilter.vue +1 -0
  67. package/components/nav/Type.vue +17 -6
  68. package/components/nav/WindowManager/panels/TabBodyContainer.vue +1 -1
  69. package/components/nav/__tests__/Type.test.ts +59 -0
  70. package/components/templates/standalone.vue +1 -1
  71. package/composables/cruResource.ts +27 -0
  72. package/composables/focusTrap.ts +3 -1
  73. package/composables/resourceDetail.ts +15 -0
  74. package/composables/useI18n.ts +10 -1
  75. package/composables/useLabeledFormElement.ts +3 -4
  76. package/config/__test__/uiplugins.test.ts +309 -0
  77. package/config/labels-annotations.js +1 -0
  78. package/config/product/explorer.js +3 -1
  79. package/config/product/fleet.js +1 -1
  80. package/config/router/navigation-guards/clusters.js +3 -3
  81. package/config/router/navigation-guards/products.js +1 -1
  82. package/config/router/routes.js +7 -7
  83. package/config/types.js +7 -0
  84. package/config/uiplugins.js +46 -2
  85. package/core/__tests__/extension-manager-impl.test.js +437 -0
  86. package/core/extension-manager-impl.js +21 -25
  87. package/core/plugin-helpers.ts +2 -2
  88. package/core/plugin.ts +9 -1
  89. package/core/plugins-loader.js +2 -2
  90. package/core/types-provisioning.ts +5 -1
  91. package/core/types.ts +35 -0
  92. package/detail/provisioning.cattle.io.cluster.vue +9 -6
  93. package/dialog/DeveloperLoadExtensionDialog.vue +13 -4
  94. package/dialog/MoveNamespaceDialog.vue +20 -4
  95. package/dialog/RollbackWorkloadDialog.vue +2 -5
  96. package/dialog/SearchDialog.vue +1 -0
  97. package/dialog/__tests__/MoveNamespaceDialog.test.ts +249 -0
  98. package/directives/__tests__/clean-tooltip.test.ts +298 -0
  99. package/directives/clean-tooltip.ts +234 -0
  100. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -2
  101. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +100 -3
  102. package/edit/autoscaling.horizontalpodautoscaler/index.vue +1 -0
  103. package/edit/configmap.vue +1 -0
  104. package/edit/constraints.gatekeeper.sh.constraint/index.vue +1 -0
  105. package/edit/fleet.cattle.io.helmop.vue +11 -6
  106. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  107. package/edit/k8s.cni.cncf.io.networkattachmentdefinition.vue +1 -0
  108. package/edit/logging-flow/index.vue +1 -0
  109. package/edit/logging.banzaicloud.io.output/index.vue +1 -0
  110. package/edit/management.cattle.io.fleetworkspace.vue +1 -1
  111. package/edit/management.cattle.io.project.vue +1 -0
  112. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +4 -1
  113. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +2 -1
  114. package/edit/monitoring.coreos.com.prometheusrule/index.vue +1 -0
  115. package/edit/monitoring.coreos.com.receiver/index.vue +2 -1
  116. package/edit/monitoring.coreos.com.route.vue +1 -1
  117. package/edit/namespace.vue +1 -0
  118. package/edit/networking.istio.io.destinationrule/index.vue +1 -0
  119. package/edit/networking.k8s.io.ingress/index.vue +1 -0
  120. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -0
  121. package/edit/networking.k8s.io.networkpolicy/index.vue +1 -0
  122. package/edit/node.vue +1 -0
  123. package/edit/persistentvolume/index.vue +27 -22
  124. package/edit/persistentvolume/plugins/awsElasticBlockStore.vue +13 -14
  125. package/edit/persistentvolume/plugins/azureDisk.vue +49 -48
  126. package/edit/persistentvolume/plugins/azureFile.vue +15 -14
  127. package/edit/persistentvolume/plugins/cephfs.vue +15 -14
  128. package/edit/persistentvolume/plugins/cinder.vue +15 -14
  129. package/edit/persistentvolume/plugins/csi.vue +18 -16
  130. package/edit/persistentvolume/plugins/fc.vue +13 -14
  131. package/edit/persistentvolume/plugins/flexVolume.vue +15 -14
  132. package/edit/persistentvolume/plugins/flocker.vue +1 -3
  133. package/edit/persistentvolume/plugins/gcePersistentDisk.vue +13 -14
  134. package/edit/persistentvolume/plugins/glusterfs.vue +15 -14
  135. package/edit/persistentvolume/plugins/hostPath.vue +40 -39
  136. package/edit/persistentvolume/plugins/iscsi.vue +13 -14
  137. package/edit/persistentvolume/plugins/local.vue +1 -3
  138. package/edit/persistentvolume/plugins/longhorn.vue +23 -22
  139. package/edit/persistentvolume/plugins/nfs.vue +15 -14
  140. package/edit/persistentvolume/plugins/photonPersistentDisk.vue +1 -14
  141. package/edit/persistentvolume/plugins/portworxVolume.vue +15 -14
  142. package/edit/persistentvolume/plugins/quobyte.vue +15 -14
  143. package/edit/persistentvolume/plugins/rbd.vue +15 -14
  144. package/edit/persistentvolume/plugins/scaleIO.vue +15 -14
  145. package/edit/persistentvolume/plugins/storageos.vue +15 -14
  146. package/edit/persistentvolume/plugins/vsphereVolume.vue +1 -3
  147. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +21 -21
  148. package/edit/provisioning.cattle.io.cluster/index.vue +5 -5
  149. package/edit/provisioning.cattle.io.cluster/rke2.vue +9 -8
  150. package/edit/resources.cattle.io.restore.vue +1 -1
  151. package/edit/secret/index.vue +1 -1
  152. package/edit/service.vue +1 -0
  153. package/edit/serviceaccount.vue +1 -0
  154. package/edit/storage.k8s.io.storageclass/index.vue +1 -0
  155. package/edit/workload/Job.vue +2 -2
  156. package/edit/workload/index.vue +2 -1
  157. package/edit/workload/mixins/workload.js +1 -1
  158. package/initialize/App.vue +4 -4
  159. package/initialize/install-plugins.js +19 -5
  160. package/machine-config/azure.vue +1 -1
  161. package/machine-config/components/GCEImage.vue +1 -1
  162. package/mixins/__tests__/brand.spec.ts +2 -2
  163. package/mixins/brand.js +1 -7
  164. package/mixins/create-edit-view/index.js +5 -0
  165. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +128 -5
  166. package/models/chart.js +70 -74
  167. package/models/management.cattle.io.cluster.js +21 -3
  168. package/models/provisioning.cattle.io.cluster.js +31 -11
  169. package/package.json +11 -10
  170. package/pages/auth/login.vue +4 -6
  171. package/pages/auth/setup.vue +1 -1
  172. package/pages/auth/verify.vue +3 -3
  173. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +135 -0
  174. package/pages/c/_cluster/apps/charts/chart.vue +33 -15
  175. package/pages/c/_cluster/apps/charts/index.vue +122 -24
  176. package/pages/c/_cluster/apps/charts/install.vue +33 -0
  177. package/pages/c/_cluster/explorer/__tests__/index.test.ts +1 -1
  178. package/pages/c/_cluster/explorer/index.vue +8 -6
  179. package/pages/c/_cluster/fleet/index.vue +4 -7
  180. package/pages/c/_cluster/manager/hostedprovider/index.vue +12 -6
  181. package/pages/c/_cluster/settings/brand.vue +1 -1
  182. package/pages/c/_cluster/settings/index.vue +5 -0
  183. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +7 -0
  184. package/pages/c/_cluster/uiplugins/catalogs.vue +147 -0
  185. package/pages/c/_cluster/uiplugins/index.vue +126 -184
  186. package/pkg/auto-import.js +3 -3
  187. package/pkg/dynamic-importer.lib.js +1 -1
  188. package/pkg/import.js +1 -1
  189. package/plugins/__tests__/mutations.tests.ts +179 -0
  190. package/plugins/dashboard-client-init.js +3 -0
  191. package/plugins/dashboard-store/getters.js +19 -2
  192. package/plugins/dashboard-store/model-loader.js +1 -1
  193. package/plugins/dashboard-store/mutations.js +23 -2
  194. package/plugins/dashboard-store/resource-class.js +11 -5
  195. package/plugins/i18n.js +8 -0
  196. package/plugins/plugin.js +2 -2
  197. package/plugins/steve/__tests__/steve-pagination-utils.test.ts +506 -0
  198. package/plugins/steve/steve-class.js +1 -1
  199. package/plugins/steve/steve-pagination-utils.ts +131 -47
  200. package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
  201. package/rancher-components/Form/LabeledInput/LabeledInput.vue +1 -1
  202. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +6 -42
  203. package/rancher-components/Pill/RcStatusBadge/index.ts +0 -1
  204. package/rancher-components/Pill/RcStatusBadge/types.ts +1 -1
  205. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +5 -28
  206. package/rancher-components/Pill/RcStatusIndicator/types.ts +2 -1
  207. package/rancher-components/Pill/types.ts +0 -1
  208. package/rancher-components/RcDropdown/useDropdownContext.ts +2 -4
  209. package/rancher-components/RcIcon/RcIcon.test.ts +51 -0
  210. package/rancher-components/RcIcon/RcIcon.vue +46 -0
  211. package/rancher-components/RcIcon/index.ts +1 -0
  212. package/rancher-components/RcIcon/types.ts +160 -0
  213. package/rancher-components/RcItemCard/RcItemCard.vue +1 -1
  214. package/rancher-components/utils/status.test.ts +67 -0
  215. package/rancher-components/utils/status.ts +77 -0
  216. package/scripts/publish-shell.sh +25 -0
  217. package/scripts/typegen.sh +1 -0
  218. package/store/__tests__/catalog.test.ts +1 -1
  219. package/store/__tests__/type-map.test.ts +164 -2
  220. package/store/action-menu.js +8 -0
  221. package/store/auth.js +25 -13
  222. package/store/catalog.js +6 -0
  223. package/store/i18n.js +3 -3
  224. package/store/index.js +8 -6
  225. package/store/notifications.ts +2 -0
  226. package/store/prefs.js +6 -7
  227. package/store/type-map.js +17 -7
  228. package/store/wm.ts +4 -4
  229. package/types/internal-api/shell/modal.d.ts +6 -6
  230. package/types/notifications/index.ts +126 -15
  231. package/types/rancher/index.d.ts +9 -0
  232. package/types/shell/index.d.ts +54 -3
  233. package/types/store/__tests__/pagination.types.spec.ts +137 -0
  234. package/types/store/pagination.types.ts +157 -9
  235. package/types/vue-shim.d.ts +5 -4
  236. package/utils/__tests__/provider.test.ts +98 -0
  237. package/utils/__tests__/router.test.js +238 -0
  238. package/utils/__tests__/selector-typed.test.ts +263 -0
  239. package/utils/cluster.js +4 -1
  240. package/utils/color.js +1 -1
  241. package/utils/dynamic-content/__tests__/info.test.ts +6 -0
  242. package/utils/dynamic-content/info.ts +43 -0
  243. package/utils/favicon.js +4 -4
  244. package/utils/fleet.ts +8 -1
  245. package/utils/pagination-utils.ts +2 -2
  246. package/utils/pagination-wrapper.ts +1 -1
  247. package/utils/provider.ts +14 -0
  248. package/utils/router.js +50 -0
  249. package/utils/selector-typed.ts +6 -2
  250. package/utils/unit-tests/pagination-utils.spec.ts +8 -8
  251. package/vue.config.js +3 -3
  252. package/composables/useExtensionManager.ts +0 -17
  253. package/core/plugins.js +0 -38
  254. package/directives/clean-tooltip.js +0 -32
  255. package/plugins/internal-api/index.ts +0 -37
  256. package/plugins/internal-api/shared/base-api.ts +0 -13
  257. package/plugins/internal-api/shell/shell.api.ts +0 -108
  258. package/plugins/nuxt-client-init.js +0 -3
  259. package/types/internal-api/shell/growl.d.ts +0 -25
  260. package/types/internal-api/shell/slideIn.d.ts +0 -15
@@ -0,0 +1,238 @@
1
+ import { findRouteDefinitionByName, filterLocationValidParams } from '@shell/utils/router';
2
+
3
+ describe('findRouteDefinitionByName', () => {
4
+ const createMockRouter = (routes) => ({ getRoutes: () => routes });
5
+
6
+ it('should find a route by its name', () => {
7
+ const routes = [
8
+ { name: 'home', path: '/' },
9
+ { name: 'about', path: '/about' },
10
+ { name: 'c-cluster', path: '/c/:cluster' },
11
+ ];
12
+ const router = createMockRouter(routes);
13
+
14
+ const result = findRouteDefinitionByName(router, 'about');
15
+
16
+ expect(result).toStrictEqual({ name: 'about', path: '/about' });
17
+ });
18
+
19
+ it('should return the first matching route when name exists', () => {
20
+ const routes = [
21
+ { name: 'c-cluster-explorer', path: '/c/:cluster/explorer' },
22
+ { name: 'c-cluster-apps', path: '/c/:cluster/apps' },
23
+ ];
24
+ const router = createMockRouter(routes);
25
+
26
+ const result = findRouteDefinitionByName(router, 'c-cluster-explorer');
27
+
28
+ expect(result).toStrictEqual({ name: 'c-cluster-explorer', path: '/c/:cluster/explorer' });
29
+ });
30
+
31
+ it('should return undefined when route name is not found', () => {
32
+ const routes = [
33
+ { name: 'home', path: '/' },
34
+ { name: 'about', path: '/about' },
35
+ ];
36
+ const router = createMockRouter(routes);
37
+
38
+ const result = findRouteDefinitionByName(router, 'nonexistent');
39
+
40
+ expect(result).toBeUndefined();
41
+ });
42
+
43
+ it('should return undefined when routes array is empty', () => {
44
+ const router = createMockRouter([]);
45
+
46
+ const result = findRouteDefinitionByName(router, 'any-route');
47
+
48
+ expect(result).toBeUndefined();
49
+ });
50
+
51
+ it('should handle routes with additional properties', () => {
52
+ const routes = [
53
+ {
54
+ name: 'c-cluster-product-resource',
55
+ path: '/c/:cluster/:product/:resource',
56
+ meta: { requiresAuthentication: true },
57
+ props: true,
58
+ },
59
+ ];
60
+ const router = createMockRouter(routes);
61
+
62
+ const result = findRouteDefinitionByName(router, 'c-cluster-product-resource');
63
+
64
+ expect(result).toStrictEqual(routes[0]);
65
+ });
66
+
67
+ it('should match exact route names only', () => {
68
+ const routes = [
69
+ { name: 'c-cluster', path: '/c/:cluster' },
70
+ { name: 'c-cluster-explorer', path: '/c/:cluster/explorer' },
71
+ ];
72
+ const router = createMockRouter(routes);
73
+
74
+ const result = findRouteDefinitionByName(router, 'c-cluster');
75
+
76
+ expect(result).toStrictEqual({ name: 'c-cluster', path: '/c/:cluster' });
77
+ expect(result.name).not.toBe('c-cluster-explorer');
78
+ });
79
+ });
80
+
81
+ describe('filterLocationValidParams', () => {
82
+ const createMockRouter = (routes) => ({ getRoutes: () => routes });
83
+
84
+ it('should filter out params not in route path', () => {
85
+ const routes = [
86
+ { name: 'c-cluster', path: '/c/:cluster' },
87
+ ];
88
+ const router = createMockRouter(routes);
89
+ const routeRecord = {
90
+ name: 'c-cluster',
91
+ params: {
92
+ cluster: 'local',
93
+ product: 'explorer',
94
+ },
95
+ };
96
+
97
+ const result = filterLocationValidParams(router, routeRecord);
98
+
99
+ expect(result.params).toStrictEqual({ cluster: 'local' });
100
+ expect(result.params.product).toBeUndefined();
101
+ });
102
+
103
+ it('should keep all params when all are valid', () => {
104
+ const routes = [
105
+ { name: 'c-cluster-product-resource', path: '/c/:cluster/:product/:resource' },
106
+ ];
107
+ const router = createMockRouter(routes);
108
+ const routeRecord = {
109
+ name: 'c-cluster-product-resource',
110
+ params: {
111
+ cluster: 'local',
112
+ product: 'explorer',
113
+ resource: 'pods',
114
+ },
115
+ };
116
+
117
+ const result = filterLocationValidParams(router, routeRecord);
118
+
119
+ expect(result.params).toStrictEqual({
120
+ cluster: 'local',
121
+ product: 'explorer',
122
+ resource: 'pods',
123
+ });
124
+ });
125
+
126
+ it('should preserve other properties on routeRecord', () => {
127
+ const routes = [
128
+ { name: 'c-cluster', path: '/c/:cluster' },
129
+ ];
130
+ const router = createMockRouter(routes);
131
+ const routeRecord = {
132
+ name: 'c-cluster',
133
+ params: { cluster: 'local' },
134
+ query: { mode: 'edit' },
135
+ hash: '#section',
136
+ };
137
+
138
+ const result = filterLocationValidParams(router, routeRecord);
139
+
140
+ expect(result.query).toStrictEqual({ mode: 'edit' });
141
+ expect(result.hash).toBe('#section');
142
+ expect(result.name).toBe('c-cluster');
143
+ });
144
+
145
+ it('should return routeRecord unchanged when routeRecord is null', () => {
146
+ const router = createMockRouter([]);
147
+
148
+ const result = filterLocationValidParams(router, null);
149
+
150
+ expect(result).toBeNull();
151
+ });
152
+
153
+ it('should return routeRecord unchanged when routeRecord is undefined', () => {
154
+ const router = createMockRouter([]);
155
+
156
+ const result = filterLocationValidParams(router, undefined);
157
+
158
+ expect(result).toBeUndefined();
159
+ });
160
+
161
+ it('should return routeRecord unchanged when name is missing', () => {
162
+ const router = createMockRouter([]);
163
+ const routeRecord = { params: { cluster: 'local' } };
164
+
165
+ const result = filterLocationValidParams(router, routeRecord);
166
+
167
+ expect(result).toStrictEqual(routeRecord);
168
+ });
169
+
170
+ it('should return routeRecord unchanged when params is missing', () => {
171
+ const router = createMockRouter([]);
172
+ const routeRecord = { name: 'c-cluster' };
173
+
174
+ const result = filterLocationValidParams(router, routeRecord);
175
+
176
+ expect(result).toStrictEqual(routeRecord);
177
+ });
178
+
179
+ it('should return routeRecord unchanged when route definition is not found', () => {
180
+ const routes = [
181
+ { name: 'home', path: '/' },
182
+ ];
183
+ const router = createMockRouter(routes);
184
+ const routeRecord = {
185
+ name: 'nonexistent-route',
186
+ params: { cluster: 'local' },
187
+ };
188
+
189
+ const result = filterLocationValidParams(router, routeRecord);
190
+
191
+ expect(result).toStrictEqual(routeRecord);
192
+ });
193
+
194
+ it('should return empty params when no params are valid', () => {
195
+ const routes = [
196
+ { name: 'home', path: '/' },
197
+ ];
198
+ const router = createMockRouter(routes);
199
+ const routeRecord = {
200
+ name: 'home',
201
+ params: {
202
+ cluster: 'local',
203
+ product: 'explorer',
204
+ },
205
+ };
206
+
207
+ const result = filterLocationValidParams(router, routeRecord);
208
+
209
+ expect(result.params).toStrictEqual({});
210
+ });
211
+
212
+ it('should handle optional params in path', () => {
213
+ const routes = [
214
+ { name: 'c-cluster-product-resource-id', path: '/c/:cluster/:product/:resource/:id?' },
215
+ ];
216
+ const router = createMockRouter(routes);
217
+ const routeRecord = {
218
+ name: 'c-cluster-product-resource-id',
219
+ params: {
220
+ cluster: 'local',
221
+ product: 'explorer',
222
+ resource: 'pods',
223
+ id: 'my-pod',
224
+ extra: 'should-be-removed',
225
+ },
226
+ };
227
+
228
+ const result = filterLocationValidParams(router, routeRecord);
229
+
230
+ expect(result.params).toStrictEqual({
231
+ cluster: 'local',
232
+ product: 'explorer',
233
+ resource: 'pods',
234
+ id: 'my-pod',
235
+ });
236
+ expect(result.params.extra).toBeUndefined();
237
+ });
238
+ });
@@ -0,0 +1,263 @@
1
+ import { labelSelectorToSelector } from '@shell/utils/selector-typed';
2
+ import { KubeLabelSelector } from '@shell/types/kube/kube-api';
3
+
4
+ describe('selector-typed', () => {
5
+ describe('labelSelectorToSelector', () => {
6
+ describe('empty label selectors', () => {
7
+ it('should return empty string for undefined label selector', () => {
8
+ const result = labelSelectorToSelector(undefined);
9
+
10
+ expect(result).toBe('');
11
+ });
12
+
13
+ it('should return empty string for label selector with no matchLabels and no matchExpressions', () => {
14
+ const labelSelector: KubeLabelSelector = {};
15
+ const result = labelSelectorToSelector(labelSelector);
16
+
17
+ expect(result).toBe('');
18
+ });
19
+
20
+ it('should return empty string for label selector with empty matchLabels', () => {
21
+ const labelSelector: KubeLabelSelector = { matchLabels: {} };
22
+ const result = labelSelectorToSelector(labelSelector);
23
+
24
+ expect(result).toBe('');
25
+ });
26
+
27
+ it('should return empty string for label selector with empty matchExpressions', () => {
28
+ const labelSelector: KubeLabelSelector = { matchExpressions: [] };
29
+ const result = labelSelectorToSelector(labelSelector);
30
+
31
+ expect(result).toBe('');
32
+ });
33
+
34
+ it('should return empty string for label selector with both empty matchLabels and matchExpressions', () => {
35
+ const labelSelector: KubeLabelSelector = {
36
+ matchLabels: {},
37
+ matchExpressions: []
38
+ };
39
+ const result = labelSelectorToSelector(labelSelector);
40
+
41
+ expect(result).toBe('');
42
+ });
43
+ });
44
+
45
+ describe('matchLabels conversion', () => {
46
+ it('should convert single matchLabel to selector string', () => {
47
+ const labelSelector: KubeLabelSelector = { matchLabels: { app: 'nginx' } };
48
+ const result = labelSelectorToSelector(labelSelector);
49
+
50
+ expect(result).toBe('app=nginx');
51
+ });
52
+
53
+ it('should convert multiple matchLabels to comma-separated selector string', () => {
54
+ const labelSelector: KubeLabelSelector = {
55
+ matchLabels: {
56
+ app: 'nginx',
57
+ version: 'v1.0',
58
+ env: 'production'
59
+ }
60
+ };
61
+ const result = labelSelectorToSelector(labelSelector);
62
+
63
+ expect(result).toBe('app=nginx,version=v1.0,env=production');
64
+ });
65
+
66
+ it('should handle matchLabels with special characters', () => {
67
+ const labelSelector: KubeLabelSelector = { matchLabels: { 'app.kubernetes.io/name': 'my-app' } };
68
+ const result = labelSelectorToSelector(labelSelector);
69
+
70
+ expect(result).toBe('app.kubernetes.io/name=my-app');
71
+ });
72
+
73
+ it('should handle matchLabels with numeric values', () => {
74
+ const labelSelector: KubeLabelSelector = { matchLabels: { tier: '3' } };
75
+ const result = labelSelectorToSelector(labelSelector);
76
+
77
+ expect(result).toBe('tier=3');
78
+ });
79
+ });
80
+
81
+ describe('matchExpressions conversion with In operator', () => {
82
+ it('should convert matchExpression with In operator and single value to equality selector', () => {
83
+ const labelSelector: KubeLabelSelector = {
84
+ matchExpressions: [
85
+ {
86
+ key: 'app',
87
+ operator: 'In',
88
+ values: ['nginx']
89
+ }
90
+ ]
91
+ };
92
+ const result = labelSelectorToSelector(labelSelector);
93
+
94
+ expect(result).toBe('app=nginx');
95
+ });
96
+
97
+ it('should convert matchExpression with In operator and multiple values to in() selector', () => {
98
+ const labelSelector: KubeLabelSelector = {
99
+ matchExpressions: [
100
+ {
101
+ key: 'env',
102
+ operator: 'In',
103
+ values: ['dev', 'staging', 'prod']
104
+ }
105
+ ]
106
+ };
107
+ const result = labelSelectorToSelector(labelSelector);
108
+
109
+ expect(result).toBe('env in (dev,staging,prod)');
110
+ });
111
+
112
+ it('should convert multiple matchExpressions with In operator', () => {
113
+ const labelSelector: KubeLabelSelector = {
114
+ matchExpressions: [
115
+ {
116
+ key: 'app',
117
+ operator: 'In',
118
+ values: ['nginx']
119
+ },
120
+ {
121
+ key: 'env',
122
+ operator: 'In',
123
+ values: ['dev', 'staging']
124
+ }
125
+ ]
126
+ };
127
+ const result = labelSelectorToSelector(labelSelector);
128
+
129
+ expect(result).toBe('app=nginx,env in (dev,staging)');
130
+ });
131
+
132
+ it('should handle matchExpression with empty values array for In operator', () => {
133
+ const labelSelector: KubeLabelSelector = {
134
+ matchExpressions: [
135
+ {
136
+ key: 'app',
137
+ operator: 'In',
138
+ values: []
139
+ }
140
+ ]
141
+ };
142
+ const result = labelSelectorToSelector(labelSelector);
143
+
144
+ // With empty values array, it should create an in() with no values
145
+ expect(result).toBe('app in ()');
146
+ });
147
+ });
148
+
149
+ describe('combined matchLabels and matchExpressions', () => {
150
+ it('should combine matchLabels and matchExpressions with single values', () => {
151
+ const labelSelector: KubeLabelSelector = {
152
+ matchLabels: { tier: 'frontend' },
153
+ matchExpressions: [
154
+ {
155
+ key: 'env',
156
+ operator: 'In',
157
+ values: ['prod']
158
+ }
159
+ ]
160
+ };
161
+ const result = labelSelectorToSelector(labelSelector);
162
+
163
+ expect(result).toBe('tier=frontend,env=prod');
164
+ });
165
+
166
+ it('should combine multiple matchLabels and matchExpressions', () => {
167
+ const labelSelector: KubeLabelSelector = {
168
+ matchLabels: {
169
+ tier: 'frontend',
170
+ version: 'v2'
171
+ },
172
+ matchExpressions: [
173
+ {
174
+ key: 'env',
175
+ operator: 'In',
176
+ values: ['dev', 'staging']
177
+ },
178
+ {
179
+ key: 'region',
180
+ operator: 'In',
181
+ values: ['us-west-1']
182
+ }
183
+ ]
184
+ };
185
+ const result = labelSelectorToSelector(labelSelector);
186
+
187
+ expect(result).toBe('tier=frontend,version=v2,env in (dev,staging),region=us-west-1');
188
+ });
189
+
190
+ it('should combine matchLabels with multiple matchExpressions using in() notation', () => {
191
+ const labelSelector: KubeLabelSelector = {
192
+ matchLabels: { 'app.kubernetes.io/name': 'myapp' },
193
+ matchExpressions: [
194
+ {
195
+ key: 'env',
196
+ operator: 'In',
197
+ values: ['dev', 'test', 'prod']
198
+ }
199
+ ]
200
+ };
201
+ const result = labelSelectorToSelector(labelSelector);
202
+
203
+ expect(result).toBe('app.kubernetes.io/name=myapp,env in (dev,test,prod)');
204
+ });
205
+ });
206
+
207
+ describe('unsupported operators', () => {
208
+ it('should throw error for NotIn operator', () => {
209
+ const labelSelector: KubeLabelSelector = {
210
+ matchExpressions: [
211
+ {
212
+ key: 'env',
213
+ operator: 'NotIn',
214
+ values: ['prod']
215
+ }
216
+ ]
217
+ };
218
+
219
+ expect(() => labelSelectorToSelector(labelSelector)).toThrow('Unsupported matchExpression found when converting to selector string.');
220
+ });
221
+ });
222
+
223
+ describe('edge cases', () => {
224
+ it('should handle matchExpression with In operator but undefined values', () => {
225
+ const labelSelector: KubeLabelSelector = {
226
+ matchExpressions: [
227
+ {
228
+ key: 'app',
229
+ operator: 'In',
230
+ values: undefined
231
+ }
232
+ ]
233
+ };
234
+
235
+ // When values is undefined, the function throws an error
236
+ expect(() => labelSelectorToSelector(labelSelector)).toThrow('Unsupported matchExpression found when converting to selector string.');
237
+ });
238
+
239
+ it('should preserve order of matchLabels and matchExpressions', () => {
240
+ const labelSelector: KubeLabelSelector = {
241
+ matchLabels: {
242
+ first: 'value1',
243
+ second: 'value2'
244
+ },
245
+ matchExpressions: [
246
+ {
247
+ key: 'third',
248
+ operator: 'In',
249
+ values: ['value3']
250
+ }
251
+ ]
252
+ };
253
+ const result = labelSelectorToSelector(labelSelector);
254
+
255
+ // matchLabels come before matchExpressions
256
+ expect(result).toContain('first=value1');
257
+ expect(result).toContain('second=value2');
258
+ expect(result).toContain('third=value3');
259
+ expect(result.indexOf('first')).toBeLessThan(result.indexOf('third'));
260
+ });
261
+ });
262
+ });
263
+ });
package/utils/cluster.js CHANGED
@@ -242,7 +242,7 @@ export function filterOutDeprecatedPatchVersions(allVersions, currentVersion) {
242
242
  return filteredVersions;
243
243
  }
244
244
 
245
- export function getAllOptionsAfterCurrentVersion(store, versions, currentVersion, defaultVersion) {
245
+ export function getAllOptionsAfterCurrentVersion(store, versions, currentVersion, defaultVersion, manual = false) {
246
246
  const out = (versions || []).filter((obj) => !!obj.serverArgs).map((obj) => {
247
247
  let disabled = false;
248
248
  let experimental = false;
@@ -260,6 +260,9 @@ export function getAllOptionsAfterCurrentVersion(store, versions, currentVersion
260
260
 
261
261
  if (isCurrentVersion) {
262
262
  label = `${ label } ${ store.getters['i18n/t']('cluster.kubernetesVersion.current') }`;
263
+ if (manual) {
264
+ label = `${ label } ${ store.getters['i18n/t']('cluster.kubernetesVersion.manual') }`;
265
+ }
263
266
  }
264
267
 
265
268
  if (experimental) {
package/utils/color.js CHANGED
@@ -13,7 +13,7 @@ Primary color classes from _light.scss
13
13
 
14
14
  */
15
15
 
16
- const Color = require('color');
16
+ import Color from 'color';
17
17
 
18
18
  export function createCssVars(color, theme = 'light', name = 'primary') {
19
19
  const contrastOpts = theme === 'light' ? LIGHT_CONTRAST_COLORS : DARK_CONTRAST_COLORS;
@@ -97,6 +97,7 @@ describe('systemInfoProvider', () => {
97
97
  }),
98
98
  'management/schemaFor': jest.fn(),
99
99
  localCluster: mockClusters.find((c) => c.id === 'local') || null,
100
+ 'features/get': jest.fn(() => 'abc'),
100
101
  };
101
102
 
102
103
  (version.getVersionData as jest.Mock).mockReturnValue({
@@ -129,6 +130,7 @@ describe('systemInfoProvider', () => {
129
130
  expect(qs).toContain('bl=en-US');
130
131
  expect(qs).toContain('bs=1024x768');
131
132
  expect(qs).toContain('ss=1920x1080');
133
+ expect(qs).toContain('ff-usc=abc');
132
134
  });
133
135
 
134
136
  it('should handle missing or partial data gracefully', () => {
@@ -161,6 +163,9 @@ describe('systemInfoProvider', () => {
161
163
  mockGetters['uiplugins/plugins'] = null; // No plugins
162
164
  mockGetters['auth/principalId'] = null; // No user
163
165
  mockGetters['localCluster'] = null; // No clusters
166
+ mockGetters['features/get'] = () => {
167
+ throw new Error('unknown feature');
168
+ };
164
169
 
165
170
  const infoProvider = new SystemInfoProvider(mockGetters, {});
166
171
  const qs = infoProvider.buildQueryString();
@@ -177,6 +182,7 @@ describe('systemInfoProvider', () => {
177
182
  expect(qs).not.toContain('lnc=');
178
183
  expect(qs).not.toContain('xkn=');
179
184
  expect(qs).not.toContain('xcc=');
185
+ expect(qs).not.toContain('ff-usc=');
180
186
  });
181
187
 
182
188
  it('should handle getAll returning undefined when types are not registered', () => {
@@ -10,6 +10,7 @@ import {
10
10
  import { SETTING } from '@shell/config/settings';
11
11
  import { getVersionData } from '@shell/config/version';
12
12
  import { SettingsInfo } from '@shell/utils/dynamic-content/types';
13
+ import { STEVE_CACHE } from '@shell/store/features';
13
14
 
14
15
  const QS_VERSION = 'v1'; // Include a version number in the query string in case we want to version the set of params we are sending
15
16
  const UNKNOWN = 'unknown';
@@ -26,6 +27,29 @@ const SUSE_EXTENSIONS = [
26
27
  'virtual-clusters'
27
28
  ];
28
29
 
30
+ type FeatureFlagInfos = {
31
+ [id: string]: {
32
+ /**
33
+ * Query param, in format `ff-<param>`
34
+ */
35
+ param: string,
36
+ /**
37
+ * The actual value used by the UI, roughly spec.value || status.default
38
+ */
39
+ value: string,
40
+ }
41
+ };
42
+
43
+ /**
44
+ * Explicit ff's to send
45
+ */
46
+ const ffs: FeatureFlagInfos = {
47
+ [STEVE_CACHE]: {
48
+ param: 'usc',
49
+ value: '',
50
+ }
51
+ };
52
+
29
53
  /**
30
54
  * System information that is collected and which can then be encoded into a query string in the dyanmic content request
31
55
  */
@@ -47,6 +71,7 @@ type SystemInfo = {
47
71
  browserSize: string;
48
72
  screenSize: string;
49
73
  language: string;
74
+ featureFlags: FeatureFlagInfos
50
75
  };
51
76
 
52
77
  /**
@@ -131,6 +156,19 @@ export class SystemInfoProvider {
131
156
  const screenSize = `${ window.screen?.width || '?' }x${ window.screen?.height || '?' }`;
132
157
  const browserSize = `${ window.innerWidth }x${ window.innerHeight }`;
133
158
 
159
+ const safeFfs = Object.entries(ffs).reduce((res, [id, ff]) => {
160
+ try {
161
+ res[id] = {
162
+ param: ff.param,
163
+ value: getters['features/get'](id),
164
+ };
165
+ } catch (e) {
166
+ console.debug(`Cannot include Feature Flag "${ id }" in dynamic feature request: `, e); // eslint-disable-line no-console
167
+ }
168
+
169
+ return res;
170
+ }, {} as FeatureFlagInfos);
171
+
134
172
  return {
135
173
  systemUUID,
136
174
  userHash,
@@ -146,6 +184,7 @@ export class SystemInfoProvider {
146
184
  screenSize,
147
185
  browserSize,
148
186
  language: window.navigator?.language,
187
+ featureFlags: safeFfs,
149
188
  };
150
189
  }
151
190
 
@@ -213,6 +252,10 @@ export class SystemInfoProvider {
213
252
  params.push(`ss=${ systemData.screenSize }`);
214
253
  }
215
254
 
255
+ Object.values(systemData.featureFlags).forEach((ff) => {
256
+ params.push(`ff-` + `${ ff.param }=${ ff.value }`);
257
+ });
258
+
216
259
  return params.join('&');
217
260
  }
218
261
  }
package/utils/favicon.js CHANGED
@@ -9,17 +9,17 @@ export function haveSetFavIcon() {
9
9
 
10
10
  export function setFavIcon(store) {
11
11
  const res = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.FAVICON);
12
- const brandSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.BRAND);
12
+ const brandSetting = store.getters['management/brand'];
13
13
  const link = findIconLink(document.head.getElementsByTagName('link'));
14
14
 
15
15
  if (link) {
16
16
  let brandImage;
17
17
 
18
- if (brandSetting?.value === 'suse') {
18
+ if (brandSetting === 'suse') {
19
19
  brandImage = require('~shell/assets/brand/suse/favicon.png');
20
- } else if (brandSetting?.value === 'csp') {
20
+ } else if (brandSetting === 'csp') {
21
21
  brandImage = require('~shell/assets/brand/csp/favicon.png');
22
- } else if (brandSetting?.value === 'harvester') {
22
+ } else if (brandSetting === 'harvester') {
23
23
  brandImage = require('~shell/assets/brand/harvester/favicon.png');
24
24
  }
25
25
 
package/utils/fleet.ts CHANGED
@@ -184,7 +184,7 @@ class Fleet {
184
184
  }
185
185
 
186
186
  detailLocation(r: Resource, mgmtClusterName: string): any {
187
- return mapStateToEnum(r.state) === STATES_ENUM.MISSING ? undefined : {
187
+ const location = mapStateToEnum(r.state) === STATES_ENUM.MISSING ? undefined : {
188
188
  name: `c-cluster-product-resource${ r.namespace ? '-namespace' : '' }-id`,
189
189
  params: {
190
190
  product: EXPLORER_NAME,
@@ -194,6 +194,13 @@ class Fleet {
194
194
  id: r.name,
195
195
  },
196
196
  };
197
+
198
+ // Having an undefined param can yield a console warning like [Vue Router warn]: Discarded invalid param(s) "namespace" when navigating
199
+ if (location && !location.params.namespace) {
200
+ delete location.params.namespace;
201
+ }
202
+
203
+ return location;
197
204
  }
198
205
 
199
206
  /**