@rancher/shell 0.3.27 → 0.3.29

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 (55) hide show
  1. package/assets/translations/en-us.yaml +19 -21
  2. package/assets/translations/zh-hans.yaml +0 -23
  3. package/chart/gatekeeper.vue +2 -11
  4. package/chart/istio.vue +1 -10
  5. package/chart/logging/index.vue +2 -11
  6. package/chart/monitoring/grafana/index.vue +2 -2
  7. package/chart/monitoring/index.vue +1 -9
  8. package/chart/rancher-backup/index.vue +1 -9
  9. package/components/AsyncButton.vue +9 -0
  10. package/components/Carousel.vue +2 -1
  11. package/components/SortableTable/THead.vue +7 -9
  12. package/components/SortableTable/index.vue +1 -2
  13. package/components/fleet/FleetStatus.vue +3 -3
  14. package/components/fleet/FleetSummary.vue +32 -27
  15. package/components/formatter/ClusterProvider.vue +1 -18
  16. package/components/nav/WindowManager/ContainerLogs.vue +22 -19
  17. package/config/product/manager.js +0 -13
  18. package/config/types.js +0 -4
  19. package/detail/provisioning.cattle.io.cluster.vue +2 -1
  20. package/edit/management.cattle.io.project.vue +1 -52
  21. package/edit/management.cattle.io.setting.vue +31 -2
  22. package/edit/provisioning.cattle.io.cluster/Basics.vue +6 -107
  23. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +0 -3
  24. package/edit/provisioning.cattle.io.cluster/rke2.vue +3 -128
  25. package/edit/workload/index.vue +2 -1
  26. package/machine-config/__tests__/vmwarevsphere.test.ts +72 -0
  27. package/machine-config/vmwarevsphere.vue +68 -13
  28. package/middleware/authenticated.js +4 -2
  29. package/models/__tests__/management.cattle.io.cluster.test.ts +19 -0
  30. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +90 -0
  31. package/models/cluster.x-k8s.io.machine.js +1 -1
  32. package/models/management.cattle.io.cluster.js +4 -0
  33. package/models/management.cattle.io.project.js +0 -36
  34. package/models/management.cattle.io.setting.js +11 -7
  35. package/models/provisioning.cattle.io.cluster.js +16 -4
  36. package/package.json +2 -1
  37. package/pages/auth/setup.vue +38 -1
  38. package/pages/c/_cluster/apps/charts/__tests__/install.helper.test.ts +2 -17
  39. package/pages/c/_cluster/apps/charts/index.vue +0 -15
  40. package/pages/c/_cluster/apps/charts/install.helpers.js +2 -13
  41. package/pages/c/_cluster/apps/charts/install.vue +1 -1
  42. package/pages/c/_cluster/explorer/index.vue +0 -47
  43. package/pages/c/_cluster/manager/pages/_page.vue +4 -5
  44. package/rancher-components/components/StringList/StringList.test.ts +270 -0
  45. package/rancher-components/components/StringList/StringList.vue +57 -18
  46. package/store/prefs.js +0 -3
  47. package/types/shell/index.d.ts +13 -7
  48. package/utils/custom-validators.js +0 -2
  49. package/utils/error.js +16 -1
  50. package/utils/validators/formRules/__tests__/index.test.ts +49 -4
  51. package/utils/validators/formRules/index.ts +12 -9
  52. package/utils/validators/setting.js +6 -10
  53. package/components/ChartPsp.vue +0 -76
  54. package/components/__tests__/ChartPsp.test.ts +0 -75
  55. package/components/formatter/__tests__/ClusterProvider.test.ts +0 -28
@@ -398,6 +398,276 @@ describe('stringList.vue', () => {
398
398
  });
399
399
  });
400
400
 
401
+ describe('bulk delimiter', () => {
402
+ const delimiter = /;/;
403
+
404
+ describe('add', () => {
405
+ const items: string[] = [];
406
+
407
+ beforeEach(() => {
408
+ wrapper = mount(StringList, {
409
+ propsData: {
410
+ items,
411
+ bulkAdditionDelimiter: delimiter,
412
+ errorMessages: { duplicate: 'error, item is duplicate.' },
413
+ }
414
+ });
415
+ });
416
+
417
+ it('should split values if delimiter set', async() => {
418
+ const value = 'test;test1;test2';
419
+ const result = ['test', 'test1', 'test2'];
420
+
421
+ // activate create mode
422
+ await wrapper.setData({ isCreateItem: true });
423
+ const inputField = wrapper.find('[data-testid="item-create"]');
424
+
425
+ await inputField.setValue(value);
426
+
427
+ // press enter
428
+ await inputField.trigger('keydown.enter');
429
+ await wrapper.vm.$nextTick();
430
+
431
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
432
+
433
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
434
+ });
435
+
436
+ it('should show warning if one of the values is a duplicate', async() => {
437
+ const value = 'test;test1;test2';
438
+
439
+ await wrapper.setProps({ items: ['test1'] });
440
+
441
+ // activate create mode
442
+ await wrapper.setData({ isCreateItem: true });
443
+ const inputField = wrapper.find('[data-testid="item-create"]');
444
+
445
+ await inputField.setValue(value);
446
+
447
+ // press enter
448
+ await inputField.trigger('keydown.enter');
449
+ await wrapper.vm.$nextTick();
450
+
451
+ const isDuplicate = (wrapper.emitted('errors') || [])[0][0].duplicate;
452
+
453
+ expect(isDuplicate).toBe(true);
454
+ });
455
+
456
+ it('should show a warning if the new values are all duplicates', async() => {
457
+ const value = 'test;test';
458
+
459
+ // activate create mode
460
+ await wrapper.setData({ isCreateItem: true });
461
+ const inputField = wrapper.find('[data-testid="item-create"]');
462
+
463
+ await inputField.setValue(value);
464
+
465
+ // press enter
466
+ await inputField.trigger('keydown.enter');
467
+ await wrapper.vm.$nextTick();
468
+
469
+ const isDuplicate = (wrapper.emitted('errors') || [])[0][0].duplicate;
470
+
471
+ expect(isDuplicate).toBe(true);
472
+ });
473
+
474
+ it('should not consider empty strings at the beginning', async() => {
475
+ const value = ';test;test1;test2';
476
+ const result = ['test', 'test1', 'test2'];
477
+
478
+ // activate create mode
479
+ await wrapper.setData({ isCreateItem: true });
480
+ const inputField = wrapper.find('[data-testid="item-create"]');
481
+
482
+ await inputField.setValue(value);
483
+
484
+ // press enter
485
+ await inputField.trigger('keydown.enter');
486
+ await wrapper.vm.$nextTick();
487
+
488
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
489
+
490
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
491
+ });
492
+
493
+ it('should not consider empty strings in the middle', async() => {
494
+ const value = 'test;test1;;test2';
495
+ const result = ['test', 'test1', 'test2'];
496
+
497
+ // activate create mode
498
+ await wrapper.setData({ isCreateItem: true });
499
+ const inputField = wrapper.find('[data-testid="item-create"]');
500
+
501
+ await inputField.setValue(value);
502
+
503
+ // press enter
504
+ await inputField.trigger('keydown.enter');
505
+ await wrapper.vm.$nextTick();
506
+
507
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
508
+
509
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
510
+ });
511
+
512
+ it('should not consider empty strings at the end', async() => {
513
+ const value = 'test;test1;test2;';
514
+ const result = ['test', 'test1', 'test2'];
515
+
516
+ // activate create mode
517
+ await wrapper.setData({ isCreateItem: true });
518
+ const inputField = wrapper.find('[data-testid="item-create"]');
519
+
520
+ await inputField.setValue(value);
521
+
522
+ // press enter
523
+ await inputField.trigger('keydown.enter');
524
+ await wrapper.vm.$nextTick();
525
+
526
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
527
+
528
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
529
+ });
530
+ });
531
+
532
+ describe('edit', () => {
533
+ const items = ['test1', 'test2'];
534
+
535
+ beforeEach(() => {
536
+ wrapper = mount(StringList, {
537
+ propsData: {
538
+ items,
539
+ bulkAdditionDelimiter: delimiter,
540
+ errorMessages: { duplicate: 'error, item is duplicate.' },
541
+ }
542
+ });
543
+ });
544
+
545
+ it('should split values if delimiter set', async() => {
546
+ const newValue = 'test1;new;values';
547
+ const result = ['test1', 'new', 'values', 'test2'];
548
+
549
+ await wrapper.setData({ editedItem: items[0] });
550
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
551
+
552
+ await inputField.setValue(newValue);
553
+
554
+ // press enter
555
+ await inputField.trigger('keydown.enter');
556
+ await wrapper.vm.$nextTick();
557
+
558
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
559
+
560
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
561
+ });
562
+
563
+ it('should show warning if one of the values is a duplicate', async() => {
564
+ const newValue = 'test1;test2';
565
+
566
+ await wrapper.setData({ editedItem: items[0] });
567
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
568
+
569
+ await inputField.setValue(newValue);
570
+
571
+ // press enter
572
+ await inputField.trigger('keydown.enter');
573
+ await wrapper.vm.$nextTick();
574
+
575
+ const isDuplicate = (wrapper.emitted('errors') || [])[0][0].duplicate;
576
+
577
+ expect(isDuplicate).toBe(true);
578
+ });
579
+
580
+ it('should show a warning if the new values are all duplicates', async() => {
581
+ const newValue = 'test;test';
582
+
583
+ await wrapper.setData({ editedItem: items[0] });
584
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
585
+
586
+ await inputField.setValue(newValue);
587
+
588
+ // press enter
589
+ await inputField.trigger('keydown.enter');
590
+ await wrapper.vm.$nextTick();
591
+
592
+ const isDuplicate = (wrapper.emitted('errors') || [])[0][0].duplicate;
593
+
594
+ expect(isDuplicate).toBe(true);
595
+ });
596
+
597
+ it('should not consider empty strings at the beginning', async() => {
598
+ const newValue = ';test1;new;value';
599
+ const result = ['test1', 'new', 'value', 'test2'];
600
+
601
+ await wrapper.setData({ editedItem: items[0] });
602
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
603
+
604
+ await inputField.setValue(newValue);
605
+
606
+ // press enter
607
+ await inputField.trigger('keydown.enter');
608
+ await wrapper.vm.$nextTick();
609
+
610
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
611
+
612
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
613
+ });
614
+
615
+ it('should not consider empty strings in the middle 1', async() => {
616
+ const newValue = 'test1; ;new;value';
617
+ const result = ['test1', 'new', 'value', 'test2'];
618
+
619
+ await wrapper.setData({ editedItem: items[0] });
620
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
621
+
622
+ await inputField.setValue(newValue);
623
+
624
+ // press enter
625
+ await inputField.trigger('keydown.enter');
626
+ await wrapper.vm.$nextTick();
627
+
628
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
629
+
630
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
631
+ });
632
+
633
+ it('should not consider empty strings in the middle 2', async() => {
634
+ const newValue = 'test1;;new;value';
635
+ const result = ['test1', 'new', 'value', 'test2'];
636
+
637
+ await wrapper.setData({ editedItem: items[0] });
638
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
639
+
640
+ await inputField.setValue(newValue);
641
+
642
+ // press enter
643
+ await inputField.trigger('keydown.enter');
644
+ await wrapper.vm.$nextTick();
645
+
646
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
647
+
648
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
649
+ });
650
+
651
+ it('should not consider empty strings at the end', async() => {
652
+ const newValue = 'test1;new;value;';
653
+ const result = ['test1', 'new', 'value', 'test2'];
654
+
655
+ await wrapper.setData({ editedItem: items[0] });
656
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
657
+
658
+ await inputField.setValue(newValue);
659
+
660
+ // press enter
661
+ await inputField.trigger('keydown.enter');
662
+ await wrapper.vm.$nextTick();
663
+
664
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
665
+
666
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
667
+ });
668
+ });
669
+ });
670
+
401
671
  describe('errors handling', () => {
402
672
  it('show duplicate warning icon when errorMessages is defined', async() => {
403
673
  const items = ['test'];
@@ -82,6 +82,13 @@ export default Vue.extend({
82
82
  return {} as ErrorMessages;
83
83
  },
84
84
  },
85
+ /**
86
+ * Enables bulk addition and defines the delimiter to split the input string.
87
+ */
88
+ bulkAdditionDelimiter: {
89
+ type: RegExp,
90
+ default: null,
91
+ }
85
92
  },
86
93
  data() {
87
94
  return {
@@ -125,13 +132,9 @@ export default Vue.extend({
125
132
  },
126
133
 
127
134
  methods: {
128
- onChange(value: string) {
135
+ onChange(value: string, index?: number) {
129
136
  this.value = value;
130
-
131
- const items = [
132
- ...this.items,
133
- this.value
134
- ];
137
+ const items = this.addValueToItems(this.items, value, index);
135
138
 
136
139
  this.toggleError(
137
140
  'duplicate',
@@ -321,10 +324,7 @@ export default Vue.extend({
321
324
  const value = this.value?.trim();
322
325
 
323
326
  if (value) {
324
- const items = [
325
- ...this.items,
326
- value,
327
- ];
327
+ const items = this.addValueToItems(this.items, value);
328
328
 
329
329
  if (!hasDuplicatedStrings(items, this.caseSensitive)) {
330
330
  this.updateItems(items);
@@ -343,12 +343,8 @@ export default Vue.extend({
343
343
  const value = this.value?.trim();
344
344
 
345
345
  if (value) {
346
- const items = [...this.items];
347
- const index = findStringIndex(items, item, false);
348
-
349
- if (index !== -1) {
350
- items[index] = value;
351
- }
346
+ const index = findStringIndex(this.items, item, false);
347
+ const items = index !== -1 ? this.addValueToItems(this.items, value, index) : this.items;
352
348
 
353
349
  if (!hasDuplicatedStrings(items, this.caseSensitive)) {
354
350
  this.updateItems(items);
@@ -360,6 +356,49 @@ export default Vue.extend({
360
356
  }
361
357
  },
362
358
 
359
+ /**
360
+ * Add a new or update an exiting item in the items list.
361
+ *
362
+ * @param items The current items list.
363
+ * @param value The new value to be added.
364
+ * @param index The list index of the item to be updated (optional).
365
+ * @returns Updated items list.
366
+ */
367
+ addValueToItems(items: string[], value: string, index?: number): string[] {
368
+ const updatedItems = [...items];
369
+
370
+ // Add new item
371
+ if (index === undefined) {
372
+ if (this.bulkAdditionDelimiter && value.search(this.bulkAdditionDelimiter) >= 0) {
373
+ updatedItems.push(...this.splitBulkValue(value));
374
+ } else {
375
+ updatedItems.push(value);
376
+ }
377
+ } else { // Update existing item
378
+ if (this.bulkAdditionDelimiter && value.search(this.bulkAdditionDelimiter) >= 0) {
379
+ updatedItems.splice(index, 1, ...this.splitBulkValue(value));
380
+ } else {
381
+ updatedItems[index] = value;
382
+ }
383
+ }
384
+
385
+ return updatedItems;
386
+ },
387
+
388
+ /**
389
+ * Split the value by the defined delimiter and remove empty strings.
390
+ *
391
+ * @param value The value to be split.
392
+ * @returns Array containing split values.
393
+ */
394
+ splitBulkValue(value: string): string[] {
395
+ return value
396
+ .split(this.bulkAdditionDelimiter)
397
+ .filter((item) => {
398
+ return item.trim().length > 0;
399
+ });
400
+ },
401
+
363
402
  /**
364
403
  * Remove an item from items list
365
404
  */
@@ -393,7 +432,7 @@ export default Vue.extend({
393
432
  @dblclick="onClickEmptyBody()"
394
433
  >
395
434
  <div
396
- v-for="item in items"
435
+ v-for="(item, index) in items"
397
436
  :key="item"
398
437
  :ref="item"
399
438
  :class="{
@@ -421,7 +460,7 @@ export default Vue.extend({
421
460
  :data-testid="`item-edit-${item}`"
422
461
  class="edit-input static"
423
462
  :value="value != null ? value : item"
424
- @input="onChange($event)"
463
+ @input="onChange($event, index)"
425
464
  @blur.prevent="updateItem(item)"
426
465
  @keydown.native.enter="updateItem(item, !errors.duplicate)"
427
466
  />
package/store/prefs.js CHANGED
@@ -112,9 +112,6 @@ export const _RKE1 = 'rke1';
112
112
  export const _RKE2 = 'rke2';
113
113
  export const PROVISIONER = create('provisioner', _RKE2, { options: [_RKE1, _RKE2] });
114
114
 
115
- // Promo for Pod Security Policies (PSPs) being deprecated on kube version 1.25 on Cluster Dashboard page
116
- export const PSP_DEPRECATION_BANNER = create('hide-psp-deprecation-banner', false, { parseJSON });
117
-
118
115
  // Maximum number of clusters to show in the slide-in menu
119
116
  export const MENU_MAX_CLUSTERS = 10;
120
117
  // Prompt for confirm when scaling down node pool in GUI and save the pref
@@ -1765,8 +1765,6 @@ export const NODE: "node";
1765
1765
  export const NETWORK_POLICY: "networking.k8s.io.networkpolicy";
1766
1766
  export const POD: "pod";
1767
1767
  export const POD_DISRUPTION_BUDGET: "policy.poddisruptionbudget";
1768
- export const PSP: "policy.podsecuritypolicy";
1769
- export const PSPS: "policy.podsecuritypolicies";
1770
1768
  export const PV: "persistentvolume";
1771
1769
  export const PVC: "persistentvolumeclaim";
1772
1770
  export const RESOURCE_QUOTA: "resourcequota";
@@ -1884,8 +1882,6 @@ export namespace MANAGEMENT {
1884
1882
  export { GLOBAL_ROLE_1 as GLOBAL_ROLE };
1885
1883
  const GLOBAL_ROLE_BINDING_1: string;
1886
1884
  export { GLOBAL_ROLE_BINDING_1 as GLOBAL_ROLE_BINDING };
1887
- export const POD_SECURITY_POLICY_TEMPLATE: string;
1888
- export const PSP_TEMPLATE_BINDING: string;
1889
1885
  export const PSA: string;
1890
1886
  export const MANAGED_CHART: string;
1891
1887
  export const USER_NOTIFICATION: string;
@@ -2869,7 +2865,6 @@ export const PLUGIN_DEVELOPER: any;
2869
2865
  export const _RKE1: "rke1";
2870
2866
  export const _RKE2: "rke2";
2871
2867
  export const PROVISIONER: any;
2872
- export const PSP_DEPRECATION_BANNER: any;
2873
2868
  export const MENU_MAX_CLUSTERS: 10;
2874
2869
  export const SCALE_POOL_PROMPT: any;
2875
2870
  export function state(): {
@@ -3301,7 +3296,6 @@ declare namespace _default {
3301
3296
  export { cronSchedule };
3302
3297
  export { podAffinity };
3303
3298
  export { roleTemplateRules };
3304
- export { isHttps };
3305
3299
  }
3306
3300
  export default _default;
3307
3301
  }
@@ -3361,8 +3355,17 @@ declare module '@shell/utils/error' {
3361
3355
  export function stringify(err: any): any;
3362
3356
  export function exceptionToErrorsArray(err: any): any;
3363
3357
  export class ClusterNotFoundError extends Error {
3358
+ static name: string;
3364
3359
  constructor(message: any);
3365
3360
  }
3361
+ /**
3362
+ * An error occurred and the user should be redirected to a certain location (where this is handled)
3363
+ */
3364
+ export class RedirectToError extends Error {
3365
+ static name: string;
3366
+ constructor(message: any, url: any);
3367
+ url: any;
3368
+ }
3366
3369
  export class ApiError extends Error {
3367
3370
  constructor(res: any);
3368
3371
  status: any;
@@ -4173,7 +4176,10 @@ export function externalName(spec: any, getters: any, errors: any, validatorArgs
4173
4176
  // @shell/utils/validators/setting
4174
4177
 
4175
4178
  declare module '@shell/utils/validators/setting' {
4176
- export function isHttps(value: any, getters: any, errors: any, validatorArgs: any, displayKey: any): any;
4179
+ export function isServerUrl(value: any): boolean;
4180
+ export function isHttps(value: any): any;
4181
+ export function isLocalhost(value: any): boolean;
4182
+ export function hasTrailingForwardSlash(value: any): any;
4177
4183
  }
4178
4184
 
4179
4185
  // @shell/utils/version
@@ -8,7 +8,6 @@ import { cronSchedule } from '@shell/utils/validators/cron-schedule';
8
8
  import { podAffinity } from '@shell/utils/validators/pod-affinity';
9
9
  import { roleTemplateRules } from '@shell/utils/validators/role-template';
10
10
  import { clusterName } from '@shell/utils/validators/cluster-name';
11
- import { isHttps } from '@shell/utils/validators/setting';
12
11
 
13
12
  /**
14
13
  * Custom validation functions beyond normal scalr types
@@ -30,5 +29,4 @@ export default {
30
29
  cronSchedule,
31
30
  podAffinity,
32
31
  roleTemplateRules,
33
- isHttps,
34
32
  };
package/utils/error.js CHANGED
@@ -1,9 +1,24 @@
1
1
  import { isArray } from '@shell/utils/array';
2
2
 
3
3
  export class ClusterNotFoundError extends Error {
4
+ static name = 'ClusterNotFoundError'
5
+
4
6
  constructor(message) {
5
7
  super(message);
6
- this.name = 'ClusterNotFoundError';
8
+ this.name = ClusterNotFoundError.name;
9
+ }
10
+ }
11
+
12
+ /**
13
+ * An error occurred and the user should be redirected to a certain location (where this is handled)
14
+ */
15
+ export class RedirectToError extends Error {
16
+ static name = 'RedirectToError'
17
+
18
+ constructor(message, url) {
19
+ super(message);
20
+ this.url = url;
21
+ this.name = RedirectToError.name;
7
22
  }
8
23
  }
9
24
 
@@ -43,21 +43,66 @@ describe('formRules', () => {
43
43
  expect(formRuleResult).toStrictEqual(expectedResult);
44
44
  });
45
45
 
46
- it('"isHttps" : returns undefined when valid https url value is supplied', () => {
46
+ it('"https" : returns undefined when valid https url value is supplied', () => {
47
47
  const testValue = 'https://url.com';
48
- const formRuleResult = formRules.isHttps('server-url')(testValue);
48
+ const formRuleResult = formRules.https(testValue);
49
49
 
50
50
  expect(formRuleResult).toBeUndefined();
51
51
  });
52
52
 
53
- it('"isHttps" : returns correct message when http url value is supplied', () => {
53
+ it('"https" : returns correct message when http url value is supplied', () => {
54
54
  const testValue = 'http://url.com';
55
- const formRuleResult = formRules.isHttps('server-url')(testValue);
55
+ const formRuleResult = formRules.https(testValue);
56
56
  const expectedResult = JSON.stringify({ message: 'validation.setting.serverUrl.https' });
57
57
 
58
58
  expect(formRuleResult).toStrictEqual(expectedResult);
59
59
  });
60
60
 
61
+ describe('localhost', () => {
62
+ const message = JSON.stringify({ message: 'validation.setting.serverUrl.localhost' });
63
+ const testCases = [
64
+ ['http://LOCALhosT:8005', message],
65
+ ['http://localhost:8005', message],
66
+ ['https://localhost:8005', message],
67
+ ['localhost', message],
68
+ ['http://127.0.0.1', message],
69
+ ['https://127.0.0.1', message],
70
+ ['127.0.0.1', message],
71
+ ['https://test.com', undefined],
72
+ ['https://test.com/localhost', undefined],
73
+ [undefined, undefined]
74
+ ];
75
+
76
+ it.each(testCases)(
77
+ 'should return undefined or correct message based on the provided url',
78
+ (url, expected) => {
79
+ const formRuleResult = formRules.localhost(url);
80
+
81
+ expect(formRuleResult).toStrictEqual(expected);
82
+ }
83
+ );
84
+ });
85
+
86
+ describe('trailingForwardSlash', () => {
87
+ const message = JSON.stringify({ message: 'validation.setting.serverUrl.trailingForwardSlash' });
88
+ const testCases = [
89
+ ['https://test.com', undefined],
90
+ ['https://test.com/', message],
91
+ ['https://', undefined],
92
+ ['/', undefined],
93
+ [undefined, undefined]
94
+ ];
95
+
96
+ it.each(testCases)(
97
+ 'should return undefined or correct message based on the provided url',
98
+ (url, expected) => {
99
+ const formRuleResult = formRules.trailingForwardSlash(url);
100
+
101
+ expect(formRuleResult).toStrictEqual(expected);
102
+ }
103
+ );
104
+ });
105
+
61
106
  it('"interval" : returns undefined when valid hour interval value is supplied', () => {
62
107
  const testValue = '5h';
63
108
  const formRuleResult = formRules.interval(testValue);
@@ -2,9 +2,11 @@ import { RBAC } from '@shell/config/types';
2
2
  import { HCI } from '@shell/config/labels-annotations';
3
3
  import isEmpty from 'lodash/isEmpty';
4
4
  import has from 'lodash/has';
5
+ import isUrl from 'is-url';
5
6
  // import uniq from 'lodash/uniq';
6
7
  import cronstrue from 'cronstrue';
7
8
  import { Translation } from '@shell/types/t';
9
+ import { isHttps, isLocalhost, hasTrailingForwardSlash } from '@shell/utils/validators/setting';
8
10
 
9
11
  // import uniq from 'lodash/uniq';
10
12
  export type Validator<T = undefined | string> = (val: any, arg?: any) => T;
@@ -34,10 +36,6 @@ export class Port {
34
36
  }
35
37
  }
36
38
 
37
- const httpsKeys = [
38
- 'server-url'
39
- ];
40
-
41
39
  const runValidators = (val: any, validators: Validator[]) => {
42
40
  for (const validator of validators) {
43
41
  const message = validator(val);
@@ -139,11 +137,13 @@ export default function(t: Translation, { key = 'Value' }: ValidationOptions): {
139
137
  }
140
138
  };
141
139
 
142
- const isHttps: ValidatorFactory = (key: string) => {
143
- const isHttps: Validator = (val: string) => httpsKeys.includes(key) && !val.toLowerCase().startsWith('https://') ? t('validation.setting.serverUrl.https') : undefined;
140
+ const https: Validator = (val: string) => val && !isHttps(val) ? t('validation.setting.serverUrl.https') : undefined;
144
141
 
145
- return isHttps;
146
- };
142
+ const localhost: Validator = (val: string) => isLocalhost(val) ? t('validation.setting.serverUrl.localhost') : undefined;
143
+
144
+ const trailingForwardSlash: Validator = (val: string) => hasTrailingForwardSlash(val) ? t('validation.setting.serverUrl.trailingForwardSlash') : undefined;
145
+
146
+ const url: Validator = (val: string) => val && !isUrl(val) ? t('validation.setting.serverUrl.url') : undefined;
147
147
 
148
148
  const interval: Validator = (val: string) => !/^\d+[hms]$/.test(val) ? t('validation.monitoring.route.interval', { key }) : undefined;
149
149
 
@@ -475,7 +475,10 @@ export default function(t: Translation, { key = 'Value' }: ValidationOptions): {
475
475
  hostname,
476
476
  imageUrl,
477
477
  interval,
478
- isHttps,
478
+ https,
479
+ localhost,
480
+ trailingForwardSlash,
481
+ url,
479
482
  matching,
480
483
  maxLength,
481
484
  maxValue,
@@ -1,13 +1,9 @@
1
- const httpsKeys = [
2
- 'server-url'
3
- ];
1
+ import isUrl from 'is-url';
4
2
 
5
- export function isHttps(value, getters, errors, validatorArgs, displayKey) {
6
- const key = validatorArgs[0];
3
+ export const isServerUrl = (value) => value === 'server-url';
7
4
 
8
- if (httpsKeys.includes(key) && !value.toLowerCase().startsWith('https://')) {
9
- errors.push(getters['i18n/t']('validation.setting.serverUrl.https'));
10
- }
5
+ export const isHttps = (value) => value.toLowerCase().startsWith('https://');
11
6
 
12
- return errors;
13
- }
7
+ export const isLocalhost = (value) => (/^(?:https?:\/\/)?(?:localhost|127\.0\.0\.1)/i).test(value);
8
+
9
+ export const hasTrailingForwardSlash = (value) => isUrl(value) && value?.toLowerCase().endsWith('/');