@rancher/shell 3.0.5-rc.6 → 3.0.5-rc.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 (243) hide show
  1. package/assets/brand/classic/metadata.json +3 -0
  2. package/assets/styles/app.scss +1 -0
  3. package/assets/styles/base/_color.scss +16 -0
  4. package/assets/styles/base/_helpers.scss +10 -0
  5. package/assets/styles/base/_variables.scss +18 -12
  6. package/assets/styles/fonts/_icons.scss +1 -32
  7. package/assets/styles/global/_layout.scss +1 -1
  8. package/assets/styles/themes/_dark.scss +262 -258
  9. package/assets/styles/themes/_light.scss +538 -509
  10. package/assets/styles/themes/_modern.scss +914 -0
  11. package/assets/translations/en-us.yaml +110 -29
  12. package/chart/__tests__/S3.test.ts +2 -1
  13. package/cloud-credential/generic.vue +18 -10
  14. package/cloud-credential/harvester.vue +1 -9
  15. package/components/AdvancedSection.vue +8 -0
  16. package/components/ChartReadme.vue +17 -7
  17. package/components/CodeMirror.vue +1 -1
  18. package/components/Drawer/Chrome.vue +0 -1
  19. package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +27 -28
  20. package/components/Drawer/ResourceDetailDrawer/composables.ts +4 -24
  21. package/components/Drawer/ResourceDetailDrawer/index.vue +18 -4
  22. package/components/InstallHelmCharts.vue +656 -0
  23. package/components/LazyImage.vue +60 -4
  24. package/components/Loading.vue +1 -1
  25. package/components/LocaleSelector.vue +7 -2
  26. package/components/Markdown.vue +4 -0
  27. package/components/PaginatedResourceTable.vue +46 -1
  28. package/components/PromptRestore.vue +22 -44
  29. package/components/Resource/Detail/Masthead/composable.ts +16 -0
  30. package/components/Resource/Detail/Masthead/index.vue +37 -0
  31. package/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts +10 -2
  32. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +26 -7
  33. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +8 -1
  34. package/components/Resource/Detail/Metadata/KeyValue.vue +12 -10
  35. package/components/Resource/Detail/Metadata/Rectangle.vue +3 -1
  36. package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +10 -17
  37. package/components/Resource/Detail/Metadata/composables.ts +9 -7
  38. package/components/Resource/Detail/Metadata/index.vue +17 -2
  39. package/components/Resource/Detail/Page.vue +35 -21
  40. package/components/Resource/Detail/SpacedRow.vue +1 -1
  41. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +8 -9
  42. package/components/Resource/Detail/TitleBar/composables.ts +5 -5
  43. package/components/Resource/Detail/TitleBar/index.vue +12 -3
  44. package/components/ResourceDetail/Masthead/legacy.vue +1 -1
  45. package/components/ResourceDetail/index.vue +569 -72
  46. package/components/ResourceList/index.vue +1 -0
  47. package/components/ResourceTable.vue +6 -1
  48. package/components/ResourceYaml.vue +1 -1
  49. package/components/RichTranslation.vue +106 -0
  50. package/components/SlideInPanelManager.vue +13 -10
  51. package/components/SortableTable/index.vue +5 -5
  52. package/components/SortableTable/selection.js +0 -1
  53. package/components/Tabbed/index.vue +35 -4
  54. package/components/__tests__/LazyImage.spec.ts +121 -0
  55. package/components/__tests__/PromptRestore.test.ts +1 -65
  56. package/components/__tests__/RichTranslation.test.ts +115 -0
  57. package/components/fleet/FleetStatus.vue +4 -0
  58. package/components/fleet/dashboard/ResourcePanel.vue +2 -1
  59. package/components/form/ClusterAppearance.vue +5 -0
  60. package/components/form/FileImageSelector.vue +1 -1
  61. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  62. package/components/form/NameNsDescription.vue +1 -0
  63. package/components/form/Networking.vue +24 -19
  64. package/components/form/ProjectMemberEditor.vue +1 -1
  65. package/components/form/ResourceLabeledSelect.vue +22 -8
  66. package/components/form/ResourceTabs/index.vue +20 -0
  67. package/components/form/SecretSelector.vue +9 -0
  68. package/components/form/SelectOrCreateAuthSecret.vue +6 -3
  69. package/components/form/__tests__/Networking.test.ts +116 -0
  70. package/components/form/labeled-select-utils/labeled-select-pagination.ts +3 -38
  71. package/components/formatter/FleetApplicationSource.vue +25 -17
  72. package/components/formatter/PodImages.vue +1 -1
  73. package/components/formatter/__tests__/LiveDate.test.ts +10 -2
  74. package/components/google/AccountAccess.vue +44 -46
  75. package/components/nav/Favorite.vue +4 -0
  76. package/components/nav/Group.vue +4 -1
  77. package/components/nav/NotificationCenter/Notification.vue +1 -27
  78. package/components/nav/WindowManager/index.vue +3 -3
  79. package/composables/resources.ts +2 -2
  80. package/config/labels-annotations.js +3 -2
  81. package/config/pagination-table-headers.js +8 -1
  82. package/config/product/explorer.js +27 -2
  83. package/config/product/manager.js +0 -1
  84. package/config/query-params.js +10 -0
  85. package/config/router/routes.js +21 -1
  86. package/config/system-namespaces.js +1 -1
  87. package/config/table-headers.js +30 -1
  88. package/config/types.js +1 -1
  89. package/config/version.js +1 -1
  90. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +11 -0
  91. package/detail/__tests__/workload.test.ts +164 -0
  92. package/detail/configmap.vue +33 -75
  93. package/detail/projectsecret.vue +11 -0
  94. package/detail/provisioning.cattle.io.cluster.vue +351 -369
  95. package/detail/secret.vue +49 -308
  96. package/detail/workload/index.vue +38 -21
  97. package/dialog/InstallExtensionDialog.vue +8 -5
  98. package/dialog/RotateEncryptionKeyDialog.vue +10 -30
  99. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
  100. package/edit/auth/ldap/__tests__/config.test.ts +14 -0
  101. package/edit/auth/ldap/config.vue +24 -0
  102. package/edit/compliance.cattle.io.clusterscan.vue +1 -1
  103. package/edit/configmap.vue +4 -1
  104. package/edit/fleet.cattle.io.gitrepo.vue +5 -6
  105. package/edit/fleet.cattle.io.helmop.vue +78 -56
  106. package/edit/logging.banzaicloud.io.output/index.vue +1 -1
  107. package/edit/logging.banzaicloud.io.output/providers/awsElasticsearch.vue +5 -6
  108. package/edit/networking.k8s.io.ingress/Certificate.vue +20 -22
  109. package/edit/networking.k8s.io.ingress/DefaultBackend.vue +8 -3
  110. package/edit/networking.k8s.io.ingress/Rule.vue +2 -5
  111. package/edit/networking.k8s.io.ingress/RulePath.vue +17 -11
  112. package/edit/networking.k8s.io.ingress/__tests__/Certificate.test.ts +165 -0
  113. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +11 -10
  114. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -3
  115. package/edit/networking.k8s.io.networkpolicy/index.vue +17 -17
  116. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +3 -2
  117. package/edit/provisioning.cattle.io.cluster/rke2.vue +123 -61
  118. package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +9 -7
  119. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +22 -13
  120. package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +10 -12
  121. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +39 -38
  122. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +41 -19
  123. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +16 -3
  124. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +32 -33
  125. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryMirrors.vue +9 -10
  126. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +1 -3
  127. package/edit/provisioning.cattle.io.cluster/tabs/upgrade/DrainOptions.vue +16 -9
  128. package/edit/secret/basic.vue +1 -0
  129. package/edit/secret/index.vue +126 -15
  130. package/edit/workload/index.vue +5 -14
  131. package/list/projectsecret.vue +345 -0
  132. package/list/provisioning.cattle.io.cluster.vue +1 -69
  133. package/list/secret.vue +109 -0
  134. package/machine-config/__tests__/vmwarevsphere.test.ts +5 -7
  135. package/machine-config/google.vue +9 -1
  136. package/machine-config/vmwarevsphere.vue +7 -17
  137. package/mixins/__tests__/brand.spec.ts +2 -2
  138. package/mixins/chart.js +0 -2
  139. package/mixins/create-edit-view/impl.js +10 -1
  140. package/mixins/resource-fetch-api-pagination.js +11 -12
  141. package/mixins/resource-fetch.js +3 -1
  142. package/models/__tests__/chart.test.ts +111 -80
  143. package/models/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
  144. package/models/__tests__/node.test.ts +7 -63
  145. package/models/catalog.cattle.io.app.js +1 -1
  146. package/models/catalog.cattle.io.operation.js +1 -1
  147. package/models/chart.js +36 -20
  148. package/models/cloudcredential.js +2 -163
  149. package/models/cluster/node.js +7 -7
  150. package/models/cluster.x-k8s.io.machine.js +3 -3
  151. package/models/cluster.x-k8s.io.machinedeployment.js +11 -2
  152. package/models/compliance.cattle.io.clusterscan.js +2 -2
  153. package/models/configmap.js +4 -0
  154. package/models/constraints.gatekeeper.sh.constraint.js +1 -1
  155. package/models/fleet-application.js +0 -17
  156. package/models/fleet.cattle.io.cluster.js +2 -2
  157. package/models/fleet.cattle.io.gitrepo.js +15 -1
  158. package/models/fleet.cattle.io.helmop.js +26 -22
  159. package/models/management.cattle.io.setting.js +4 -0
  160. package/models/persistentvolumeclaim.js +1 -1
  161. package/models/pod.js +2 -2
  162. package/models/provisioning.cattle.io.cluster.js +39 -67
  163. package/models/rke.cattle.io.etcdsnapshot.js +1 -1
  164. package/models/secret.js +161 -2
  165. package/models/storage.k8s.io.storageclass.js +2 -2
  166. package/models/workload.js +3 -3
  167. package/package.json +11 -10
  168. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +1 -0
  169. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +4 -1
  170. package/pages/c/_cluster/apps/charts/__tests__/AppChartCardFooter.spec.js +41 -0
  171. package/pages/c/_cluster/apps/charts/chart.vue +422 -174
  172. package/pages/c/_cluster/apps/charts/index.vue +46 -35
  173. package/pages/c/_cluster/apps/charts/install.vue +1 -1
  174. package/pages/c/_cluster/explorer/projectsecret.vue +24 -0
  175. package/pages/c/_cluster/fleet/__tests__/index.test.ts +608 -314
  176. package/pages/c/_cluster/fleet/index.vue +103 -45
  177. package/pages/c/_cluster/manager/cloudCredential/index.vue +2 -59
  178. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +10 -3
  179. package/pages/c/_cluster/uiplugins/index.vue +36 -25
  180. package/plugins/dashboard-store/__tests__/normalize.test.ts +223 -0
  181. package/plugins/dashboard-store/__tests__/resource-class.test.ts +191 -0
  182. package/plugins/dashboard-store/__tests__/utils/normalize-usecases.ts +1526 -0
  183. package/plugins/dashboard-store/actions.js +42 -22
  184. package/plugins/dashboard-store/normalize.js +29 -17
  185. package/plugins/dashboard-store/resource-class.js +83 -17
  186. package/plugins/steve/__tests__/getters.test.ts +1 -1
  187. package/plugins/steve/__tests__/subscribe.spec.ts +259 -1
  188. package/plugins/steve/getters.js +8 -2
  189. package/plugins/steve/resourceWatcher.js +10 -3
  190. package/plugins/steve/steve-pagination-utils.ts +14 -3
  191. package/plugins/steve/subscribe.js +192 -19
  192. package/plugins/steve/worker/web-worker.advanced.js +2 -0
  193. package/rancher-components/Card/Card.vue +0 -18
  194. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.test.ts +15 -0
  195. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +65 -0
  196. package/rancher-components/Pill/RcStatusBadge/index.ts +2 -0
  197. package/rancher-components/Pill/RcStatusBadge/types.ts +5 -0
  198. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.test.ts +33 -0
  199. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +75 -0
  200. package/rancher-components/Pill/RcStatusIndicator/index.ts +2 -0
  201. package/rancher-components/Pill/RcStatusIndicator/types.ts +7 -0
  202. package/rancher-components/Pill/types.ts +2 -0
  203. package/rancher-components/RcButton/RcButton.vue +1 -1
  204. package/rancher-components/RcDropdown/RcDropdown.test.ts +98 -0
  205. package/rancher-components/RcDropdown/RcDropdown.vue +5 -0
  206. package/rancher-components/RcDropdown/RcDropdownItem.vue +7 -1
  207. package/rancher-components/RcDropdown/RcDropdownItemCheckbox.vue +2 -1
  208. package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +2 -1
  209. package/rancher-components/RcDropdown/useDropdownContext.ts +21 -0
  210. package/rancher-components/RcDropdown/useDropdownItem.ts +30 -1
  211. package/rancher-components/RcItemCard/RcItemCard.test.ts +20 -0
  212. package/rancher-components/RcItemCard/RcItemCard.vue +40 -6
  213. package/store/__tests__/catalog.test.ts +93 -1
  214. package/store/aws.js +19 -8
  215. package/store/catalog.js +8 -3
  216. package/types/kube/kube-api.ts +12 -0
  217. package/types/resources/settings.d.ts +1 -1
  218. package/types/shell/index.d.ts +643 -585
  219. package/types/store/pagination.types.ts +16 -6
  220. package/types/uiplugins.ts +73 -0
  221. package/utils/__tests__/back-off.test.ts +354 -0
  222. package/utils/__tests__/create-yaml.test.ts +235 -0
  223. package/utils/__tests__/kontainer.test.ts +19 -0
  224. package/utils/__tests__/uiplugins.test.ts +84 -0
  225. package/utils/back-off.ts +176 -0
  226. package/utils/create-yaml.js +103 -9
  227. package/utils/dynamic-importer.js +8 -0
  228. package/utils/kontainer.ts +3 -5
  229. package/utils/pagination-utils.ts +18 -0
  230. package/utils/style.ts +3 -0
  231. package/utils/uiplugins.ts +29 -2
  232. package/utils/validators/__tests__/setting.test.js +92 -0
  233. package/utils/validators/formRules/__tests__/index.test.ts +88 -7
  234. package/utils/validators/formRules/index.ts +83 -8
  235. package/utils/validators/setting.js +17 -0
  236. package/cloud-credential/__tests__/harvester.test.ts +0 -18
  237. package/components/ResourceDetail/__tests__/index.test.ts +0 -135
  238. package/components/ResourceDetail/legacy.vue +0 -562
  239. package/components/formatter/CloudCredExpired.vue +0 -69
  240. package/models/etcdbackup.js +0 -45
  241. package/pages/explorer/resource/detail/configmap.vue +0 -42
  242. package/pages/explorer/resource/detail/secret.vue +0 -50
  243. package/utils/aws.js +0 -0
@@ -68,6 +68,14 @@ export function importDialog(name) {
68
68
  return defineAsyncComponent(() => import(/* webpackChunkName: "dialog" */ `@shell/dialog/${name}`));
69
69
  }
70
70
 
71
+ export function importDrawer(name) {
72
+ if ( !name ) {
73
+ throw new Error('Name required');
74
+ }
75
+
76
+ return defineAsyncComponent(() => import(/* webpackChunkName: "drawer" */ `@shell/components/Drawer/${name}`));
77
+ }
78
+
71
79
  export function importWindowComponent(name) {
72
80
  if ( !name ) {
73
81
  throw new Error('Name required');
@@ -17,11 +17,9 @@ export function syncUpstreamConfig(configPrefix: string, normanCluster: {[key: s
17
17
 
18
18
  if (!isEmpty(upstreamConfig)) {
19
19
  Object.keys(upstreamConfig).forEach((key) => {
20
- if (typeof upstreamConfig[key] === 'object') {
21
- if (isEmpty(rancherConfig[key]) && !isEmpty(upstreamConfig[key])) {
22
- set(rancherConfig, key, upstreamConfig[key]);
23
- }
24
- } else if ((rancherConfig[key] === null || rancherConfig[key] === undefined) && upstreamConfig[key] !== null && upstreamConfig[key] !== undefined) {
20
+ const empty = typeof upstreamConfig[key] === 'object' && isEmpty(upstreamConfig[key]);
21
+
22
+ if ((rancherConfig[key] === null || rancherConfig[key] === undefined) && upstreamConfig[key] !== null && upstreamConfig[key] !== undefined && !empty) {
25
23
  set(rancherConfig, key, upstreamConfig[key]);
26
24
  }
27
25
  });
@@ -15,6 +15,7 @@ import { isEqual } from '@shell/utils/object';
15
15
  import { STEVE_CACHE } from '@shell/store/features';
16
16
  import { getPerformanceSetting } from '@shell/utils/settings';
17
17
  import { PAGINATION_SETTINGS_STORE_DEFAULTS } from '@shell/plugins/steve/steve-pagination-utils';
18
+ import { MANAGEMENT } from '@shell/config/types';
18
19
 
19
20
  /**
20
21
  * Helper functions for server side pagination
@@ -54,6 +55,23 @@ class PaginationUtils {
54
55
  return rootGetters['features/get']?.(STEVE_CACHE);
55
56
  }
56
57
 
58
+ /**
59
+ * Determine if the downstream cluster has vai enabled
60
+ *
61
+ * Almost all the time the downstream cluster vai state will align with upstream (it manages it)
62
+ * ... unless it's harvester then weird things happen
63
+ */
64
+ async isDownstreamSteveCacheEnabled({ dispatch }: any, clusterId: string): Promise<boolean> {
65
+ const url = `/k8s/clusters/${ clusterId }/v1/${ MANAGEMENT.FEATURE }s/${ STEVE_CACHE }`;
66
+ const entry = await dispatch('cluster/request', { url });
67
+
68
+ if (entry.status.lockedValue !== null) {
69
+ return entry.status.lockedValue;
70
+ }
71
+
72
+ return (entry.spec.value !== null) ? entry.spec.value : entry.status.default;
73
+ }
74
+
57
75
  /**
58
76
  * Is pagination enabled at a global level or for a specific resource
59
77
  */
package/utils/style.ts CHANGED
@@ -37,3 +37,6 @@ export function getHighestAlertColor(colors: StateColor[]) {
37
37
 
38
38
  return highestAlert;
39
39
  }
40
+
41
+ // 1x1 transparent image as a placeholder image
42
+ export const BLANK_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
@@ -1,7 +1,8 @@
1
1
  import { matchesSomeRegex } from '@shell/utils/string';
2
2
  import { CATALOG as CATALOG_ANNOTATIONS } from '@shell/config/labels-annotations';
3
3
  import { CATALOG } from '@shell/config/types';
4
- import { UI_PLUGIN_BASE_URL, isSupportedChartVersion } from '@shell/config/uiplugins';
4
+ import { UI_PLUGIN_BASE_URL, isSupportedChartVersion, UI_PLUGIN_LABELS } from '@shell/config/uiplugins';
5
+ import { Plugin, Version } from '@shell/types/uiplugins';
5
6
 
6
7
  const MAX_RETRIES = 10;
7
8
  const RETRY_WAIT = 2500;
@@ -180,10 +181,17 @@ export async function getHelmRepositoryExact(store: any, url: string): Promise<H
180
181
  *
181
182
  * @param store Vue store
182
183
  * @param urlRegexes Regex to match a community repository
184
+ * @param catalogImages Catalog images to match against the repository's labels
183
185
  * @returns HelmRepository
184
186
  */
185
- export async function getHelmRepositoryMatch(store: any, urlRegexes: string[]): Promise<HelmRepository> {
187
+ export async function getHelmRepositoryMatch(store: any, urlRegexes: string[], catalogImages: string[]): Promise<HelmRepository> {
186
188
  return await getHelmRepository(store, (repository: any) => {
189
+ // if installed from rancher/ui-plugin-catalog or rancher/ui-extension-harvester-ui-extension
190
+ const catalog = repository?.metadata?.labels?.[UI_PLUGIN_LABELS.CATALOG_IMAGE] || '';
191
+
192
+ if (catalogImages.includes(catalog)) {
193
+ return true;
194
+ }
187
195
  const target = repository.spec?.gitBranch ? repository.spec?.gitRepo : repository.spec?.url;
188
196
 
189
197
  return matchesSomeRegex(target, urlRegexes);
@@ -343,3 +351,22 @@ export async function onExtensionsReady(store: any) {
343
351
 
344
352
  await store.dispatch('uiplugins/setReady', true);
345
353
  }
354
+
355
+ /**
356
+ * Finds a Helm Chart version which matches plugin displayVersion. First it checks against Chart.appVersion and
357
+ * falls back to Chart.version if appVersion is not present.
358
+ *
359
+ * @param plugin A data object constructed from UIPlugin and Helm Chart versions
360
+ * @returns string Helm Chart version
361
+ */
362
+ export function getPluginChartVersion(plugin?: Plugin) {
363
+ const pluginVersion = plugin?.displayVersion;
364
+
365
+ return plugin?.versions?.find((v) => pluginVersion === (v.appVersion ?? v.version))?.version ?? pluginVersion;
366
+ }
367
+
368
+ export function getPluginChartVersionLabel(version: Version) {
369
+ if (version.appVersion === version.version) return `${ version.version }`;
370
+
371
+ return `${ version.appVersion } (${ version.version })`;
372
+ }
@@ -0,0 +1,92 @@
1
+ import {
2
+ isServerUrl,
3
+ isHttps,
4
+ isDomainWithoutProtocol,
5
+ isLocalhost,
6
+ hasTrailingForwardSlash,
7
+ } from '@shell/utils/validators/setting';
8
+
9
+ describe('isServerUrl', () => {
10
+ it.each([
11
+ ['server-url', true],
12
+ ['SERVER-URL', false],
13
+ ['server-url/', false],
14
+ ['not-server-url', false],
15
+ ['', false],
16
+ ])('should validate that isServerUrl("%s") returns %s', (input, expected) => {
17
+ expect(isServerUrl(input)).toBe(expected);
18
+ });
19
+ });
20
+
21
+ describe('isHttps', () => {
22
+ it.each([
23
+ ['https://example.com', true],
24
+ ['HTTPS://EXAMPLE.COM', true],
25
+ ['http://example.com', false],
26
+ ['ftp://example.com', false],
27
+ ['example.com', false],
28
+ ['', false],
29
+ ])('should validate that isHttps("%s") returns %s', (input, expected) => {
30
+ expect(isHttps(input)).toBe(expected);
31
+ });
32
+ });
33
+
34
+ describe('isDomainWithoutProtocol (follows domain format, no protocol)', () => {
35
+ it.each([
36
+ ['ec2.us-west-2.amazonaws.com', true],
37
+ ['ec2.us-west-2.api.aws', true],
38
+ ['ec2.us-west-2.amazonaws.com.cn', true],
39
+ ['s3.eu-central-1.amazonaws.com.cn', true],
40
+ ['my-service.internal.net', true],
41
+ ['db.example.org:5432', true],
42
+ ['ex.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.com', true],
43
+ ['service.company.local/path/to/resource', true],
44
+ ['example.org:443', true],
45
+ ['https://ec2.us-west-2.amazonaws.com', false],
46
+ ['http://example.com', false],
47
+ ['udp://example.com', false],
48
+ ['ftp://example.com', false],
49
+ ['ftps://example.com', false],
50
+ ['example://example.com', false],
51
+ ['example', false],
52
+ ['-bad.example.com', false],
53
+ ['bad-.example.com', false],
54
+ ['example.c', false],
55
+ ['exa mple.com', false],
56
+ ['exa.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.com', false],
57
+ ['', false],
58
+ ])('should validate that isDomainWithoutProtocol("%s") returns %s', (input, expected) => {
59
+ expect(isDomainWithoutProtocol(input)).toBe(expected);
60
+ });
61
+ });
62
+
63
+ describe('isLocalhost', () => {
64
+ it.each([
65
+ ['localhost', true],
66
+ ['LOCALHOST', true],
67
+ ['http://localhost', true],
68
+ ['https://localhost:3000', true],
69
+ ['127.0.0.1', true],
70
+ ['http://127.0.0.1:8080/path', true],
71
+ ['HTTPS://127.0.0.1/Health', true],
72
+ ['127.0.0.2', false],
73
+ ['http://127.0.0.2', false],
74
+ ['mylocalhost', false],
75
+ ])('should validate that isLocalhost("%s") returns %s', (input, expected) => {
76
+ expect(isLocalhost(input)).toBe(expected);
77
+ });
78
+ });
79
+
80
+ describe('hasTrailingForwardSlash', () => {
81
+ it.each([
82
+ ['https://example.com/', true],
83
+ ['http://example.com/', true],
84
+ ['HTTPS://EXAMPLE.COM/', true],
85
+ ['https://example.com/path/', true],
86
+ ['https://example.com', false],
87
+ ['http://example.com/path', false],
88
+ ['example.com/', false],
89
+ ])('should validate that hasTrailingForwardSlash("%s") returns %s', (input, expected) => {
90
+ expect(hasTrailingForwardSlash(input)).toBe(expected);
91
+ });
92
+ });
@@ -97,32 +97,54 @@ describe('formRules', () => {
97
97
  });
98
98
 
99
99
  describe('urlRepository', () => {
100
- const message = JSON.stringify({ message: 'validation.git.url' });
100
+ const message = JSON.stringify({ message: 'validation.repository.url' });
101
101
  const testCases = [
102
102
  // Valid HTTP(s)
103
103
  ['https://github.com/rancher/dashboard.git', undefined],
104
104
  ['http://github.com/rancher/dashboard.git', undefined],
105
105
  ['https://github.com/rancher/dashboard', undefined],
106
106
  ['https://github.com/rancher/dashboard/', undefined],
107
+ ['https://github.com/rancher/%20dashboard/', undefined],
108
+ ['https://github.com/rancher/dashboard/%20', undefined],
109
+ ['https://localhost:8005', undefined],
107
110
 
108
111
  // Valid SSH
109
112
  ['git@github.com:rancher/dashboard.git', undefined],
110
113
  ['git@github.com:rancher/dashboard', undefined],
111
114
  ['git@github.com:rancher/dashboard/', undefined],
115
+ ['git@github.com:rancher/%20dashboard/', undefined],
116
+ ['git@github.com:rancher/dashboard/%20', undefined],
112
117
 
113
118
  // Not valid HTTP(s)
114
119
  ['https://github.com/rancher/ dashboard.git', message],
115
120
  ['http://github.com/rancher/ dashboard.git', message],
121
+ ['http://github.com/ rancher/dashboard.git', message],
122
+ ['http://github.com /rancher/dashboard.git', message],
116
123
  ['https://github.com/rancher/dashboard ', message],
124
+ ['https%20://github.com/rancher/dashboard ', message],
125
+ ['ht%20tps://github.com/rancher/dashboard ', message],
126
+ ['https://git%20hub.com/rancher/dashboard/%20', message],
127
+ ['https://https://', message],
128
+ ['http:/ww.abc.com', message],
129
+ ['http:ww.abc.com', message],
117
130
  ['foo://github.com/rancher/dashboard/', message],
118
131
  ['github.com/rancher/dashboard/', message],
119
132
 
120
133
  // Not valid SSH
121
134
  ['git@github.com:rancher/ dashboard.git', message],
122
135
  ['git@github.com:rancher/dashboard ', message],
136
+ ['git@github.com:rancher/ dashboard', message],
137
+ ['git @github.com:rancher/dashboard', message],
138
+ ['git@github.com: rancher/dashboard', message],
123
139
  ['git@github.comrancher/dashboard', message],
140
+ ['git@githubcomrancher/dashboard', message],
141
+ ['%20git@github.comrancher/dashboard', message],
142
+ ['git@git%20hub.comrancher/dashboard', message],
143
+ ['git@.git', message],
144
+ ['git@', message],
124
145
 
125
- [undefined, undefined]
146
+ [undefined, message],
147
+ ['', message]
126
148
  ];
127
149
 
128
150
  it.each(testCases)(
@@ -139,20 +161,26 @@ describe('formRules', () => {
139
161
  const message = JSON.stringify({ message: 'validation.oci.url' });
140
162
  const testCases = [
141
163
  // Valid
142
- ['oci://bucket/object', undefined],
164
+ ['oci://registry.example.com', undefined],
165
+ ['oci://myregistry.dev:5000', undefined],
166
+ ['oci://192.168.1.100', undefined],
167
+ ['oci://my.domain.com/my/image:tag', undefined],
168
+ ['oci://localhost:5000', undefined],
143
169
  ['oci://region.objectstorage.example.com/n', undefined],
144
- ['oci://a', undefined],
145
- ['oci://UPPERCASE/path', undefined],
146
170
 
147
171
  // Invalid
148
172
  ['http://example.com/oci', message],
149
173
  ['https://oci.cloud.com', message],
150
174
  ['ftp://oci.server.net', message],
151
- ['/path/to/oci', message],
175
+ ['path/to/oci', message],
176
+ ['oci://a', message],
152
177
  ['oci:/missing/slash', message],
153
178
  ['oci:', message],
154
179
  ['oci://', message],
155
- ['oci://space between', message],
180
+ ['oci://oci://duplicate/protocol', message],
181
+ ['oci ://registry.example.com/foo/bar', message],
182
+ ['oci://registry.example. com/foo/bar', message],
183
+ ['oci://registry.example.com/ foo/bar', message],
156
184
  ['oci://resource multiple spaces', message],
157
185
  ['', message],
158
186
  [undefined, message],
@@ -168,6 +196,59 @@ describe('formRules', () => {
168
196
  );
169
197
  });
170
198
 
199
+ describe('version', () => {
200
+ const message = JSON.stringify({ message: 'validation.version' });
201
+ const testCases: (null | string | undefined)[][] = [
202
+ // Valid
203
+ ['1.2.3', undefined],
204
+ ['', undefined],
205
+ [null, undefined],
206
+
207
+ // Invalid
208
+ ['1.2.x', message],
209
+ ['foo', message],
210
+ ['1.2', message],
211
+ ['1.2.', message],
212
+ ['.', message],
213
+ ];
214
+
215
+ it.each(testCases)(
216
+ 'should return undefined or correct message based on the provided Version: %p',
217
+ (version, expected) => {
218
+ const formRuleResult = formRules.version(version);
219
+
220
+ expect(formRuleResult).toStrictEqual(expected);
221
+ }
222
+ );
223
+ });
224
+
225
+ describe('semanticVersion', () => {
226
+ const message = JSON.stringify({ message: 'validation.semanticVersion' });
227
+ const testCases: (null | string | undefined)[][] = [
228
+ // Valid
229
+ ['1.2.x', undefined],
230
+ ['1.2.3', undefined],
231
+ ['1.2', undefined],
232
+ ['> 1', undefined],
233
+ ['', undefined],
234
+ [null, undefined],
235
+
236
+ // Invalid
237
+ ['foo', message],
238
+ ['1.2.', message],
239
+ ['.', message],
240
+ ];
241
+
242
+ it.each(testCases)(
243
+ 'should return undefined or correct message based on the provided Semantic Version: %p',
244
+ (version, expected) => {
245
+ const formRuleResult = formRules.semanticVersion(version);
246
+
247
+ expect(formRuleResult).toStrictEqual(expected);
248
+ }
249
+ );
250
+ });
251
+
171
252
  describe('alphanumeric', () => {
172
253
  const message = JSON.stringify({ message: 'validation.alphanumeric', key: 'testDisplayKey' });
173
254
  const testCases = [
@@ -1,3 +1,5 @@
1
+ import semver from 'semver';
2
+ import { parse } from '@shell/utils/url';
1
3
  import { RBAC } from '@shell/config/types';
2
4
  import { HCI } from '@shell/config/labels-annotations';
3
5
  import isEmpty from 'lodash/isEmpty';
@@ -5,7 +7,7 @@ import has from 'lodash/has';
5
7
  import isUrl from 'is-url';
6
8
  // import uniq from 'lodash/uniq';
7
9
  import { Translation } from '@shell/types/t';
8
- import { isHttps, isLocalhost, hasTrailingForwardSlash } from '@shell/utils/validators/setting';
10
+ import { isHttps, isLocalhost, hasTrailingForwardSlash, isDomainWithoutProtocol } from '@shell/utils/validators/setting';
9
11
  import { cronScheduleRule } from '@shell/utils/validators/cron-schedule';
10
12
 
11
13
  // import uniq from 'lodash/uniq';
@@ -164,6 +166,8 @@ export default function(
164
166
 
165
167
  const https: Validator = (val: string) => val && !isHttps(val) ? t('validation.setting.serverUrl.https') : undefined;
166
168
 
169
+ const awsStyleEndpoint: Validator = (val: string) => val && !isDomainWithoutProtocol(val) ? t('validation.setting.serverUrl.awsStyleEndpoint') : undefined;
170
+
167
171
  const localhost: Validator = (val: string) => isLocalhost(val) ? t('validation.setting.serverUrl.localhost') : undefined;
168
172
 
169
173
  const trailingForwardSlash: Validator = (val: string) => hasTrailingForwardSlash(val) ? t('validation.setting.serverUrl.trailingForwardSlash') : undefined;
@@ -173,22 +177,90 @@ export default function(
173
177
  const genericUrl: Validator = (val: string) => val && !isUrl(val) ? t('validation.genericUrl') : undefined;
174
178
 
175
179
  const urlRepository: Validator = (url: string) => {
176
- const regexPart1 = /^((http|git|ssh|http(s)|file|\/?)|(git@[\w\.]+))(:(\/\/)?)/gm;
177
- const regexPart2 = /^([\w\.@\:\/\-]+)([\d\/\w.-]+?)(.git){0,1}(\/)?$/gm;
180
+ const message = t('validation.repository.url');
181
+
182
+ if (!url) {
183
+ return message;
184
+ }
185
+
186
+ if (url.includes(' ')) {
187
+ return message;
188
+ }
189
+
190
+ const {
191
+ protocol,
192
+ authority,
193
+ host,
194
+ path
195
+ } = parse(url);
196
+
197
+ // Test duplicate protocol
198
+ if (!host || protocol === host) {
199
+ return message;
200
+ }
201
+
202
+ // Test http(s) protocol
203
+ if (protocol && (!/^(http|http(s))/gm.test(protocol) || (!url.startsWith('https://') && !url.startsWith('http://')))) {
204
+ return message;
205
+ }
206
+
207
+ // Test ssh, authority must be valid (SSH user + host)
208
+ if (!protocol && !authority.endsWith(':')) {
209
+ return message;
210
+ }
178
211
 
179
- if (url) {
180
- const urlPart2 = url.replaceAll(regexPart1, '');
212
+ // Encoded space characters (%20) are allowed only in the path
213
+ const hostAndPath = `${ host }${ path.replaceAll('%20', '') }`;
181
214
 
182
- return !urlPart2 || url === urlPart2 || !regexPart2.test(urlPart2.replaceAll('%20', '')) ? t('validation.git.url') : undefined;
215
+ // Test host/path
216
+ if (!/^([\w\.@\:\/\-]+)([\d\/\w.-]+?)(.git){0,1}(\/)?$/gm.test(hostAndPath)) {
217
+ return message;
183
218
  }
184
219
 
185
220
  return undefined;
186
221
  };
187
222
 
188
223
  const ociRegistry: Validator = (url: string) => {
189
- const regex = /^oci:\/\/\S+$/gm;
224
+ const message = t('validation.oci.url');
225
+
226
+ if (!url) {
227
+ return message;
228
+ }
229
+
230
+ if (url.includes(' ')) {
231
+ return message;
232
+ }
233
+
234
+ const {
235
+ protocol,
236
+ host,
237
+ path
238
+ } = parse(url);
239
+
240
+ // Test duplicate protocol
241
+ if (!host || protocol === host) {
242
+ return message;
243
+ }
244
+
245
+ // Test oci protocol
246
+ if (!url.startsWith('oci://')) {
247
+ return message;
248
+ }
249
+
250
+ // Test host/path
251
+ if (!/^([\w\.@\:\/\-]+)([\d\/\w.-]+?)(\/)?$/gm.test(`${ host }${ path }`)) {
252
+ return message;
253
+ }
254
+
255
+ return undefined;
256
+ };
257
+
258
+ const version: Validator = (value: string) => {
259
+ return value && !semver.valid(value) ? t('validation.version') : undefined;
260
+ };
190
261
 
191
- return !regex.test(url) ? t('validation.oci.url') : undefined;
262
+ const semanticVersion: Validator = (value: string) => {
263
+ return value && !semver.validRange(value) ? t('validation.semanticVersion') : undefined;
192
264
  };
193
265
 
194
266
  const alphanumeric: Validator = (val: string) => val && !/^[a-zA-Z0-9]+$/.test(val) ? t('validation.alphanumeric', { key }) : undefined;
@@ -541,6 +613,7 @@ export default function(
541
613
  imageUrl,
542
614
  interval,
543
615
  https,
616
+ awsStyleEndpoint,
544
617
  localhost,
545
618
  trailingForwardSlash,
546
619
  url,
@@ -561,9 +634,11 @@ export default function(
561
634
  isOctal,
562
635
  roleTemplateRules,
563
636
  ruleGroups,
637
+ semanticVersion,
564
638
  servicePort,
565
639
  subDomain,
566
640
  testRule,
641
+ version,
567
642
  wildcardHostname
568
643
  };
569
644
  }
@@ -1,9 +1,26 @@
1
1
  import isUrl from 'is-url';
2
2
 
3
+ // Note that these function cover specific use cases and you need to make sure it works for your use case before using them.
4
+ // ie they would consider empty values as valid, not all endpoint formatting is enforced
5
+
3
6
  export const isServerUrl = (value) => value === 'server-url';
4
7
 
5
8
  export const isHttps = (value) => value.toLowerCase().startsWith('https://');
6
9
 
10
+ /**
11
+ * Checks that provided string is a domain without protocol (case insensitive):
12
+ * - Cannot start with any protocol, such as http://, https://, ftp://, ftps://, udp://
13
+ * - Must only use the letters a to z, the numbers 0 to 9, and the dot (.) and hyphen (-) characters.
14
+ * - if the hyphen character is used in a domain name, it cannot be the first or the last character in the name.
15
+ * - The length of each label can be 2-63 characters
16
+ * - TLD is at least 2 characters
17
+ * - The total length of a domain name, including the dot at the end, cannot exceed 254 characters.
18
+ * - Allows for optional port and path
19
+ * @param {*} value
20
+ * @returns boolean indicating if the value is a domain without protocol
21
+ */
22
+ export const isDomainWithoutProtocol = (value) => (/^(?=.{1,254}$)(?![a-z][a-z0-9+.-]*:\/\/)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?::\d{1,5})?(?:\/\S*)?$/i).test(value);
23
+
7
24
  export const isLocalhost = (value) => (/^(?:https?:\/\/)?(?:localhost|127\.0\.0\.1)/i).test(value);
8
25
 
9
26
  export const hasTrailingForwardSlash = (value) => isUrl(value) && value?.toLowerCase().endsWith('/');
@@ -1,18 +0,0 @@
1
- import { mount } from '@vue/test-utils';
2
- import HarvesterCloudCreds from '@shell/cloud-credential/harvester.vue';
3
-
4
- const mockStore = { getters: { 'i18n/t': jest.fn() } };
5
-
6
- describe('cloud credentials: Harvester', () => {
7
- const wrapper = mount(HarvesterCloudCreds, {
8
- props: { value: {} },
9
- global: { mocks: { $store: mockStore } }
10
- });
11
-
12
- it('should display the warning banner for token expiration', async() => {
13
- const warningBanner = wrapper.find('[data-testid="harvester-token-expiration-warning-banner"]');
14
-
15
- expect(warningBanner.exists()).toBe(true);
16
- expect(warningBanner.isVisible()).toBe(true);
17
- });
18
- });