@rancher/shell 0.3.25 → 0.3.27

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 (51) hide show
  1. package/.DS_Store +0 -0
  2. package/assets/translations/en-us.yaml +11 -3
  3. package/assets/translations/zh-hans.yaml +2 -3
  4. package/components/AlertTable.vue +8 -6
  5. package/components/CruResource.vue +7 -4
  6. package/components/EmberPage.vue +2 -2
  7. package/components/EtcdInfoBanner.vue +12 -2
  8. package/components/GlobalRoleBindings.vue +10 -0
  9. package/components/GrafanaDashboard.vue +8 -3
  10. package/components/Wizard.vue +17 -1
  11. package/components/__tests__/ProjectRow.test.ts +63 -0
  12. package/components/auth/RoleDetailEdit.vue +21 -1
  13. package/components/auth/__tests__/RoleDetailEdit.test.ts +41 -0
  14. package/components/form/ArrayList.vue +20 -11
  15. package/components/form/ResourceQuota/ProjectRow.vue +6 -2
  16. package/components/form/__tests__/ArrayList.test.ts +44 -0
  17. package/components/nav/Header.vue +5 -4
  18. package/components/nav/TopLevelMenu.vue +38 -15
  19. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -0
  20. package/components/nav/__tests__/Type.test.ts +139 -0
  21. package/config/private-label.js +1 -1
  22. package/config/settings.ts +0 -2
  23. package/core/types.ts +11 -4
  24. package/edit/provisioning.cattle.io.cluster/Basics.vue +13 -0
  25. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +1 -1
  26. package/edit/provisioning.cattle.io.cluster/rke2.vue +18 -2
  27. package/edit/workload/mixins/workload.js +14 -4
  28. package/models/fleet.cattle.io.cluster.js +11 -1
  29. package/models/management.cattle.io.globalrole.js +1 -1
  30. package/models/management.cattle.io.roletemplate.js +1 -1
  31. package/package.json +1 -1
  32. package/pages/c/_cluster/auth/roles/index.vue +11 -1
  33. package/pages/c/_cluster/explorer/index.vue +7 -2
  34. package/pages/c/_cluster/monitoring/index.vue +26 -39
  35. package/pages/support/index.vue +1 -8
  36. package/promptRemove/management.cattle.io.project.vue +6 -9
  37. package/rancher-components/components/Form/Radio/RadioGroup.test.ts +30 -0
  38. package/rancher-components/components/Form/Radio/RadioGroup.vue +4 -0
  39. package/scripts/extension/helm/charts/ui-plugin-server/Chart.yaml +2 -2
  40. package/store/features.js +1 -0
  41. package/types/shell/index.d.ts +4 -1
  42. package/utils/__tests__/object.test.ts +67 -1
  43. package/utils/__tests__/version.test.ts +13 -23
  44. package/utils/cluster.js +1 -1
  45. package/utils/grafana.js +1 -2
  46. package/utils/monitoring.js +25 -1
  47. package/utils/object.js +4 -3
  48. package/utils/sort.js +1 -1
  49. package/utils/validators/formRules/index.ts +1 -1
  50. package/utils/validators/role-template.js +1 -1
  51. package/utils/version.js +0 -13
@@ -1,4 +1,5 @@
1
1
  <script>
2
+ import { mapGetters } from 'vuex';
2
3
  import Tab from '@shell/components/Tabbed/Tab';
3
4
  import Tabbed from '@shell/components/Tabbed';
4
5
  import { MANAGEMENT } from '@shell/config/types';
@@ -7,6 +8,7 @@ import Loading from '@shell/components/Loading';
7
8
  import { SUBTYPE_MAPPING, CREATE_VERBS } from '@shell/models/management.cattle.io.roletemplate';
8
9
  import { NAME } from '@shell/config/product/auth';
9
10
  import { BLANK_CLUSTER } from '@shell/store/store-types.js';
11
+ import { Banner } from '@components/Banner';
10
12
 
11
13
  const GLOBAL = SUBTYPE_MAPPING.GLOBAL.key;
12
14
  const CLUSTER = SUBTYPE_MAPPING.CLUSTER.key;
@@ -31,7 +33,7 @@ export default {
31
33
  name: 'Roles',
32
34
 
33
35
  components: {
34
- Tab, Tabbed, ResourceTable, Loading
36
+ Tab, Tabbed, ResourceTable, Loading, Banner
35
37
  },
36
38
 
37
39
  async asyncData({ store }) {
@@ -100,6 +102,8 @@ export default {
100
102
  },
101
103
 
102
104
  computed: {
105
+ ...mapGetters(['releaseNotesUrl']),
106
+
103
107
  globalResources() {
104
108
  return this.globalRoles;
105
109
  },
@@ -181,6 +185,12 @@ export default {
181
185
  :weight="tabs[GLOBAL].weight"
182
186
  :label-key="tabs[GLOBAL].labelKey"
183
187
  >
188
+ <Banner
189
+ color="warning"
190
+ class="mb-20"
191
+ >
192
+ <span v-clean-html="t('rbac.globalRoles.role.restricted-admin.deprecation', { releaseNotesUrl }, true)" />
193
+ </Banner>
184
194
  <ResourceTable
185
195
  :schema="tabs[GLOBAL].schema"
186
196
  :rows="globalResources"
@@ -28,7 +28,7 @@ import {
28
28
  } from '@shell/config/table-headers';
29
29
 
30
30
  import { mapPref, PSP_DEPRECATION_BANNER } from '@shell/store/prefs';
31
- import { haveV1Monitoring, monitoringStatus } from '@shell/utils/monitoring';
31
+ import { haveV1Monitoring, monitoringStatus, canViewGrafanaLink } from '@shell/utils/monitoring';
32
32
  import Tabbed from '@shell/components/Tabbed';
33
33
  import Tab from '@shell/components/Tabbed/Tab';
34
34
  import { allDashboardsExist } from '@shell/utils/grafana';
@@ -103,6 +103,10 @@ export default {
103
103
  `Determine etcd metrics`
104
104
  );
105
105
 
106
+ // It's not enough to check that the grafana links are working for the current user; embedded cluster-level dashboards should only be shown if the user can view the grafana endpoint
107
+ // https://github.com/rancher/dashboard/issues/9792
108
+ setPromiseResult(canViewGrafanaLink(this.$store), this, 'canViewMetrics', 'Determine Grafana Permission');
109
+
106
110
  if (this.currentCluster.isLocal && this.$store.getters['management/schemaFor'](MANAGEMENT.NODE)) {
107
111
  this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE });
108
112
  }
@@ -125,6 +129,7 @@ export default {
125
129
  showClusterMetrics: false,
126
130
  showK8sMetrics: false,
127
131
  showEtcdMetrics: false,
132
+ canViewMetrics: false,
128
133
  CLUSTER_METRICS_DETAIL_URL,
129
134
  CLUSTER_METRICS_SUMMARY_URL,
130
135
  K8S_METRICS_DETAIL_URL,
@@ -346,7 +351,7 @@ export default {
346
351
  },
347
352
 
348
353
  hasMetricsTabs() {
349
- return this.showClusterMetrics || this.showK8sMetrics || this.showEtcdMetrics;
354
+ return this.canViewMetrics && ( this.showClusterMetrics || this.showK8sMetrics || this.showEtcdMetrics);
350
355
  },
351
356
 
352
357
  hasBadge() {
@@ -4,16 +4,14 @@ import isEmpty from 'lodash/isEmpty';
4
4
  import InstallRedirect from '@shell/utils/install-redirect';
5
5
  import AlertTable from '@shell/components/AlertTable';
6
6
  import { NAME, CHART_NAME } from '@shell/config/product/monitoring';
7
- import { CATALOG, ENDPOINTS, MONITORING } from '@shell/config/types';
7
+ import { CATALOG, MONITORING } from '@shell/config/types';
8
8
  import { allHash } from '@shell/utils/promise';
9
9
  import { findBy } from '@shell/utils/array';
10
10
  import { getClusterPrefix } from '@shell/utils/grafana';
11
11
  import { Banner } from '@components/Banner';
12
12
  import LazyImage from '@shell/components/LazyImage';
13
13
  import SimpleBox from '@shell/components/SimpleBox';
14
- import { haveV1MonitoringWorkloads } from '@shell/utils/monitoring';
15
-
16
- const CATTLE_MONITORING_NAMESPACE = 'cattle-monitoring-system';
14
+ import { haveV1MonitoringWorkloads, canViewAlertManagerLink, canViewGrafanaLink, canViewPrometheusLink } from '@shell/utils/monitoring';
17
15
 
18
16
  export default {
19
17
  components: {
@@ -96,52 +94,41 @@ export default {
96
94
  const { $store, externalLinks } = this;
97
95
 
98
96
  this.v1Installed = await haveV1MonitoringWorkloads($store);
99
- const hash = await allHash({
100
- apps: $store.dispatch('cluster/findAll', { type: CATALOG.APP }),
101
- endpoints: $store.dispatch('cluster/findAll', { type: ENDPOINTS }),
102
- });
97
+ const hash = {};
98
+
99
+ if ($store.getters['cluster/canList'](CATALOG.APP)) {
100
+ hash.apps = $store.dispatch('cluster/findAll', { type: CATALOG.APP });
101
+ }
102
+ const res = await allHash(hash);
103
+
104
+ const canViewAlertManager = await canViewAlertManagerLink(this.$store);
105
+ const canViewGrafana = await canViewGrafanaLink(this.$store);
106
+ const canViewPrometheus = await canViewPrometheusLink(this.$store);
103
107
 
104
- if (!isEmpty(hash.endpoints)) {
108
+ if (canViewAlertManager) {
105
109
  const amMatch = findBy(externalLinks, 'group', 'alertmanager');
106
- const grafanaMatch = findBy(externalLinks, 'group', 'grafana');
107
- const promeMatch = externalLinks.filter(
108
- (el) => el.group === 'prometheus'
109
- );
110
110
 
111
+ amMatch.enabled = true;
112
+ }
113
+ if (canViewGrafana) {
114
+ const grafanaMatch = findBy(externalLinks, 'group', 'grafana');
111
115
  // Generate Grafana link
112
116
  const currentCluster = this.$store.getters['currentCluster'];
113
- const rancherMonitoring = !isEmpty(hash.apps) ? findBy(hash.apps, 'id', 'cattle-monitoring-system/rancher-monitoring') : '';
117
+ const rancherMonitoring = !isEmpty(res.apps) ? findBy(res.apps, 'id', 'cattle-monitoring-system/rancher-monitoring') : '';
114
118
  const clusterPrefix = getClusterPrefix(rancherMonitoring?.currentVersion || '', currentCluster.id);
115
119
 
116
120
  grafanaMatch.link = `${ clusterPrefix }/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/`;
121
+ grafanaMatch.enabled = true;
122
+ }
117
123
 
118
- const alertmanager = findBy(
119
- hash.endpoints,
120
- 'id',
121
- `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-alertmanager`
122
- );
123
- const grafana = findBy(
124
- hash.endpoints,
125
- 'id',
126
- `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-grafana`
127
- );
128
- const prometheus = findBy(
129
- hash.endpoints,
130
- 'id',
131
- `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-prometheus`
124
+ if (canViewPrometheus) {
125
+ const promeMatch = externalLinks.filter(
126
+ (el) => el.group === 'prometheus'
132
127
  );
133
128
 
134
- if (!isEmpty(alertmanager) && !isEmpty(alertmanager.subsets)) {
135
- amMatch.enabled = true;
136
- }
137
- if (!isEmpty(grafana) && !isEmpty(grafana.subsets)) {
138
- grafanaMatch.enabled = true;
139
- }
140
- if (!isEmpty(prometheus) && !isEmpty(prometheus.subsets)) {
141
- promeMatch.forEach((match) => {
142
- match.enabled = true;
143
- });
144
- }
129
+ promeMatch.forEach((match) => {
130
+ match.enabled = true;
131
+ });
145
132
  }
146
133
  },
147
134
  },
@@ -8,7 +8,6 @@ import { SETTING } from '@shell/config/settings';
8
8
  import { addParam } from '@shell/utils/url';
9
9
  import { isRancherPrime } from '@shell/config/version';
10
10
  import { hasCspAdapter } from 'mixins/brand';
11
- import { generateSupportLink } from '@shell/utils/version';
12
11
 
13
12
  export default {
14
13
  layout: 'home',
@@ -112,12 +111,6 @@ export default {
112
111
 
113
112
  sccLink() {
114
113
  return this.hasAWSSupport ? addParam('https://scc.suse.com', 'from_marketplace', '1') : 'https://scc.suse.com';
115
- },
116
-
117
- supportLink() {
118
- const version = this.settings?.find((s) => s.id === SETTING.VERSION_RANCHER)?.value;
119
-
120
- return generateSupportLink(version);
121
114
  }
122
115
  },
123
116
 
@@ -139,7 +132,7 @@ export default {
139
132
  <div class="support-link">
140
133
  <a
141
134
  class="support-link"
142
- :href="supportLink"
135
+ href="https://www.rancher.com/support"
143
136
  target="_blank"
144
137
  rel="noopener noreferrer nofollow"
145
138
  >{{ t('support.community.learnMore') }}</a>
@@ -71,12 +71,9 @@ export default {
71
71
  names() {
72
72
  return this.filteredNamespaces.map((obj) => obj.nameDisplay).slice(0, 5);
73
73
  },
74
-
75
- canManageNamespaces() {
76
- // Only admins and cluster owners can see namespaces outside of projects
77
- // BUT cluster members can also manage projects and namespaces and may want to not delete the namespaces associated with the project
78
- // as per https://github.com/rancher/dashboard/issues/9517 despite the namespaces cannot be seen afterwards (projectless)
79
- return this.currentCluster.canUpdate || (this.currentProject.canDelete && this.filteredNamespaces.length && this.filteredNamespaces[0]?.canDelete);
74
+ // Only admins and cluster owners can see namespaces outside of projects
75
+ canSeeProjectlessNamespaces() {
76
+ return this.currentCluster.canUpdate;
80
77
  }
81
78
  },
82
79
  methods: {
@@ -84,7 +81,7 @@ export default {
84
81
  remove() {
85
82
  // Delete all of thre namespaces and return false - this tells the prompt remove dialog to continue and delete the project
86
83
  // Delete all namespaces if the user wouldn't be able to see them after deleting the project
87
- if (this.deleteProjectNamespaces || !this.canManageNamespaces) {
84
+ if (this.deleteProjectNamespaces || !this.canSeeProjectlessNamespaces) {
88
85
  return Promise.all(this.filteredNamespaces.map((n) => n.remove())).then(() => false);
89
86
  }
90
87
 
@@ -100,7 +97,7 @@ export default {
100
97
  <div>
101
98
  <div class="mb-10">
102
99
  {{ t('promptRemove.attemptingToRemove', { type }) }} <span class="display-name">{{ `${displayName}.` }}</span>
103
- <template v-if="!canManageNamespaces">
100
+ <template v-if="!canSeeProjectlessNamespaces">
104
101
  <span class="delete-warning"> {{ t('promptRemove.willDeleteAssociatedNamespaces') }}</span> <br>
105
102
  <div
106
103
  v-clean-html="resourceNames(names, plusMore, t)"
@@ -109,7 +106,7 @@ export default {
109
106
  </template>
110
107
  </div>
111
108
  <div
112
- v-if="filteredNamespaces.length > 0 && canManageNamespaces"
109
+ v-if="filteredNamespaces.length > 0 && canSeeProjectlessNamespaces"
113
110
  class="mt-20 remove-project-dialog"
114
111
  >
115
112
  <Checkbox
@@ -0,0 +1,30 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { RadioGroup } from './index';
3
+
4
+ describe('component: RadioGroup', () => {
5
+ describe('when disabled', () => {
6
+ it.each([true, false])('should expose disabled slot prop for indexed slots for %p', (disabled) => {
7
+ const wrapper = mount(RadioGroup, {
8
+ propsData: {
9
+ name: 'whatever',
10
+ options: [{ label: 'whatever', value: 'whatever' }],
11
+ disabled
12
+ },
13
+ scopedSlots: {
14
+ 0(props: {isDisabled: boolean}) {
15
+ return this.$createElement('input', {
16
+ attrs: {
17
+ id: 'test',
18
+ disabled: props.isDisabled
19
+ }
20
+ });
21
+ }
22
+ }
23
+ });
24
+
25
+ const slot = wrapper.find('#test').element as HTMLInputElement;
26
+
27
+ expect(slot.disabled).toBe(disabled);
28
+ });
29
+ });
30
+ });
@@ -169,6 +169,7 @@ export default Vue.extend({
169
169
 
170
170
  <template>
171
171
  <div>
172
+ <!-- Label -->
172
173
  <div
173
174
  v-if="label || labelKey || tooltip || tooltipKey || $slots.label"
174
175
  class="radio-group label"
@@ -195,6 +196,8 @@ export default Vue.extend({
195
196
  </h3>
196
197
  </slot>
197
198
  </div>
199
+
200
+ <!-- Group -->
198
201
  <div
199
202
  class="radio-group"
200
203
  :class="{'row':row}"
@@ -212,6 +215,7 @@ export default Vue.extend({
212
215
  :is-disabled="isDisabled"
213
216
  :name="i"
214
217
  >
218
+ <!-- Default input -->
215
219
  <RadioButton
216
220
  :key="name+'-'+i"
217
221
  :name="name"
@@ -1,10 +1,10 @@
1
1
  annotations:
2
2
  catalog.cattle.io/certified: rancher # Any application we are adding as a helm chart
3
- catalog.cattle.io/kube-version: '>= 1.16.0-0 < 1.28.0-0'
3
+ catalog.cattle.io/kube-version: '>= 1.16.0-0 < 1.29.0-0'
4
4
  catalog.cattle.io/namespace: cattle-ui-plugin-system # Must prefix with cattle- and suffix with -system=
5
5
  catalog.cattle.io/os: linux
6
6
  catalog.cattle.io/permits-os: linux, windows
7
- catalog.cattle.io/rancher-version: '>= 2.7.0-0 < 2.8.0-0'
7
+ catalog.cattle.io/rancher-version: '>= 2.7.0-0 < 2.9.0-0'
8
8
  catalog.cattle.io/scope: management
9
9
  catalog.cattle.io/ui-component: plugins
10
10
  apiVersion: v2
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);
@@ -2807,6 +2807,7 @@ export const UNSUPPORTED_STORAGE_DRIVERS: any;
2807
2807
  export const FLEET: any;
2808
2808
  export const HARVESTER: any;
2809
2809
  export const HARVESTER_CONTAINER: any;
2810
+ export const FLEET_WORKSPACE_BACK: any;
2810
2811
  export namespace getters {
2811
2812
  function get(state: any, getters: any, rootState: any, rootGetters: any): (name: any) => any;
2812
2813
  }
@@ -3444,6 +3445,9 @@ export function monitoringStatus(): {
3444
3445
  export function haveV2Monitoring(getters: any): boolean;
3445
3446
  export function haveV1Monitoring(getters: any): boolean;
3446
3447
  export function haveV1MonitoringWorkloads(store: any): Promise<boolean>;
3448
+ export function canViewGrafanaLink(store: any): Promise<boolean>;
3449
+ export function canViewAlertManagerLink(store: any): Promise<boolean>;
3450
+ export function canViewPrometheusLink(store: any): Promise<boolean>;
3447
3451
  }
3448
3452
 
3449
3453
  // @shell/utils/namespace-filter
@@ -4188,7 +4192,6 @@ export function seenReleaseNotes(store: any): boolean;
4188
4192
  export function markSeenReleaseNotes(store: any): Promise<void>;
4189
4193
  export function readReleaseNotes(store: any): boolean;
4190
4194
  export function markReadReleaseNotes(store: any): Promise<void>;
4191
- export function generateSupportLink(version: any): string;
4192
4195
  }
4193
4196
 
4194
4197
  // @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
  }
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);
@@ -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
 
@@ -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
  }
package/utils/version.js CHANGED
@@ -125,16 +125,3 @@ export async function markReadReleaseNotes(store) {
125
125
  await store.dispatch('prefs/set', { key: READ_WHATS_NEW, value: getVersionInfo(store).fullVersion });
126
126
  }
127
127
  }
128
-
129
- export function generateSupportLink(version) {
130
- const defaultSupportURL = 'https://rancher.com/support-maintenance-terms';
131
-
132
- if (!version || isDevBuild(version)) {
133
- return defaultSupportURL;
134
- }
135
-
136
- const baseUrl = 'https://www.suse.com/suse-rancher/support-matrix/all-supported-versions/rancher-';
137
- const formattedVersion = version.split('.').join('-');
138
-
139
- return baseUrl + formattedVersion;
140
- }