@rancher/shell 0.3.26 → 0.3.28

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 (99) hide show
  1. package/assets/translations/en-us.yaml +8 -23
  2. package/assets/translations/zh-hans.yaml +2 -26
  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/index.vue +1 -9
  7. package/chart/rancher-backup/index.vue +1 -9
  8. package/components/AlertTable.vue +8 -6
  9. package/components/Carousel.vue +2 -1
  10. package/components/EmberPage.vue +2 -2
  11. package/components/EtcdInfoBanner.vue +12 -2
  12. package/components/GlobalRoleBindings.vue +10 -0
  13. package/components/GrafanaDashboard.vue +8 -3
  14. package/components/Wizard.vue +17 -1
  15. package/components/auth/RoleDetailEdit.vue +17 -1
  16. package/components/form/ArrayList.vue +20 -11
  17. package/components/form/__tests__/ArrayList.test.ts +44 -0
  18. package/components/formatter/ClusterProvider.vue +1 -18
  19. package/components/nav/Header.vue +5 -4
  20. package/components/nav/TopLevelMenu.vue +38 -15
  21. package/components/nav/WindowManager/ContainerLogs.vue +22 -19
  22. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -0
  23. package/components/nav/__tests__/Type.test.ts +139 -0
  24. package/config/private-label.js +1 -1
  25. package/config/product/manager.js +0 -13
  26. package/config/settings.ts +0 -2
  27. package/config/types.js +0 -4
  28. package/core/types.ts +11 -4
  29. package/edit/management.cattle.io.project.vue +1 -52
  30. package/edit/management.cattle.io.setting.vue +31 -2
  31. package/edit/provisioning.cattle.io.cluster/Basics.vue +19 -107
  32. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +1 -1
  33. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +0 -3
  34. package/edit/provisioning.cattle.io.cluster/rke2.vue +3 -128
  35. package/edit/workload/mixins/workload.js +14 -4
  36. package/middleware/authenticated.js +4 -2
  37. package/models/__tests__/management.cattle.io.cluster.test.ts +19 -0
  38. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +90 -0
  39. package/models/cluster.x-k8s.io.machine.js +1 -1
  40. package/models/fleet.cattle.io.cluster.js +11 -1
  41. package/models/management.cattle.io.cluster.js +4 -0
  42. package/models/management.cattle.io.project.js +0 -36
  43. package/models/management.cattle.io.setting.js +11 -7
  44. package/models/provisioning.cattle.io.cluster.js +16 -4
  45. package/package.json +1 -1
  46. package/pages/auth/setup.vue +38 -1
  47. package/pages/c/_cluster/apps/charts/__tests__/install.helper.test.ts +2 -17
  48. package/pages/c/_cluster/apps/charts/index.vue +0 -15
  49. package/pages/c/_cluster/apps/charts/install.helpers.js +2 -13
  50. package/pages/c/_cluster/apps/charts/install.vue +1 -1
  51. package/pages/c/_cluster/auth/roles/index.vue +11 -1
  52. package/pages/c/_cluster/explorer/index.vue +7 -49
  53. package/pages/c/_cluster/manager/pages/_page.vue +4 -5
  54. package/pages/c/_cluster/monitoring/index.vue +26 -39
  55. package/pages/support/index.vue +1 -8
  56. package/promptRemove/management.cattle.io.project.vue +6 -9
  57. package/rancher-components/BadgeState/BadgeState.vue +1 -5
  58. package/rancher-components/Banner/Banner.test.ts +1 -51
  59. package/rancher-components/Banner/Banner.vue +53 -134
  60. package/rancher-components/Card/Card.vue +7 -24
  61. package/rancher-components/Form/Checkbox/Checkbox.test.ts +29 -20
  62. package/rancher-components/Form/Checkbox/Checkbox.vue +20 -45
  63. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +8 -2
  64. package/rancher-components/Form/LabeledInput/LabeledInput.vue +10 -22
  65. package/rancher-components/Form/Radio/RadioButton.vue +13 -30
  66. package/rancher-components/Form/Radio/RadioGroup.vue +7 -26
  67. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +6 -7
  68. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +38 -25
  69. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +11 -23
  70. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +5 -19
  71. package/rancher-components/StringList/StringList.test.ts +49 -453
  72. package/rancher-components/StringList/StringList.vue +58 -92
  73. package/rancher-components/components/Form/Radio/RadioGroup.test.ts +30 -0
  74. package/rancher-components/components/Form/Radio/RadioGroup.vue +4 -0
  75. package/rancher-components/components/StringList/StringList.test.ts +270 -0
  76. package/rancher-components/components/StringList/StringList.vue +57 -18
  77. package/store/features.js +1 -0
  78. package/store/prefs.js +0 -3
  79. package/types/shell/index.d.ts +26 -17
  80. package/utils/__tests__/object.test.ts +67 -1
  81. package/utils/__tests__/version.test.ts +13 -23
  82. package/utils/cluster.js +1 -1
  83. package/utils/custom-validators.js +0 -2
  84. package/utils/error.js +16 -1
  85. package/utils/grafana.js +1 -2
  86. package/utils/monitoring.js +25 -1
  87. package/utils/object.js +4 -3
  88. package/utils/sort.js +1 -1
  89. package/utils/validators/formRules/__tests__/index.test.ts +49 -4
  90. package/utils/validators/formRules/index.ts +13 -10
  91. package/utils/validators/role-template.js +1 -1
  92. package/utils/validators/setting.js +6 -10
  93. package/utils/version.js +0 -13
  94. package/components/ChartPsp.vue +0 -76
  95. package/components/__tests__/ChartPsp.test.ts +0 -75
  96. package/components/formatter/__tests__/ClusterProvider.test.ts +0 -28
  97. package/rancher-components/Card/Card.test.ts +0 -37
  98. package/rancher-components/Form/Radio/RadioButton.test.ts +0 -31
  99. package/yarn-error.log +0 -200
@@ -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/features.js CHANGED
@@ -31,6 +31,7 @@ export const UNSUPPORTED_STORAGE_DRIVERS = create('unsupported-storage-drivers',
31
31
  export const FLEET = create('continuous-delivery', true);
32
32
  export const HARVESTER = create('harvester', true);
33
33
  export const HARVESTER_CONTAINER = create('harvester-baremetal-container-workload', false);
34
+ export const FLEET_WORKSPACE_BACK = create('provisioningv2-fleet-workspace-back-population', false);
34
35
 
35
36
  // Not currently used.. no point defining ones we don't use
36
37
  // export const EMBEDDED_CLUSTER_API = create('embedded-cluster-api', true);
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;
@@ -2807,6 +2803,7 @@ export const UNSUPPORTED_STORAGE_DRIVERS: any;
2807
2803
  export const FLEET: any;
2808
2804
  export const HARVESTER: any;
2809
2805
  export const HARVESTER_CONTAINER: any;
2806
+ export const FLEET_WORKSPACE_BACK: any;
2810
2807
  export namespace getters {
2811
2808
  function get(state: any, getters: any, rootState: any, rootGetters: any): (name: any) => any;
2812
2809
  }
@@ -2868,7 +2865,6 @@ export const PLUGIN_DEVELOPER: any;
2868
2865
  export const _RKE1: "rke1";
2869
2866
  export const _RKE2: "rke2";
2870
2867
  export const PROVISIONER: any;
2871
- export const PSP_DEPRECATION_BANNER: any;
2872
2868
  export const MENU_MAX_CLUSTERS: 10;
2873
2869
  export const SCALE_POOL_PROMPT: any;
2874
2870
  export function state(): {
@@ -3300,7 +3296,6 @@ declare namespace _default {
3300
3296
  export { cronSchedule };
3301
3297
  export { podAffinity };
3302
3298
  export { roleTemplateRules };
3303
- export { isHttps };
3304
3299
  }
3305
3300
  export default _default;
3306
3301
  }
@@ -3360,8 +3355,17 @@ declare module '@shell/utils/error' {
3360
3355
  export function stringify(err: any): any;
3361
3356
  export function exceptionToErrorsArray(err: any): any;
3362
3357
  export class ClusterNotFoundError extends Error {
3358
+ static name: string;
3363
3359
  constructor(message: any);
3364
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
+ }
3365
3369
  export class ApiError extends Error {
3366
3370
  constructor(res: any);
3367
3371
  status: any;
@@ -3444,6 +3448,9 @@ export function monitoringStatus(): {
3444
3448
  export function haveV2Monitoring(getters: any): boolean;
3445
3449
  export function haveV1Monitoring(getters: any): boolean;
3446
3450
  export function haveV1MonitoringWorkloads(store: any): Promise<boolean>;
3451
+ export function canViewGrafanaLink(store: any): Promise<boolean>;
3452
+ export function canViewAlertManagerLink(store: any): Promise<boolean>;
3453
+ export function canViewPrometheusLink(store: any): Promise<boolean>;
3447
3454
  }
3448
3455
 
3449
3456
  // @shell/utils/namespace-filter
@@ -3614,35 +3621,35 @@ export namespace KEY {
3614
3621
  }
3615
3622
  }
3616
3623
 
3617
- // @shell/utils/poller-sequential
3624
+ // @shell/utils/poller
3618
3625
 
3619
- declare module '@shell/utils/poller-sequential' {
3620
- export default class PollerSequential {
3626
+ declare module '@shell/utils/poller' {
3627
+ export default class Poller {
3621
3628
  constructor(fn: any, pollRateMs: any, maxRetries?: number);
3622
3629
  fn: any;
3623
3630
  pollRateMs: any;
3624
3631
  maxRetries: number;
3625
- timeoutId: any;
3632
+ intervalId: any;
3626
3633
  tryCount: number;
3627
3634
  start(): void;
3628
3635
  stop(): void;
3629
- _poll(): void;
3630
3636
  _intervalMethod(): Promise<void>;
3631
3637
  }
3632
3638
  }
3633
3639
 
3634
- // @shell/utils/poller
3640
+ // @shell/utils/poller-sequential
3635
3641
 
3636
- declare module '@shell/utils/poller' {
3637
- export default class Poller {
3642
+ declare module '@shell/utils/poller-sequential' {
3643
+ export default class PollerSequential {
3638
3644
  constructor(fn: any, pollRateMs: any, maxRetries?: number);
3639
3645
  fn: any;
3640
3646
  pollRateMs: any;
3641
3647
  maxRetries: number;
3642
- intervalId: any;
3648
+ timeoutId: any;
3643
3649
  tryCount: number;
3644
3650
  start(): void;
3645
3651
  stop(): void;
3652
+ _poll(): void;
3646
3653
  _intervalMethod(): Promise<void>;
3647
3654
  }
3648
3655
  }
@@ -4169,7 +4176,10 @@ export function externalName(spec: any, getters: any, errors: any, validatorArgs
4169
4176
  // @shell/utils/validators/setting
4170
4177
 
4171
4178
  declare module '@shell/utils/validators/setting' {
4172
- 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;
4173
4183
  }
4174
4184
 
4175
4185
  // @shell/utils/version
@@ -4188,7 +4198,6 @@ export function seenReleaseNotes(store: any): boolean;
4188
4198
  export function markSeenReleaseNotes(store: any): Promise<void>;
4189
4199
  export function readReleaseNotes(store: any): boolean;
4190
4200
  export function markReadReleaseNotes(store: any): Promise<void>;
4191
- export function generateSupportLink(version: any): string;
4192
4201
  }
4193
4202
 
4194
4203
  // @shell/utils/width
@@ -1,5 +1,5 @@
1
1
  import {
2
- clone, get, getter, isEmpty, toDictionary, remove
2
+ clone, get, getter, isEmpty, toDictionary, remove, diff, definedKeys
3
3
  } from '@shell/utils/object';
4
4
 
5
5
  describe('fx: get', () => {
@@ -161,3 +161,69 @@ describe('fx: remove', () => {
161
161
  expect(result).toStrictEqual(expected);
162
162
  });
163
163
  });
164
+
165
+ describe('fx: diff', () => {
166
+ it('should return an object including only the differences between two objects', () => {
167
+ const from = {
168
+ foo: 'bar',
169
+ baz: 'bang',
170
+ };
171
+ const to = {
172
+ foo: 'bar',
173
+ bang: 'baz'
174
+ };
175
+
176
+ const result = diff(from, to);
177
+ const expected = {
178
+ baz: null,
179
+ bang: 'baz'
180
+ };
181
+
182
+ expect(result).toStrictEqual(expected);
183
+ });
184
+ it('should return an object and dot characters in object should still be respected', () => {
185
+ const from = {};
186
+ const to = { foo: { 'bar.baz': 'bang' } };
187
+
188
+ const result = diff(from, to);
189
+ const expected = { foo: { 'bar.baz': 'bang' } };
190
+
191
+ expect(result).toStrictEqual(expected);
192
+ });
193
+ });
194
+
195
+ describe('fx: definedKeys', () => {
196
+ it('should return an array of keys within an array', () => {
197
+ const obj = {
198
+ foo: 'bar',
199
+ baz: 'bang',
200
+ };
201
+
202
+ const result = definedKeys(obj);
203
+ const expected = ['"foo"', '"baz"'];
204
+
205
+ expect(result).toStrictEqual(expected);
206
+ });
207
+ it('should return an array of keys with primitive values and their full nested path', () => {
208
+ const obj = {
209
+ foo: 'bar',
210
+ baz: { bang: 'bop' },
211
+ };
212
+
213
+ const result = definedKeys(obj);
214
+ const expected = ['"foo"', '"baz"."bang"'];
215
+
216
+ expect(result).toStrictEqual(expected);
217
+ });
218
+ it('should return an array of keys with primitive values and their full nested path with quotation marks to escape keys with dots in them', () => {
219
+ const obj = {
220
+ foo: 'bar',
221
+ baz: { 'bang.bop': 'beep' },
222
+ };
223
+
224
+ const result = definedKeys(obj);
225
+ const expected = ['"foo"', '"baz"."bang.bop"'];
226
+
227
+ expect(result).toStrictEqual(expected);
228
+ });
229
+ });
@@ -1,28 +1,18 @@
1
- import { generateSupportLink } from '@shell/utils/version';
1
+ import { isDevBuild } from '@shell/utils/version';
2
2
 
3
- describe('fx: generateSupportLink', () => {
4
- it('should generate support link corresponding to the installed Rancher version', () => {
5
- const version = 'v2.7.5';
6
- const expectation = 'https://www.suse.com/suse-rancher/support-matrix/all-supported-versions/rancher-v2-7-5';
3
+ describe('fx: isDevBuild', () => {
4
+ it.each([
5
+ 'dev',
6
+ 'master',
7
+ 'head',
8
+ 'whatever-head',
9
+ 'whatever-rc1',
10
+ 'whatever-alpha1',
11
+ ])(
12
+ 'should exclude version type %p', (version: string) => {
13
+ const result = isDevBuild(version);
7
14
 
8
- const result = generateSupportLink(version);
9
-
10
- expect(result).toStrictEqual(expectation);
11
- });
12
-
13
- const latestVersionSupportURL = 'https://rancher.com/support-maintenance-terms';
14
- const testCases = [
15
- ['v2.7-0bcf068e1237acafd4aca01385c7c6b432e22fd7-head', latestVersionSupportURL],
16
- ['v2.7.5-rc4', latestVersionSupportURL],
17
- [undefined, latestVersionSupportURL],
18
- ];
19
-
20
- it.each(testCases)(
21
- 'should generate support link corresponding to the latest Rancher version when version is unknown or for dev build',
22
- (version, expected) => {
23
- const result = generateSupportLink(version);
24
-
25
- expect(result).toBe(expected);
15
+ expect(result).toBe(true);
26
16
  }
27
17
  );
28
18
  });
package/utils/cluster.js CHANGED
@@ -7,7 +7,7 @@ import { SETTING } from '@shell/config/settings';
7
7
  export function filterOnlyKubernetesClusters(mgmtClusters, store) {
8
8
  const openHarvesterContainerWorkload = store.getters['features/get']('harvester-baremetal-container-workload');
9
9
 
10
- return mgmtClusters.filter((c) => {
10
+ return mgmtClusters?.filter((c) => {
11
11
  return openHarvesterContainerWorkload ? true : !isHarvesterCluster(c);
12
12
  });
13
13
  }
@@ -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
 
package/utils/grafana.js CHANGED
@@ -63,14 +63,13 @@ export async function allDashboardsExist(store, clusterId, embeddedUrls, storeNa
63
63
 
64
64
  let monitoringVersion = '';
65
65
 
66
- if (!projectId) {
66
+ if (!projectId && store.getters[`${ storeName }/canList`](CATALOG.APP)) {
67
67
  try {
68
68
  res = await store.dispatch(`${ storeName }/find`, {
69
69
  type: CATALOG.APP,
70
70
  id: 'cattle-monitoring-system/rancher-monitoring'
71
71
  });
72
72
  } catch (err) {
73
- return false;
74
73
  }
75
74
 
76
75
  monitoringVersion = res?.currentVersion;
@@ -1,6 +1,6 @@
1
1
  // Helpers for determining if V2 or v1 Monitoring are installed
2
2
 
3
- import { SCHEMA, MONITORING, WORKLOAD_TYPES } from '@shell/config/types';
3
+ import { SCHEMA, MONITORING, WORKLOAD_TYPES, ENDPOINTS } from '@shell/config/types';
4
4
  import { normalizeType } from '@shell/plugins/dashboard-store/normalize';
5
5
  import { findBy } from '@shell/utils/array';
6
6
  import { isEmpty } from '@shell/utils/object';
@@ -65,6 +65,30 @@ export async function haveV1MonitoringWorkloads(store) {
65
65
  }
66
66
  }
67
67
 
68
+ async function hasEndpointSubsets(store, id) {
69
+ if (store.getters['cluster/schemaFor'](ENDPOINTS)) {
70
+ const endpoints = await store.dispatch('cluster/findAll', { type: ENDPOINTS }) || [];
71
+
72
+ const endpoint = endpoints.find((ep) => ep.id === id);
73
+
74
+ return endpoint && !isEmpty(endpoint) && !isEmpty(endpoint.subsets);
75
+ }
76
+
77
+ return false;
78
+ }
79
+
80
+ export async function canViewGrafanaLink(store) {
81
+ return await hasEndpointSubsets(store, `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-grafana`);
82
+ }
83
+
84
+ export async function canViewAlertManagerLink(store) {
85
+ return await hasEndpointSubsets(store, `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-alertmanager`);
86
+ }
87
+
88
+ export async function canViewPrometheusLink(store) {
89
+ return await hasEndpointSubsets(store, `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-prometheus`);
90
+ }
91
+
68
92
  // Other ways we check for monitoring:
69
93
 
70
94
  // (1) Using counts (requires RBAC permissions)
package/utils/object.js CHANGED
@@ -178,11 +178,12 @@ export function definedKeys(obj) {
178
178
  const val = obj[key];
179
179
 
180
180
  if ( Array.isArray(val) ) {
181
- return key;
181
+ return `"${ key }"`;
182
182
  } else if ( isObject(val) ) {
183
- return ( definedKeys(val) || [] ).map((subkey) => `${ key }.${ subkey }`);
183
+ // no need for quotes around the subkey since the recursive call will fill that in via one of the other two statements in the if block
184
+ return ( definedKeys(val) || [] ).map((subkey) => `"${ key }".${ subkey }`);
184
185
  } else {
185
- return key;
186
+ return `"${ key }"`;
186
187
  }
187
188
  });
188
189
 
package/utils/sort.js CHANGED
@@ -184,7 +184,7 @@ export function sortBy(ary, keys, desc) {
184
184
  keys = [keys];
185
185
  }
186
186
 
187
- return ary.slice().sort((objA, objB) => {
187
+ return (ary || []).slice().sort((objA, objB) => {
188
188
  for ( let i = 0 ; i < keys.length ; i++ ) {
189
189
  const parsed = parseField(keys[i]);
190
190
  const a = get(objA, parsed.field);
@@ -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
 
@@ -382,7 +382,7 @@ export default function(t: Translation, { key = 'Value' }: ValidationOptions): {
382
382
  if (val.some((rule: any) => isEmpty(rule.apiGroups))) {
383
383
  return t('validation.roleTemplate.roleTemplateRules.missingApiGroup');
384
384
  }
385
- } else if (val.some((rule: any) => isEmpty(rule.resources) && isEmpty(rule.nonResourceURLs) && isEmpty(rule.apiGroups))) {
385
+ } else if (val.some((rule: any) => isEmpty(rule.resources) && isEmpty(rule.nonResourceURLs))) {
386
386
  return t('validation.roleTemplate.roleTemplateRules.missingOneResource');
387
387
  }
388
388
 
@@ -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,
@@ -21,7 +21,7 @@ export function roleTemplateRules(rules = [], getters, errors, validatorArgs = [
21
21
  errors.push(getters['i18n/t']('validation.roleTemplate.roleTemplateRules.noResourceAndNonResource'));
22
22
  }
23
23
 
24
- if (rules.some((rule) => isEmpty(rule.resources) && isEmpty(rule.nonResourceURLs) && isEmpty(rule.apiGroups))) {
24
+ if (rules.some((rule) => isEmpty(rule.resources) && isEmpty(rule.nonResourceURLs))) {
25
25
  errors.push(getters['i18n/t']('validation.roleTemplate.roleTemplateRules.missingOneResource'));
26
26
  }
27
27
  }