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

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 (272) hide show
  1. package/apis/impl/apis.ts +6 -0
  2. package/apis/index.ts +26 -0
  3. package/apis/intf/resources-api/cluster-api.ts +18 -0
  4. package/apis/intf/resources-api/mgmt-api.ts +15 -0
  5. package/apis/intf/resources-api/resource-base.ts +107 -0
  6. package/apis/intf/resources-api/resource-constants.ts +147 -0
  7. package/apis/intf/resources-api/resources-api.ts +143 -0
  8. package/apis/intf/resources.ts +49 -0
  9. package/apis/intf/{modal.ts → shell-api/modal.ts} +21 -26
  10. package/apis/intf/shell-api/proxy.ts +216 -0
  11. package/apis/intf/{slide-in.ts → shell-api/slide-in.ts} +4 -3
  12. package/apis/intf/{system.ts → shell-api/system.ts} +4 -1
  13. package/apis/intf/shell.ts +12 -6
  14. package/apis/resources/__tests__/resources-api-class.test.ts +550 -0
  15. package/apis/resources/index.ts +22 -0
  16. package/apis/resources/resources-api-class.ts +187 -0
  17. package/apis/shell/__tests__/proxy.test.ts +369 -0
  18. package/apis/shell/index.ts +8 -1
  19. package/apis/shell/modal.ts +4 -1
  20. package/apis/shell/notifications.ts +9 -6
  21. package/apis/shell/proxy.ts +256 -0
  22. package/apis/shell/slide-in.ts +4 -1
  23. package/apis/vue-shim.d.ts +2 -1
  24. package/assets/data/aws-regions.json +4 -0
  25. package/assets/fonts/lato/LatoLatin-Black.woff +0 -0
  26. package/assets/fonts/lato/LatoLatin-Black.woff2 +0 -0
  27. package/assets/fonts/lato/LatoLatin-BlackItalic.woff +0 -0
  28. package/assets/fonts/lato/LatoLatin-BlackItalic.woff2 +0 -0
  29. package/assets/fonts/lato/LatoLatin-Bold.woff +0 -0
  30. package/assets/fonts/lato/LatoLatin-Bold.woff2 +0 -0
  31. package/assets/fonts/lato/LatoLatin-BoldItalic.woff +0 -0
  32. package/assets/fonts/lato/LatoLatin-BoldItalic.woff2 +0 -0
  33. package/assets/fonts/lato/LatoLatin-Heavy.woff +0 -0
  34. package/assets/fonts/lato/LatoLatin-Heavy.woff2 +0 -0
  35. package/assets/fonts/lato/LatoLatin-HeavyItalic.woff +0 -0
  36. package/assets/fonts/lato/LatoLatin-HeavyItalic.woff2 +0 -0
  37. package/assets/fonts/lato/LatoLatin-Italic.woff +0 -0
  38. package/assets/fonts/lato/LatoLatin-Italic.woff2 +0 -0
  39. package/assets/fonts/lato/LatoLatin-Light.woff +0 -0
  40. package/assets/fonts/lato/LatoLatin-Light.woff2 +0 -0
  41. package/assets/fonts/lato/LatoLatin-LightItalic.woff +0 -0
  42. package/assets/fonts/lato/LatoLatin-LightItalic.woff2 +0 -0
  43. package/assets/fonts/lato/LatoLatin-Medium.woff +0 -0
  44. package/assets/fonts/lato/LatoLatin-Medium.woff2 +0 -0
  45. package/assets/fonts/lato/LatoLatin-MediumItalic.woff +0 -0
  46. package/assets/fonts/lato/LatoLatin-MediumItalic.woff2 +0 -0
  47. package/assets/fonts/lato/LatoLatin-Regular.woff +0 -0
  48. package/assets/fonts/lato/LatoLatin-Regular.woff2 +0 -0
  49. package/assets/fonts/lato/LatoLatin-Semibold.woff +0 -0
  50. package/assets/fonts/lato/LatoLatin-Semibold.woff2 +0 -0
  51. package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff +0 -0
  52. package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff2 +0 -0
  53. package/assets/styles/base/_variables.scss +2 -0
  54. package/assets/styles/fonts/_fontstack.scss +132 -8
  55. package/assets/translations/en-us.yaml +22 -5
  56. package/chart/monitoring/index.vue +10 -1
  57. package/components/ActionDropdownShell.vue +2 -1
  58. package/components/CruResourceFooter.vue +9 -5
  59. package/components/ExplorerProjectsNamespaces.vue +1 -1
  60. package/components/InstallHelmCharts.vue +2 -2
  61. package/components/LandingPagePreference.vue +14 -5
  62. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +15 -1
  63. package/components/Resource/Detail/Metadata/index.vue +6 -0
  64. package/components/Resource/Detail/ResourcePopover/index.vue +12 -1
  65. package/components/Resource/Detail/SpacedRow.vue +3 -1
  66. package/components/Resource/Detail/TitleBar/index.vue +10 -11
  67. package/components/ResourceList/Masthead.vue +12 -8
  68. package/components/SelectIconGrid.vue +0 -10
  69. package/components/SingleClusterInfo.vue +1 -0
  70. package/components/SortableTable/__tests__/sorting.test.ts +126 -0
  71. package/components/SortableTable/index.vue +6 -9
  72. package/components/SortableTable/selection.js +23 -5
  73. package/components/SortableTable/sorting.js +6 -3
  74. package/components/Wizard.vue +14 -13
  75. package/components/fleet/FleetBundles.vue +100 -12
  76. package/components/fleet/FleetClusterTargets/index.vue +37 -15
  77. package/components/fleet/__tests__/FleetClusterTargets.test.ts +149 -115
  78. package/components/fleet/__tests__/FleetClusters.test.ts +12 -12
  79. package/components/form/LabeledSelect.vue +20 -3
  80. package/components/form/NameNsDescription.vue +11 -0
  81. package/components/form/Security.vue +6 -2
  82. package/components/form/WorkloadPorts.vue +2 -7
  83. package/components/form/__tests__/Security.test.ts +76 -0
  84. package/components/formatter/Autoscaler.vue +4 -4
  85. package/components/formatter/ClusterKubeVersion.vue +27 -0
  86. package/components/formatter/ClusterLink.vue +1 -7
  87. package/components/formatter/ClusterProvider.vue +6 -10
  88. package/components/formatter/FleetSummaryGraph.vue +0 -3
  89. package/components/formatter/MachineSummaryGraph.vue +1 -1
  90. package/components/formatter/PodsUsage.vue +2 -2
  91. package/components/formatter/__tests__/Autoscaler.test.ts +19 -22
  92. package/components/formatter/__tests__/FleetSummaryGraph.test.ts +216 -0
  93. package/components/formatter/__tests__/PodsUsage.test.ts +6 -10
  94. package/components/nav/NamespaceFilter.vue +2 -2
  95. package/components/nav/TopLevelMenu.helper.ts +15 -3
  96. package/components/nav/TopLevelMenu.vue +16 -5
  97. package/components/nav/__tests__/TopLevelMenu.test.ts +145 -21
  98. package/components/templates/home.vue +18 -0
  99. package/components/templates/plain.vue +18 -0
  100. package/components/templates/standalone.vue +17 -0
  101. package/composables/useFormValidation.ts +93 -0
  102. package/composables/useVeeValidateField.test.ts +159 -0
  103. package/composables/useVeeValidateField.ts +67 -0
  104. package/config/pagination-table-headers.js +18 -1
  105. package/config/product/manager.js +82 -21
  106. package/config/router/routes.js +6 -0
  107. package/config/table-headers.js +20 -1
  108. package/config/types.js +2 -1
  109. package/core/__tests__/plugin-products.test.ts +904 -20
  110. package/core/plugin-products-base.ts +107 -7
  111. package/core/plugin-products.ts +4 -0
  112. package/core/plugin-types.ts +111 -1
  113. package/core/plugin.ts +15 -7
  114. package/core/productDebugger.js +9 -4
  115. package/core/types-provisioning.ts +43 -30
  116. package/core/types.ts +57 -20
  117. package/detail/__tests__/pod.test.ts +41 -0
  118. package/detail/harvesterhci.io.management.cluster.vue +6 -2
  119. package/detail/pod.vue +1 -1
  120. package/detail/provisioning.cattle.io.cluster.vue +4 -10
  121. package/edit/auth/__tests__/azuread.test.ts +217 -34
  122. package/edit/auth/azuread.vue +122 -14
  123. package/edit/auth/oidc.vue +2 -2
  124. package/edit/networking.k8s.io.ingress/DefaultBackend.vue +13 -4
  125. package/edit/networking.k8s.io.ingress/RulePath.vue +8 -4
  126. package/edit/networking.k8s.io.ingress/index.vue +75 -20
  127. package/edit/provisioning.cattle.io.cluster/__tests__/MachinePool.test.ts +104 -0
  128. package/edit/provisioning.cattle.io.cluster/index.vue +11 -7
  129. package/edit/provisioning.cattle.io.cluster/rke2.vue +8 -4
  130. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  131. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +37 -4
  132. package/edit/provisioning.cattle.io.cluster/tabs/registries/__tests__/RegistryConfigs.test.ts +132 -7
  133. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +2 -1
  134. package/edit/secret/__tests__/ssh.test.ts +5 -6
  135. package/edit/secret/basic.vue +31 -0
  136. package/edit/secret/index.vue +68 -17
  137. package/edit/secret/registry.vue +38 -0
  138. package/edit/secret/ssh.vue +29 -0
  139. package/edit/secret/tls.vue +30 -0
  140. package/edit/service.vue +4 -4
  141. package/edit/workload/Upgrading.vue +3 -3
  142. package/edit/workload/__tests__/Upgrading.test.ts +6 -9
  143. package/edit/workload/mixins/workload.js +2 -1
  144. package/list/fleet.cattle.io.bundle.vue +7 -104
  145. package/list/fleet.cattle.io.clusterregistrationtoken.vue +20 -0
  146. package/list/provisioning.cattle.io.cluster.vue +262 -180
  147. package/list/utils/management.cattle.io.cluster.utils.ts +128 -0
  148. package/mixins/__tests__/chart.test.ts +112 -0
  149. package/mixins/brand.js +2 -1
  150. package/mixins/chart.js +12 -8
  151. package/mixins/resource-fetch-api-pagination.js +41 -5
  152. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +67 -67
  153. package/models/__tests__/management.cattle.io.cluster.test.ts +1 -1
  154. package/models/__tests__/management.cattle.io.node.ts +6 -5
  155. package/models/__tests__/management.cattle.io.nodepool.ts +5 -4
  156. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +32 -11
  157. package/models/base-cluster.x-k8s.io.js +26 -0
  158. package/models/cluster.js +1 -1
  159. package/models/cluster.x-k8s.io.machine.js +4 -22
  160. package/models/cluster.x-k8s.io.machinedeployment.js +2 -20
  161. package/models/cluster.x-k8s.io.machineset.js +2 -20
  162. package/models/compliance.cattle.io.clusterscan.js +130 -2
  163. package/models/ext.cattle.io.kubeconfig.ts +4 -7
  164. package/models/fleet-application.js +3 -1
  165. package/models/management.cattle.io.cluster.js +417 -40
  166. package/models/management.cattle.io.node.js +6 -4
  167. package/models/management.cattle.io.nodepool.js +1 -1
  168. package/models/networking.k8s.io.ingress.js +12 -4
  169. package/models/provisioning.cattle.io.cluster.js +47 -330
  170. package/models/rke.cattle.io.etcdsnapshot.js +1 -2
  171. package/package.json +11 -29
  172. package/pages/__tests__/readme.test.ts +49 -0
  173. package/pages/auth/setup.vue +2 -3
  174. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +76 -0
  175. package/pages/c/_cluster/apps/charts/chart.vue +60 -8
  176. package/pages/c/_cluster/apps/charts/install.vue +10 -7
  177. package/pages/c/_cluster/explorer/__tests__/index.test.ts +23 -25
  178. package/pages/c/_cluster/explorer/index.vue +5 -49
  179. package/pages/c/_cluster/istio/__tests__/istio.index.test.ts +194 -0
  180. package/pages/c/_cluster/istio/index.vue +21 -6
  181. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -0
  182. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +719 -2
  183. package/pages/c/_cluster/uiplugins/index.vue +203 -197
  184. package/pages/diagnostic.vue +13 -17
  185. package/pages/fail-whale.vue +18 -0
  186. package/pages/home.vue +77 -260
  187. package/pages/readme.vue +88 -0
  188. package/plugins/dashboard-store/__tests__/resource-class.test.ts +88 -0
  189. package/plugins/dashboard-store/actions.js +40 -18
  190. package/plugins/dashboard-store/resource-class.js +5 -2
  191. package/plugins/steve/__tests__/subscribe.spec.ts +6 -3
  192. package/plugins/steve/steve-pagination-utils.ts +11 -3
  193. package/plugins/steve/subscribe.js +35 -5
  194. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +10 -4
  195. package/rancher-components/Form/LabeledInput/LabeledInput.vue +7 -52
  196. package/rancher-components/RcButton/RcButton.test.ts +37 -1
  197. package/rancher-components/RcButton/RcButton.vue +38 -8
  198. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +10 -8
  199. package/store/__tests__/catalog.test.ts +115 -1
  200. package/store/__tests__/type-map.test.ts +556 -1
  201. package/store/action-menu.js +8 -3
  202. package/store/auth.js +1 -1
  203. package/store/aws.js +27 -16
  204. package/store/catalog.js +27 -3
  205. package/store/digitalocean.js +20 -38
  206. package/store/index.js +2 -0
  207. package/store/linode.js +25 -40
  208. package/store/pnap.js +1 -0
  209. package/store/type-map.js +111 -29
  210. package/tsconfig.paths.json +8 -8
  211. package/types/kube/kube-api.ts +14 -1
  212. package/types/rancher/steve.api.ts +12 -12
  213. package/types/resources/settings.d.ts +2 -1
  214. package/types/shell/index.d.ts +102 -2
  215. package/types/store/dashboard-store.types.ts +108 -11
  216. package/types/store/pagination.types.ts +6 -3
  217. package/utils/__tests__/alertmanagerconfig.test.ts +117 -0
  218. package/utils/__tests__/async.test.ts +87 -0
  219. package/utils/__tests__/aws.test.ts +140 -0
  220. package/utils/__tests__/banners.test.ts +176 -0
  221. package/utils/__tests__/chart.test.ts +64 -1
  222. package/utils/__tests__/color.test.ts +226 -0
  223. package/utils/__tests__/duration.test.ts +140 -0
  224. package/utils/__tests__/fleet.test.ts +340 -0
  225. package/utils/__tests__/ingress.test.ts +553 -0
  226. package/utils/__tests__/kube.test.ts +68 -0
  227. package/utils/__tests__/namespace-filter.test.ts +109 -0
  228. package/utils/__tests__/pagination-utils.test.ts +361 -0
  229. package/utils/__tests__/parse-externalid.test.ts +137 -0
  230. package/utils/__tests__/perf-setting.utils.test.ts +98 -0
  231. package/utils/__tests__/poller-sequential.test.ts +177 -0
  232. package/utils/__tests__/poller.test.ts +170 -0
  233. package/utils/__tests__/promise.test.ts +346 -0
  234. package/utils/__tests__/settings.test.ts +140 -0
  235. package/utils/__tests__/sort-utils.test.ts +301 -0
  236. package/utils/__tests__/string-utils.test.ts +798 -0
  237. package/utils/__tests__/string.test.ts +23 -1
  238. package/utils/__tests__/style.test.ts +154 -0
  239. package/utils/__tests__/svg-filter.test.ts +184 -0
  240. package/utils/__tests__/units.test.ts +417 -0
  241. package/utils/__tests__/versions.test.ts +128 -0
  242. package/utils/__tests__/xccdf.test.ts +391 -0
  243. package/utils/chart.js +36 -0
  244. package/utils/fleet.ts +13 -3
  245. package/utils/gatekeeper/__tests__/util.test.ts +174 -0
  246. package/utils/gc/__tests__/gc-interval.test.ts +119 -0
  247. package/utils/gc/__tests__/gc-root-store.test.ts +225 -0
  248. package/utils/gc/__tests__/gc-route-changed.test.ts +96 -0
  249. package/utils/gc/__tests__/gc.test.ts +487 -0
  250. package/utils/ingress.ts +9 -1
  251. package/utils/pagination-utils.ts +2 -1
  252. package/utils/string.js +25 -2
  253. package/utils/uiplugins.ts +5 -5
  254. package/utils/validators/__tests__/cluster-name.test.ts +110 -0
  255. package/utils/validators/__tests__/cron-schedule.test.ts +79 -0
  256. package/utils/validators/__tests__/index.test.ts +481 -0
  257. package/utils/validators/__tests__/kubernetes-name.test.ts +163 -0
  258. package/utils/validators/__tests__/misc-validators.test.ts +246 -0
  259. package/utils/validators/__tests__/pod-affinity.test.ts +382 -0
  260. package/utils/validators/__tests__/prometheusrule.test.ts +211 -0
  261. package/utils/validators/__tests__/role-template.test.ts +149 -0
  262. package/utils/validators/__tests__/service.test.ts +283 -0
  263. package/utils/validators/__tests__/setting.test.js +32 -0
  264. package/utils/validators/formRules/__tests__/index.test.ts +50 -0
  265. package/utils/validators/formRules/index.ts +5 -5
  266. package/utils/validators/machine-pool.ts +1 -1
  267. package/utils/validators/setting.js +18 -3
  268. package/utils/xccdf.ts +418 -0
  269. package/assets/fonts/lato/lato-v17-latin-700.woff +0 -0
  270. package/assets/fonts/lato/lato-v17-latin-700.woff2 +0 -0
  271. package/assets/fonts/lato/lato-v17-latin-regular.woff +0 -0
  272. package/assets/fonts/lato/lato-v17-latin-regular.woff2 +0 -0
@@ -0,0 +1,487 @@
1
+ import type defaultGc from '@shell/utils/gc/gc';
2
+ import { MANAGEMENT } from '@shell/config/types';
3
+
4
+ type GcInstance = typeof defaultGc;
5
+
6
+ const DEFAULT_PREFS = {
7
+ enabled: true,
8
+ enabledInterval: true,
9
+ interval: 300,
10
+ enabledOnNavigate: true,
11
+ ageThreshold: 120, // seconds → maxAge = 120_000 ms
12
+ countThreshold: 500,
13
+ };
14
+
15
+ let stampCounter = 1000;
16
+
17
+ function makeRootState(prefs = DEFAULT_PREFS, clusterReady = true): any {
18
+ stampCounter++;
19
+
20
+ return {
21
+ clusterReady,
22
+ management: {
23
+ types: {
24
+ [MANAGEMENT.SETTING]: {
25
+ list: [
26
+ {
27
+ id: 'ui-performance',
28
+ value: JSON.stringify({ garbageCollection: prefs }),
29
+ metadata: { generation: stampCounter, resourceVersion: '1' },
30
+ },
31
+ ],
32
+ },
33
+ },
34
+ },
35
+ };
36
+ }
37
+
38
+ function makeEmptyRootState(): any {
39
+ return {
40
+ clusterReady: true,
41
+ management: { types: { [MANAGEMENT.SETTING]: { list: [] } } },
42
+ };
43
+ }
44
+
45
+ function makeCtx(options: {
46
+ namespace?: string;
47
+ supportsGc?: boolean;
48
+ gcIgnoreTypes?: Record<string, boolean>;
49
+ prefs?: Partial<typeof DEFAULT_PREFS>;
50
+ dispatch?: jest.Mock;
51
+ countsByType?: Record<string, number>;
52
+ clusterReady?: boolean;
53
+ } = {}): any {
54
+ const {
55
+ namespace = 'teststore',
56
+ supportsGc = true,
57
+ gcIgnoreTypes = {},
58
+ prefs,
59
+ dispatch = jest.fn(),
60
+ countsByType = {},
61
+ clusterReady = true,
62
+ } = options;
63
+
64
+ const rootState = makeRootState(
65
+ prefs ? { ...DEFAULT_PREFS, ...prefs } : DEFAULT_PREFS,
66
+ clusterReady
67
+ );
68
+
69
+ return {
70
+ state: { config: { supportsGc, namespace } },
71
+ rootState,
72
+ getters: {
73
+ gcIgnoreTypes,
74
+ all: () => [{
75
+ counts: Object.fromEntries(
76
+ Object.entries(countsByType).map(([t, c]) => [t, { summary: { count: c } }])
77
+ ),
78
+ }],
79
+ },
80
+ dispatch,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Each test in the outer describe gets a fresh gc instance (gcLastRun = 0,
86
+ * empty caches) so tests do not interfere with each other via shared singleton state.
87
+ */
88
+ describe('gc', () => {
89
+ let gc: GcInstance;
90
+
91
+ beforeEach(() => {
92
+ jest.resetModules();
93
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
94
+ gc = require('@shell/utils/gc/gc').default as GcInstance;
95
+ });
96
+
97
+ afterEach(() => {
98
+ jest.useRealTimers();
99
+ });
100
+
101
+ // ─── gcEnabledForStore ─────────────────────────────────────────────────────
102
+
103
+ describe('gcEnabledForStore', () => {
104
+ it.each([
105
+ {
106
+ desc: 'true when supportsGc is true', state: { config: { supportsGc: true } }, expected: true
107
+ },
108
+ {
109
+ desc: 'false when supportsGc is false', state: { config: { supportsGc: false } }, expected: false
110
+ },
111
+ {
112
+ desc: 'undefined when state is undefined', state: undefined, expected: undefined
113
+ },
114
+ ])('returns $desc', ({ state, expected }) => {
115
+ expect(gc.gcEnabledForStore(state)).toBe(expected);
116
+ });
117
+ });
118
+
119
+ // ─── gcEnabledForType ──────────────────────────────────────────────────────
120
+
121
+ describe('gcEnabledForType', () => {
122
+ it.each([
123
+ {
124
+ desc: 'false when type is empty string', gcIgnoreTypes: {}, type: '', expected: false
125
+ },
126
+ {
127
+ desc: 'false when type is in gcIgnoreTypes', gcIgnoreTypes: { pods: true }, type: 'pods', expected: false
128
+ },
129
+ {
130
+ desc: 'true when type is non-empty and not ignored', gcIgnoreTypes: {}, type: 'deployments', expected: true
131
+ },
132
+ ])('returns $desc', ({ gcIgnoreTypes, type, expected }) => {
133
+ expect(gc.gcEnabledForType({ getters: { gcIgnoreTypes } }, type)).toBe(expected);
134
+ });
135
+ });
136
+
137
+ // ─── gcEnabledSetting ──────────────────────────────────────────────────────
138
+
139
+ describe('gcEnabledSetting', () => {
140
+ it('returns true when GC is enabled in ui-performance setting', () => {
141
+ const ctx = { rootState: makeRootState({ ...DEFAULT_PREFS, enabled: true }) };
142
+
143
+ expect(gc.gcEnabledSetting(ctx)).toBe(true);
144
+ });
145
+
146
+ it('returns false when GC is disabled in ui-performance setting', () => {
147
+ const ctx = { rootState: makeRootState({ ...DEFAULT_PREFS, enabled: false }) };
148
+
149
+ expect(gc.gcEnabledSetting(ctx)).toBe(false);
150
+ });
151
+
152
+ it('returns undefined when ui-performance setting is absent', () => {
153
+ const ctx = { rootState: makeEmptyRootState() };
154
+
155
+ expect(gc.gcEnabledSetting(ctx)).toBeUndefined();
156
+ });
157
+ });
158
+
159
+ // ─── gcEnabledAll ──────────────────────────────────────────────────────────
160
+
161
+ describe('gcEnabledAll', () => {
162
+ it.each([
163
+ {
164
+ desc: 'true when all checks pass', options: {}, type: 'deployments', expected: true
165
+ },
166
+ {
167
+ desc: 'false when store does not support GC', options: { supportsGc: false }, type: 'deployments', expected: false
168
+ },
169
+ {
170
+ desc: 'false when GC is disabled in settings', options: { prefs: { enabled: false } }, type: 'deployments', expected: false
171
+ },
172
+ {
173
+ desc: 'false when type is in gcIgnoreTypes', options: { gcIgnoreTypes: { schema: true } }, type: 'schema', expected: false
174
+ },
175
+ ])('returns $desc', ({ options, type, expected }) => {
176
+ expect(gc.gcEnabledAll(makeCtx(options), type)).toBe(expected);
177
+ });
178
+ });
179
+
180
+ // ─── gcEnabledInterval ─────────────────────────────────────────────────────
181
+
182
+ describe('gcEnabledInterval', () => {
183
+ it('returns enabledInterval and interval from settings', () => {
184
+ const ctx = {
185
+ rootState: makeRootState({
186
+ ...DEFAULT_PREFS, enabledInterval: true, interval: 600
187
+ })
188
+ };
189
+
190
+ expect(gc.gcEnabledInterval(ctx)).toStrictEqual({ enabled: true, interval: 600 });
191
+ });
192
+
193
+ it('returns enabled: undefined and interval: 0 when setting is absent', () => {
194
+ const ctx = { rootState: makeEmptyRootState() };
195
+
196
+ expect(gc.gcEnabledInterval(ctx)).toStrictEqual({ enabled: undefined, interval: 0 });
197
+ });
198
+
199
+ it('returns interval: 0 when the interval value is falsy', () => {
200
+ const ctx = { rootState: makeRootState({ ...DEFAULT_PREFS, interval: 0 }) };
201
+
202
+ expect(gc.gcEnabledInterval(ctx)).toStrictEqual({ enabled: true, interval: 0 });
203
+ });
204
+ });
205
+
206
+ // ─── gcEnabledRoute ────────────────────────────────────────────────────────
207
+
208
+ describe('gcEnabledRoute', () => {
209
+ it('returns enabledOnNavigate from settings', () => {
210
+ const ctx = { rootState: makeRootState({ ...DEFAULT_PREFS, enabledOnNavigate: false }) };
211
+
212
+ expect(gc.gcEnabledRoute(ctx)).toBe(false);
213
+ });
214
+
215
+ it('returns undefined when setting is absent', () => {
216
+ const ctx = { rootState: makeEmptyRootState() };
217
+
218
+ expect(gc.gcEnabledRoute(ctx)).toBeUndefined();
219
+ });
220
+ });
221
+
222
+ // ─── gcUpdateRouteChanged ──────────────────────────────────────────────────
223
+
224
+ describe('gcUpdateRouteChanged', () => {
225
+ /**
226
+ * Condition in garbageCollect:
227
+ * if (lastRouteChange < lastAccessed) → skip (resource is in current route)
228
+ *
229
+ * So: route changed at T1, resource accessed at T2 > T1
230
+ * → lastRouteChange (T1) < lastAccessed (T2) → GC skips this type.
231
+ */
232
+ it('prevents a resource from being GC\'d when route changed before last access', () => {
233
+ const dispatch = jest.fn();
234
+ const type = 'pods';
235
+ const namespace = 'routetest';
236
+
237
+ jest.useFakeTimers();
238
+
239
+ // Route change at t=200_000 ms
240
+ jest.setSystemTime(200_000);
241
+ gc.gcUpdateRouteChanged();
242
+
243
+ // Resource accessed at t=300_000 ms (AFTER route change → in current route)
244
+ jest.setSystemTime(300_000);
245
+ gc.gcUpdateLastAccessed(makeCtx({ namespace }), type);
246
+
247
+ // GC runs at t=600_000 ms
248
+ // now - lastAccessed = 300_000 > maxAge(120_000) ✓ age check passes
249
+ // lastRouteChange(200_000) < lastAccessed(300_000) → skip (in current route)
250
+ jest.setSystemTime(600_000);
251
+ gc.garbageCollect(makeCtx({
252
+ namespace,
253
+ countsByType: { [type]: 1000 },
254
+ dispatch,
255
+ }));
256
+
257
+ expect(dispatch).not.toHaveBeenCalledWith('forgetType', type);
258
+ });
259
+ });
260
+
261
+ // ─── garbageCollect ────────────────────────────────────────────────────────
262
+
263
+ describe('garbageCollect', () => {
264
+ it('skips when recently run (within GC_RE_RUN_GAP of 5 s)', () => {
265
+ const dispatch = jest.fn();
266
+
267
+ jest.useFakeTimers();
268
+ jest.setSystemTime(10_000);
269
+
270
+ // First call: cluster not ready → gcLastRun = 10_000
271
+ gc.garbageCollect(makeCtx({ clusterReady: false, dispatch }));
272
+ // Second call at same time: 10_000 - 10_000 = 0 < 5000 → skips
273
+ gc.garbageCollect(makeCtx({ dispatch }));
274
+
275
+ expect(dispatch).not.toHaveBeenCalledWith('forgetType', expect.any(String));
276
+ });
277
+
278
+ it('skips when cluster is not ready', () => {
279
+ const dispatch = jest.fn();
280
+
281
+ jest.useFakeTimers();
282
+ jest.setSystemTime(10_000);
283
+
284
+ gc.garbageCollect(makeCtx({ clusterReady: false, dispatch }));
285
+
286
+ expect(dispatch).not.toHaveBeenCalledWith('forgetType', expect.any(String));
287
+ });
288
+
289
+ it('skips when ui-performance setting is absent', () => {
290
+ const dispatch = jest.fn();
291
+
292
+ jest.useFakeTimers();
293
+ jest.setSystemTime(10_000);
294
+
295
+ const ctx = {
296
+ state: { config: { supportsGc: true, namespace: 'teststore' } },
297
+ rootState: makeEmptyRootState(),
298
+ getters: { gcIgnoreTypes: {} },
299
+ dispatch,
300
+ };
301
+
302
+ gc.garbageCollect(ctx);
303
+
304
+ expect(dispatch).not.toHaveBeenCalledWith('forgetType', expect.any(String));
305
+ });
306
+
307
+ it('skips a type listed in the explicit ignoreTypes parameter', () => {
308
+ const dispatch = jest.fn();
309
+ const type = 'configmaps';
310
+ const namespace = 'ignoretypetest';
311
+
312
+ jest.useFakeTimers();
313
+
314
+ jest.setSystemTime(200_000);
315
+ gc.gcUpdateLastAccessed(makeCtx({ namespace }), type);
316
+
317
+ jest.setSystemTime(300_000);
318
+ gc.gcUpdateRouteChanged();
319
+
320
+ jest.setSystemTime(400_000);
321
+ gc.garbageCollect(
322
+ makeCtx({
323
+ namespace, countsByType: { [type]: 1000 }, dispatch
324
+ }),
325
+ { [type]: true }
326
+ );
327
+
328
+ expect(dispatch).not.toHaveBeenCalledWith('forgetType', type);
329
+ });
330
+
331
+ it('skips a type that was accessed within the ageThreshold', () => {
332
+ const dispatch = jest.fn();
333
+ const type = 'secrets';
334
+ const namespace = 'recenttest';
335
+
336
+ jest.useFakeTimers();
337
+
338
+ // Access 30 s ago (less than 120 s ageThreshold)
339
+ jest.setSystemTime(10_000);
340
+ gc.gcUpdateLastAccessed(makeCtx({ namespace }), type);
341
+
342
+ jest.setSystemTime(40_000); // 30 s later
343
+ gc.garbageCollect(makeCtx({
344
+ namespace, countsByType: { [type]: 1000 }, dispatch
345
+ }));
346
+
347
+ expect(dispatch).not.toHaveBeenCalledWith('forgetType', type);
348
+ });
349
+
350
+ it('skips a type whose count is below countThreshold', () => {
351
+ const dispatch = jest.fn();
352
+ const type = 'namespaces';
353
+ const namespace = 'lowcounttest';
354
+
355
+ jest.useFakeTimers();
356
+
357
+ jest.setSystemTime(200_000);
358
+ gc.gcUpdateLastAccessed(makeCtx({ namespace }), type);
359
+
360
+ jest.setSystemTime(300_000);
361
+ gc.gcUpdateRouteChanged();
362
+
363
+ jest.setSystemTime(400_000);
364
+ // count(100) < countThreshold(500) → skip
365
+ gc.garbageCollect(makeCtx({
366
+ namespace, countsByType: { [type]: 100 }, dispatch
367
+ }));
368
+
369
+ expect(dispatch).not.toHaveBeenCalledWith('forgetType', type);
370
+ });
371
+
372
+ it('dispatches forgetType for a resource that meets all GC criteria', () => {
373
+ const dispatch = jest.fn();
374
+ const type = 'events';
375
+ const namespace = 'gcqualifytest';
376
+
377
+ jest.useFakeTimers();
378
+
379
+ // Resource accessed at t=200_000
380
+ jest.setSystemTime(200_000);
381
+ gc.gcUpdateLastAccessed(makeCtx({ namespace }), type);
382
+
383
+ // Route changed at t=300_000 (AFTER access → lastRouteChange > lastAccessed → no skip)
384
+ jest.setSystemTime(300_000);
385
+ gc.gcUpdateRouteChanged();
386
+
387
+ // GC at t=400_000: now - lastAccessed = 200_000 > maxAge(120_000) ✓
388
+ jest.setSystemTime(400_000);
389
+ gc.garbageCollect(makeCtx({
390
+ namespace, countsByType: { [type]: 1000 }, dispatch
391
+ }));
392
+
393
+ expect(dispatch).toHaveBeenCalledWith('forgetType', type);
394
+ });
395
+ });
396
+
397
+ // ─── gcResetStore ──────────────────────────────────────────────────────────
398
+
399
+ describe('gcResetStore', () => {
400
+ it('removes all cached entries for the store, preventing forgetType dispatch', () => {
401
+ const dispatch = jest.fn();
402
+ const type = 'ingresses';
403
+ const namespace = 'resetstoretest';
404
+
405
+ jest.useFakeTimers();
406
+
407
+ jest.setSystemTime(200_000);
408
+ gc.gcUpdateLastAccessed(makeCtx({ namespace }), type);
409
+
410
+ jest.setSystemTime(300_000);
411
+ gc.gcUpdateRouteChanged();
412
+
413
+ gc.gcResetStore({ config: { namespace } });
414
+
415
+ jest.setSystemTime(400_000);
416
+ gc.garbageCollect(makeCtx({
417
+ namespace, countsByType: { [type]: 1000 }, dispatch
418
+ }));
419
+
420
+ expect(dispatch).not.toHaveBeenCalledWith('forgetType', type);
421
+ });
422
+ });
423
+
424
+ // ─── gcResetType ───────────────────────────────────────────────────────────
425
+
426
+ describe('gcResetType', () => {
427
+ it('removes a specific type so it is not GC\'d while leaving other types intact', () => {
428
+ const dispatch = jest.fn();
429
+ const typeA = 'storageclasses';
430
+ const typeB = 'persistentvolumes';
431
+ const namespace = 'resettypetest';
432
+
433
+ jest.useFakeTimers();
434
+
435
+ jest.setSystemTime(200_000);
436
+ gc.gcUpdateLastAccessed(makeCtx({ namespace }), typeA);
437
+ gc.gcUpdateLastAccessed(makeCtx({ namespace }), typeB);
438
+
439
+ jest.setSystemTime(300_000);
440
+ gc.gcUpdateRouteChanged();
441
+
442
+ gc.gcResetType({ config: { namespace } }, typeA);
443
+
444
+ jest.setSystemTime(400_000);
445
+ gc.garbageCollect(makeCtx({
446
+ namespace,
447
+ countsByType: { [typeA]: 1000, [typeB]: 1000 },
448
+ dispatch,
449
+ }));
450
+
451
+ expect(dispatch).not.toHaveBeenCalledWith('forgetType', typeA);
452
+ expect(dispatch).toHaveBeenCalledWith('forgetType', typeB);
453
+ });
454
+
455
+ it('is a no-op when the store has no cached entries', () => {
456
+ expect(() => {
457
+ gc.gcResetType({ config: { namespace: 'nonexistent-store' } }, 'pods');
458
+ }).not.toThrow();
459
+ });
460
+ });
461
+
462
+ // ─── gcUpdateLastAccessed ──────────────────────────────────────────────────
463
+
464
+ describe('gcUpdateLastAccessed', () => {
465
+ it('does not populate cache when GC is disabled for the store', () => {
466
+ const dispatch = jest.fn();
467
+ const type = 'replicasets';
468
+ const namespace = 'disabledgctest';
469
+
470
+ jest.useFakeTimers();
471
+
472
+ // supportsGc: false → gcEnabledAll = false → cache not updated
473
+ jest.setSystemTime(200_000);
474
+ gc.gcUpdateLastAccessed(makeCtx({ namespace, supportsGc: false }), type);
475
+
476
+ jest.setSystemTime(300_000);
477
+ gc.gcUpdateRouteChanged();
478
+
479
+ jest.setSystemTime(400_000);
480
+ gc.garbageCollect(makeCtx({
481
+ namespace, countsByType: { [type]: 1000 }, dispatch
482
+ }));
483
+
484
+ expect(dispatch).not.toHaveBeenCalledWith('forgetType', type);
485
+ });
486
+ });
487
+ });
package/utils/ingress.ts CHANGED
@@ -56,7 +56,15 @@ class IngressDetailEditHelper {
56
56
  return services.map((service) => ({
57
57
  label: service.metadata.name,
58
58
  value: service.metadata.name,
59
- ports: service.spec.ports?.map((p: any) => p.port)
59
+ ports: service.spec.ports?.flatMap((p: any) => {
60
+ const options = [p.port];
61
+
62
+ if (p.name) {
63
+ options.push(p.name);
64
+ }
65
+
66
+ return options;
67
+ })
60
68
  }));
61
69
  }
62
70
  }
@@ -24,9 +24,10 @@ import { EXT_IDS } from '@shell/core/plugin';
24
24
  import { ExtensionManager } from '@shell/types/extension-manager';
25
25
  import { DEFAULT_PERF_SETTING } from '@shell/config/settings';
26
26
 
27
+ // This feature will be removed soon - https://github.com/rancher/dashboard/issues/17323
27
28
  const homePageClusterFeature: PaginationFeature<PaginationFeatureHomePageClusterConfig> = {
28
29
  version: 1,
29
- enabled: true,
30
+ enabled: false,
30
31
  configuration: {
31
32
  threshold: 500, results: 250, pagesPerRow: 25
32
33
  }
package/utils/string.js CHANGED
@@ -349,10 +349,33 @@ export function xOfy(x, y) {
349
349
  return `${ typeof x === 'number' ? x : '?' }/${ typeof y === 'number' ? y : '?' }`;
350
350
  }
351
351
 
352
+ const BASE64_REGEX = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
353
+
352
354
  export function isBase64(value) {
353
- const base64regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
355
+ return BASE64_REGEX.test(value);
356
+ }
357
+
358
+ /**
359
+ * Checks if a value is a valid base64-encoded CA bundle.
360
+ * Unlike isBase64, this handles multiline base64 (e.g. openssl wraps at 76 chars)
361
+ * and rejects short strings that could be false positives.
362
+ * @param {string} value
363
+ * @returns {boolean}
364
+ */
365
+ export function isBase64EncodedCert(value) {
366
+ if (!value || typeof value !== 'string') {
367
+ return false;
368
+ }
369
+
370
+ // Strip whitespace to handle line-wrapped base64 output
371
+ const stripped = value.replace(/\s/g, '');
372
+
373
+ // CA certs are long enough that legitimate base64 will always exceed this
374
+ if (stripped.length < 16) {
375
+ return false;
376
+ }
354
377
 
355
- return base64regex.test(value);
378
+ return BASE64_REGEX.test(stripped);
356
379
  }
357
380
 
358
381
  export function generateRandomAlphaString(length) {
@@ -180,18 +180,19 @@ export async function getHelmRepositoryExact(store: any, url: string): Promise<H
180
180
  /**
181
181
  *
182
182
  * @param store Vue store
183
- * @param urlRegexes Regex to match a community repository
183
+ * @param urlRegexes Regex to match against the repository's urls
184
184
  * @param catalogImages Catalog images to match against the repository's labels
185
185
  * @returns HelmRepository
186
186
  */
187
187
  export async function getHelmRepositoryMatch(store: any, urlRegexes: string[], catalogImages: string[]): Promise<HelmRepository> {
188
188
  return await getHelmRepository(store, (repository: any) => {
189
- // if installed from rancher/ui-plugin-catalog or rancher/ui-extension-harvester-ui-extension
190
189
  const catalog = repository?.metadata?.labels?.[UI_PLUGIN_LABELS.CATALOG_IMAGE] || '';
191
190
 
192
- if (catalogImages.includes(catalog)) {
191
+ // if installed from rancher/ui-plugin-catalog or rancher/ui-extension-harvester-ui-extension
192
+ if (catalog && catalogImages.includes(catalog)) {
193
193
  return true;
194
194
  }
195
+
195
196
  const target = repository.spec?.gitBranch ? repository.spec?.gitRepo : repository.spec?.url;
196
197
 
197
198
  return matchesSomeRegex(target, urlRegexes);
@@ -272,8 +273,7 @@ export async function createHelmRepository(store: any, name: string, url: string
272
273
  });
273
274
 
274
275
  tries++;
275
-
276
- const downloaded = repo.status.conditions.find((s: any) => s.type === 'Downloaded');
276
+ const downloaded = repo.status?.conditions.find((s: any) => s.type === 'Downloaded');
277
277
 
278
278
  console.log(`Waiting for helm repository to be downloaded... try ${ tries } time(s).`); // eslint-disable-line no-console
279
279
 
@@ -0,0 +1,110 @@
1
+ import { clusterName } from '@shell/utils/validators/cluster-name';
2
+
3
+ function makeGetters(): Record<string, unknown> {
4
+ return {
5
+ 'i18n/t': (key: string, args?: unknown) => (args ? `${ key }:${ JSON.stringify(args) }` : key),
6
+ 'i18n/exists': () => false,
7
+ };
8
+ }
9
+
10
+ describe('clusterName', () => {
11
+ describe('non-rke2 mode', () => {
12
+ it('returns no errors for any name when not rke2', () => {
13
+ const errors = clusterName('c-abcde', makeGetters(), [], ['false'], 'Name');
14
+
15
+ expect(errors).toStrictEqual([]);
16
+ });
17
+
18
+ it('returns no errors for local when not rke2', () => {
19
+ const errors = clusterName('local', makeGetters(), [], ['false'], 'Name');
20
+
21
+ expect(errors).toStrictEqual([]);
22
+ });
23
+
24
+ it('returns no errors for arbitrary name when not rke2', () => {
25
+ const errors = clusterName('my-cluster', makeGetters(), [], ['false'], 'Name');
26
+
27
+ expect(errors).toStrictEqual([]);
28
+ });
29
+ });
30
+
31
+ describe('rke2 mode', () => {
32
+ it('rejects name matching c-XXXXX pattern', () => {
33
+ const errors = clusterName('c-abcde', makeGetters(), [], ['true'], 'Name');
34
+
35
+ expect(errors).toContain('validation.cluster.name');
36
+ });
37
+
38
+ it('rejects "local" cluster name', () => {
39
+ const errors = clusterName('local', makeGetters(), [], ['true'], 'Name');
40
+
41
+ expect(errors).toContain('validation.cluster.name');
42
+ });
43
+
44
+ it('rejects "LOCAL" (case-insensitive match)', () => {
45
+ const errors = clusterName('LOCAL', makeGetters(), [], ['true'], 'Name');
46
+
47
+ expect(errors).toContain('validation.cluster.name');
48
+ });
49
+
50
+ it('rejects "Local" (mixed case)', () => {
51
+ const errors = clusterName('Local', makeGetters(), [], ['true'], 'Name');
52
+
53
+ expect(errors).toContain('validation.cluster.name');
54
+ });
55
+
56
+ it('rejects "c-ABCDE" (uppercase legacy id)', () => {
57
+ const errors = clusterName('c-ABCDE', makeGetters(), [], ['true'], 'Name');
58
+
59
+ expect(errors).toContain('validation.cluster.name');
60
+ });
61
+
62
+ it('accepts a valid rke2 cluster name', () => {
63
+ const errors = clusterName('my-cluster', makeGetters(), [], ['true'], 'Name');
64
+
65
+ expect(errors).toStrictEqual([]);
66
+ });
67
+
68
+ it('accepts name with more than 5 chars after c-', () => {
69
+ const errors = clusterName('c-abcdef', makeGetters(), [], ['true'], 'Name');
70
+
71
+ expect(errors).toStrictEqual([]);
72
+ });
73
+
74
+ it('accepts name starting with c- but less than 5 chars after', () => {
75
+ const errors = clusterName('c-abc', makeGetters(), [], ['true'], 'Name');
76
+
77
+ expect(errors).toStrictEqual([]);
78
+ });
79
+ });
80
+
81
+ describe('empty/null pathValue', () => {
82
+ it('handles empty string without throwing', () => {
83
+ const errors = clusterName('', makeGetters(), [], ['true'], 'Name');
84
+
85
+ expect(errors).toStrictEqual([]);
86
+ });
87
+
88
+ it('handles null without throwing', () => {
89
+ const errors = clusterName(null, makeGetters(), [], ['true'], 'Name');
90
+
91
+ expect(errors).toStrictEqual([]);
92
+ });
93
+
94
+ it('handles undefined without throwing', () => {
95
+ const errors = clusterName(undefined, makeGetters(), [], ['true'], 'Name');
96
+
97
+ expect(errors).toStrictEqual([]);
98
+ });
99
+ });
100
+
101
+ describe('errors array accumulation', () => {
102
+ it('appends to existing errors array', () => {
103
+ const existing = ['prior-error'];
104
+ const errors = clusterName('local', makeGetters(), existing, ['true'], 'Name');
105
+
106
+ expect(errors[0]).toBe('prior-error');
107
+ expect(errors).toContain('validation.cluster.name');
108
+ });
109
+ });
110
+ });