@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,553 @@
1
+ import IngressDetailEditHelper from '@shell/utils/ingress';
2
+ import { SECRET_TYPES as TYPES } from '@shell/config/secret';
3
+ import { get, set } from '@shell/utils/object';
4
+
5
+ function parseServicePort(rawPort: string | number): string | number {
6
+ const parsed = Number.parseInt(String(rawPort));
7
+
8
+ return Number.isNaN(parsed) ? rawPort : parsed;
9
+ }
10
+
11
+ function portRequired(port: any): string | undefined {
12
+ if (typeof port === 'string' || typeof port === 'number') {
13
+ if (!port) {
14
+ return 'Port is required';
15
+ }
16
+ } else if (!port || (!port.number && !port.name)) {
17
+ return 'Port is required';
18
+ }
19
+
20
+ return undefined;
21
+ }
22
+
23
+ function portRange(port: any): string | undefined {
24
+ let num;
25
+
26
+ if (typeof port === 'number') {
27
+ num = port;
28
+ } else if (typeof port === 'string') {
29
+ num = Number.parseInt(port);
30
+ } else if (port?.number) {
31
+ num = port.number;
32
+ }
33
+
34
+ if (num !== undefined && !Number.isNaN(num) && (num < 1 || num > 65535)) {
35
+ return 'Port number must be between 1 and 65535';
36
+ }
37
+
38
+ return undefined;
39
+ }
40
+
41
+ /**
42
+ * Mirrors willSave() from index.vue.
43
+ * Clears the default backend when the service name or port is missing.
44
+ */
45
+ function willSave(spec: any, paths: { defaultBackendPath: string; serviceNamePath: string; servicePortPath: string; servicePortNamePath: string }): void {
46
+ const backend = get(spec, paths.defaultBackendPath);
47
+ const serviceName = get(backend, paths.serviceNamePath);
48
+ const servicePort = get(backend, paths.servicePortPath) ||
49
+ get(backend, paths.servicePortNamePath);
50
+
51
+ if (backend && (!serviceName || !servicePort)) {
52
+ set(spec, paths.defaultBackendPath, null);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Mirrors defaultBackendNameRequired from index.vue.
58
+ * Only enforces required when a service is actually selected.
59
+ */
60
+ function defaultBackendNameRequired(name: any, hasService: boolean): string | undefined {
61
+ if (hasService && !name) {
62
+ return 'Target Service is required';
63
+ }
64
+
65
+ return undefined;
66
+ }
67
+
68
+ /**
69
+ * Mirrors defaultBackendPortRequired from index.vue.
70
+ * Delegates to portRequired only when a service is selected.
71
+ */
72
+ function defaultBackendPortRequired(port: any, hasService: boolean): string | undefined {
73
+ if (!hasService) {
74
+ return undefined;
75
+ }
76
+
77
+ return portRequired(port);
78
+ }
79
+
80
+ const makeHelper = () => new IngressDetailEditHelper({
81
+ $store: {} as any,
82
+ namespace: 'default',
83
+ });
84
+
85
+ describe('ingress', () => {
86
+ describe('findAndMapCerts', () => {
87
+ it.each([
88
+ {
89
+ desc: 'returns empty array when given empty secrets list',
90
+ secrets: [],
91
+ expected: [],
92
+ },
93
+ {
94
+ desc: 'returns empty array when no secrets are of TLS type',
95
+ secrets: [
96
+ { _type: 'Opaque', id: 'default/opaque-secret' },
97
+ { _type: 'kubernetes.io/basic-auth', id: 'default/basic-secret' },
98
+ ],
99
+ expected: [],
100
+ },
101
+ {
102
+ desc: 'extracts name part (after slash) from TLS secret id',
103
+ secrets: [{ _type: TYPES.TLS, id: 'default/my-tls-cert' }],
104
+ expected: ['my-tls-cert'],
105
+ },
106
+ {
107
+ desc: 'filters to TLS secrets only from a mixed list',
108
+ secrets: [
109
+ { _type: TYPES.TLS, id: 'default/tls-one' },
110
+ { _type: 'Opaque', id: 'default/not-tls' },
111
+ { _type: TYPES.TLS, id: 'default/tls-two' },
112
+ ],
113
+ expected: ['tls-one', 'tls-two'],
114
+ },
115
+ {
116
+ desc: 'handles TLS secret id with no slash by returning full id',
117
+ secrets: [{ _type: TYPES.TLS, id: 'no-slash-id' }],
118
+ expected: ['no-slash-id'],
119
+ },
120
+ {
121
+ desc: 'handles multiple slashes by splitting on the first slash only',
122
+ secrets: [{ _type: TYPES.TLS, id: 'ns/name/extra' }],
123
+ expected: ['name/extra'],
124
+ },
125
+ ])('$desc', ({ secrets, expected }) => {
126
+ const helper = makeHelper();
127
+
128
+ expect(helper.findAndMapCerts(secrets)).toStrictEqual(expected);
129
+ });
130
+ });
131
+
132
+ describe('findAndMapServiceTargets', () => {
133
+ it.each([
134
+ {
135
+ desc: 'returns empty array when given empty services list',
136
+ services: [],
137
+ expected: [],
138
+ },
139
+ {
140
+ desc: 'maps service to label, value, and ports',
141
+ services: [{
142
+ metadata: { name: 'my-service' },
143
+ spec: { ports: [{ port: 80 }, { port: 443 }] },
144
+ }],
145
+ expected: [{
146
+ label: 'my-service',
147
+ value: 'my-service',
148
+ ports: [80, 443],
149
+ }],
150
+ },
151
+ {
152
+ desc: 'includes port names alongside port numbers when available',
153
+ services: [{
154
+ metadata: { name: 'named-ports-service' },
155
+ spec: { ports: [{ port: 80, name: 'http' }, { port: 443, name: 'https' }] },
156
+ }],
157
+ expected: [{
158
+ label: 'named-ports-service',
159
+ value: 'named-ports-service',
160
+ ports: [80, 'http', 443, 'https'],
161
+ }],
162
+ },
163
+ {
164
+ desc: 'returns undefined ports when service has no spec.ports',
165
+ services: [{
166
+ metadata: { name: 'headless-service' },
167
+ spec: {},
168
+ }],
169
+ expected: [{
170
+ label: 'headless-service',
171
+ value: 'headless-service',
172
+ ports: undefined,
173
+ }],
174
+ },
175
+ {
176
+ desc: 'returns empty ports array when spec.ports is empty',
177
+ services: [{
178
+ metadata: { name: 'no-port-service' },
179
+ spec: { ports: [] },
180
+ }],
181
+ expected: [{
182
+ label: 'no-port-service',
183
+ value: 'no-port-service',
184
+ ports: [],
185
+ }],
186
+ },
187
+ {
188
+ desc: 'maps multiple services correctly',
189
+ services: [
190
+ {
191
+ metadata: { name: 'svc-a' },
192
+ spec: { ports: [{ port: 8080 }] },
193
+ },
194
+ {
195
+ metadata: { name: 'svc-b' },
196
+ spec: { ports: [{ port: 9090 }, { port: 9091 }] },
197
+ },
198
+ ],
199
+ expected: [
200
+ {
201
+ label: 'svc-a',
202
+ value: 'svc-a',
203
+ ports: [8080],
204
+ },
205
+ {
206
+ label: 'svc-b',
207
+ value: 'svc-b',
208
+ ports: [9090, 9091],
209
+ },
210
+ ],
211
+ },
212
+ {
213
+ desc: 'includes only port numbers when ports have no names',
214
+ services: [{
215
+ metadata: { name: 'unnamed-service' },
216
+ spec: { ports: [{ port: 3000 }] },
217
+ }],
218
+ expected: [{
219
+ label: 'unnamed-service',
220
+ value: 'unnamed-service',
221
+ ports: [3000],
222
+ }],
223
+ },
224
+ {
225
+ desc: 'skips empty-string port names',
226
+ services: [{
227
+ metadata: { name: 'empty-name-svc' },
228
+ spec: { ports: [{ port: 8080, name: '' }] },
229
+ }],
230
+ expected: [{
231
+ label: 'empty-name-svc',
232
+ value: 'empty-name-svc',
233
+ ports: [8080],
234
+ }],
235
+ },
236
+ {
237
+ desc: 'handles mix of named and unnamed ports on the same service',
238
+ services: [{
239
+ metadata: { name: 'mixed-svc' },
240
+ spec: { ports: [{ port: 80, name: 'http' }, { port: 9090 }] },
241
+ }],
242
+ expected: [{
243
+ label: 'mixed-svc',
244
+ value: 'mixed-svc',
245
+ ports: [80, 'http', 9090],
246
+ }],
247
+ },
248
+ ])('$desc', ({ services, expected }) => {
249
+ const helper = makeHelper();
250
+
251
+ expect(helper.findAndMapServiceTargets(services)).toStrictEqual(expected);
252
+ });
253
+ });
254
+
255
+ describe('parseServicePort', () => {
256
+ it.each([
257
+ {
258
+ desc: 'parses numeric string to number',
259
+ input: '80',
260
+ expected: 80,
261
+ },
262
+ {
263
+ desc: 'parses large port number',
264
+ input: '65535',
265
+ expected: 65535,
266
+ },
267
+ {
268
+ desc: 'keeps string port name as-is',
269
+ input: 'http',
270
+ expected: 'http',
271
+ },
272
+ {
273
+ desc: 'keeps string port name with hyphens',
274
+ input: 'my-port',
275
+ expected: 'my-port',
276
+ },
277
+ {
278
+ desc: 'handles zero as a number (not as falsy)',
279
+ input: '0',
280
+ expected: 0,
281
+ },
282
+ {
283
+ desc: 'handles already-numeric input',
284
+ input: 443 as any,
285
+ expected: 443,
286
+ },
287
+ {
288
+ desc: 'keeps empty string as empty string',
289
+ input: '',
290
+ expected: '',
291
+ },
292
+ ])('$desc', ({ input, expected }) => {
293
+ expect(parseServicePort(input)).toStrictEqual(expected);
294
+ });
295
+
296
+ it('routes numeric result to port.number path', () => {
297
+ const servicePort = parseServicePort('80');
298
+
299
+ expect(typeof servicePort).toBe('number');
300
+ });
301
+
302
+ it('routes string result to port.name path', () => {
303
+ const servicePort = parseServicePort('http');
304
+
305
+ expect(typeof servicePort).toBe('string');
306
+ });
307
+ });
308
+
309
+ describe('portRequired validation', () => {
310
+ describe('object input (form mixin)', () => {
311
+ it.each([
312
+ {
313
+ desc: 'passes when port.number is set',
314
+ port: { number: 80 },
315
+ },
316
+ {
317
+ desc: 'passes when port.name is set',
318
+ port: { name: 'http' },
319
+ },
320
+ {
321
+ desc: 'passes when both are set',
322
+ port: {
323
+ number: 80,
324
+ name: 'http',
325
+ },
326
+ },
327
+ {
328
+ desc: 'passes when port.number is 0 but name is set',
329
+ port: {
330
+ number: 0,
331
+ name: 'http',
332
+ },
333
+ },
334
+ ])('valid: $desc', ({ port }) => {
335
+ expect(portRequired(port)).toBeUndefined();
336
+ });
337
+
338
+ it.each([
339
+ {
340
+ desc: 'port is undefined',
341
+ port: undefined,
342
+ },
343
+ {
344
+ desc: 'port is null',
345
+ port: null,
346
+ },
347
+ {
348
+ desc: 'port is empty object',
349
+ port: {},
350
+ },
351
+ {
352
+ desc: 'port.number is 0 and no name',
353
+ port: { number: 0 },
354
+ },
355
+ {
356
+ desc: 'port.name is empty string and no number',
357
+ port: { name: '' },
358
+ },
359
+ ])('invalid: $desc', ({ port }) => {
360
+ expect(portRequired(port)).toBeDefined();
361
+ });
362
+ });
363
+
364
+ describe('scalar input (component)', () => {
365
+ it.each([
366
+ {
367
+ desc: 'passes for numeric port',
368
+ port: 80,
369
+ },
370
+ {
371
+ desc: 'passes for string port name',
372
+ port: 'http',
373
+ },
374
+ ])('valid: $desc', ({ port }) => {
375
+ expect(portRequired(port)).toBeUndefined();
376
+ });
377
+
378
+ it.each([
379
+ {
380
+ desc: 'empty string',
381
+ port: '',
382
+ },
383
+ {
384
+ desc: 'zero',
385
+ port: 0,
386
+ },
387
+ ])('invalid: $desc', ({ port }) => {
388
+ expect(portRequired(port)).toBeDefined();
389
+ });
390
+ });
391
+ });
392
+
393
+ describe('portRange validation', () => {
394
+ describe('object input (form mixin)', () => {
395
+ it.each([
396
+ {
397
+ desc: 'passes for port.number 1 (minimum)',
398
+ port: { number: 1 },
399
+ },
400
+ {
401
+ desc: 'passes for port.number 65535 (maximum)',
402
+ port: { number: 65535 },
403
+ },
404
+ {
405
+ desc: 'passes when only port.name is set',
406
+ port: { name: 'http' },
407
+ },
408
+ ])('valid: $desc', ({ port }) => {
409
+ expect(portRange(port)).toBeUndefined();
410
+ });
411
+
412
+ it.each([
413
+ {
414
+ desc: 'port.number exceeds 65535',
415
+ port: { number: 70000 },
416
+ },
417
+ {
418
+ desc: 'port.number is negative',
419
+ port: { number: -1 },
420
+ },
421
+ ])('invalid: $desc', ({ port }) => {
422
+ expect(portRange(port)).toBeDefined();
423
+ });
424
+ });
425
+
426
+ describe('scalar input (component)', () => {
427
+ it.each([
428
+ {
429
+ desc: 'passes for port 1 (minimum)',
430
+ port: 1,
431
+ },
432
+ {
433
+ desc: 'passes for port 65535 (maximum)',
434
+ port: 65535,
435
+ },
436
+ {
437
+ desc: 'passes for numeric string "443"',
438
+ port: '443',
439
+ },
440
+ {
441
+ desc: 'passes for string port name',
442
+ port: 'http',
443
+ },
444
+ ])('valid: $desc', ({ port }) => {
445
+ expect(portRange(port)).toBeUndefined();
446
+ });
447
+
448
+ it.each([
449
+ {
450
+ desc: 'number exceeds 65535',
451
+ port: 70000,
452
+ },
453
+ {
454
+ desc: 'negative number',
455
+ port: -1,
456
+ },
457
+ {
458
+ desc: 'string "70000" exceeds range',
459
+ port: '70000',
460
+ },
461
+ ])('invalid: $desc', ({ port }) => {
462
+ expect(portRange(port)).toBeDefined();
463
+ });
464
+ });
465
+ });
466
+
467
+ describe('willSave', () => {
468
+ const nestedPaths = {
469
+ defaultBackendPath: 'defaultBackend',
470
+ serviceNamePath: 'service.name',
471
+ servicePortPath: 'service.port.number',
472
+ servicePortNamePath: 'service.port.name',
473
+ };
474
+
475
+ it('preserves backend when port.number is set', () => {
476
+ const spec = { defaultBackend: { service: { name: 'my-svc', port: { number: 80 } } } };
477
+
478
+ willSave(spec, nestedPaths);
479
+ expect(spec.defaultBackend).toStrictEqual({ service: { name: 'my-svc', port: { number: 80 } } });
480
+ });
481
+
482
+ it('preserves backend when port.name is set', () => {
483
+ const spec = { defaultBackend: { service: { name: 'my-svc', port: { name: 'http' } } } };
484
+
485
+ willSave(spec, nestedPaths);
486
+ expect(spec.defaultBackend).toStrictEqual({ service: { name: 'my-svc', port: { name: 'http' } } });
487
+ });
488
+
489
+ it('clears backend when service name is empty', () => {
490
+ const spec = { defaultBackend: { service: { name: '', port: { number: 80 } } } };
491
+
492
+ willSave(spec, nestedPaths);
493
+ expect(spec.defaultBackend).toBeNull();
494
+ });
495
+
496
+ it('clears backend when both port paths are empty', () => {
497
+ const spec = { defaultBackend: { service: { name: 'my-svc', port: {} } } };
498
+
499
+ willSave(spec, nestedPaths);
500
+ expect(spec.defaultBackend).toBeNull();
501
+ });
502
+
503
+ it('clears backend when port is undefined', () => {
504
+ const spec = { defaultBackend: { service: { name: 'my-svc' } } };
505
+
506
+ willSave(spec, nestedPaths);
507
+ expect(spec.defaultBackend).toBeNull();
508
+ });
509
+
510
+ it('does nothing when there is no backend', () => {
511
+ const spec = { rules: [{}] } as any;
512
+
513
+ willSave(spec, nestedPaths);
514
+ expect(spec).toStrictEqual({ rules: [{}] });
515
+ });
516
+ });
517
+
518
+ describe('defaultBackendNameRequired', () => {
519
+ it('returns error when service is selected but name is empty', () => {
520
+ expect(defaultBackendNameRequired('', true)).toBeDefined();
521
+ });
522
+
523
+ it('passes when service is selected and name is set', () => {
524
+ expect(defaultBackendNameRequired('my-svc', true)).toBeUndefined();
525
+ });
526
+
527
+ it('passes when no service is selected and name is empty', () => {
528
+ expect(defaultBackendNameRequired('', false)).toBeUndefined();
529
+ });
530
+
531
+ it('passes when no service is selected and name is undefined', () => {
532
+ expect(defaultBackendNameRequired(undefined, false)).toBeUndefined();
533
+ });
534
+ });
535
+
536
+ describe('defaultBackendPortRequired', () => {
537
+ it('delegates to portRequired when service is selected', () => {
538
+ expect(defaultBackendPortRequired('', true)).toBeDefined();
539
+ expect(defaultBackendPortRequired(80, true)).toBeUndefined();
540
+ expect(defaultBackendPortRequired('http', true)).toBeUndefined();
541
+ expect(defaultBackendPortRequired({ number: 80 }, true)).toBeUndefined();
542
+ expect(defaultBackendPortRequired({ name: 'http' }, true)).toBeUndefined();
543
+ expect(defaultBackendPortRequired({}, true)).toBeDefined();
544
+ });
545
+
546
+ it('skips validation when no service is selected', () => {
547
+ expect(defaultBackendPortRequired('', false)).toBeUndefined();
548
+ expect(defaultBackendPortRequired(undefined, false)).toBeUndefined();
549
+ expect(defaultBackendPortRequired(null, false)).toBeUndefined();
550
+ expect(defaultBackendPortRequired({}, false)).toBeUndefined();
551
+ });
552
+ });
553
+ });
@@ -0,0 +1,68 @@
1
+ import { normalizeName } from '@shell/utils/kube';
2
+
3
+ describe('normalizeName', () => {
4
+ it.each([
5
+ {
6
+ desc: 'trims leading and trailing whitespace',
7
+ input: ' hello ',
8
+ expected: 'hello',
9
+ },
10
+ {
11
+ desc: 'converts uppercase to lowercase',
12
+ input: 'Hello World',
13
+ expected: 'hello-world',
14
+ },
15
+ {
16
+ desc: 'replaces spaces with hyphens',
17
+ input: 'my resource name',
18
+ expected: 'my-resource-name',
19
+ },
20
+ {
21
+ desc: 'collapses multiple spaces into a single hyphen',
22
+ input: 'foo bar',
23
+ expected: 'foo-bar',
24
+ },
25
+ {
26
+ desc: 'collapses consecutive hyphens into one',
27
+ input: 'foo--bar',
28
+ expected: 'foo-bar',
29
+ },
30
+ {
31
+ desc: 'strips leading hyphens',
32
+ input: '---foo',
33
+ expected: 'foo',
34
+ },
35
+ {
36
+ desc: 'strips trailing hyphens',
37
+ input: 'foo---',
38
+ expected: 'foo',
39
+ },
40
+ {
41
+ desc: 'handles empty string',
42
+ input: '',
43
+ expected: '',
44
+ },
45
+ {
46
+ desc: 'handles null by treating it as empty string',
47
+ input: null as unknown as string,
48
+ expected: '',
49
+ },
50
+ {
51
+ desc: 'handles undefined by treating it as empty string',
52
+ input: undefined as unknown as string,
53
+ expected: '',
54
+ },
55
+ {
56
+ desc: 'leaves already-normalized names unchanged',
57
+ input: 'my-resource',
58
+ expected: 'my-resource',
59
+ },
60
+ {
61
+ desc: 'handles mixed-case with spaces and leading/trailing hyphens',
62
+ input: ' -My Resource- ',
63
+ expected: 'my-resource',
64
+ },
65
+ ])('$desc', ({ input, expected }) => {
66
+ expect(normalizeName(input)).toStrictEqual(expected);
67
+ });
68
+ });