@rancher/shell 0.3.26 → 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 (43) hide show
  1. package/.DS_Store +0 -0
  2. package/assets/translations/en-us.yaml +4 -3
  3. package/assets/translations/zh-hans.yaml +2 -3
  4. package/components/AlertTable.vue +8 -6
  5. package/components/EmberPage.vue +2 -2
  6. package/components/EtcdInfoBanner.vue +12 -2
  7. package/components/GlobalRoleBindings.vue +10 -0
  8. package/components/GrafanaDashboard.vue +8 -3
  9. package/components/Wizard.vue +17 -1
  10. package/components/auth/RoleDetailEdit.vue +17 -1
  11. package/components/form/ArrayList.vue +20 -11
  12. package/components/form/__tests__/ArrayList.test.ts +44 -0
  13. package/components/nav/Header.vue +5 -4
  14. package/components/nav/TopLevelMenu.vue +38 -15
  15. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -0
  16. package/components/nav/__tests__/Type.test.ts +139 -0
  17. package/config/private-label.js +1 -1
  18. package/config/settings.ts +0 -2
  19. package/core/types.ts +11 -4
  20. package/edit/provisioning.cattle.io.cluster/Basics.vue +13 -0
  21. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +1 -1
  22. package/edit/workload/mixins/workload.js +14 -4
  23. package/models/fleet.cattle.io.cluster.js +11 -1
  24. package/package.json +1 -1
  25. package/pages/c/_cluster/auth/roles/index.vue +11 -1
  26. package/pages/c/_cluster/explorer/index.vue +7 -2
  27. package/pages/c/_cluster/monitoring/index.vue +26 -39
  28. package/pages/support/index.vue +1 -8
  29. package/promptRemove/management.cattle.io.project.vue +6 -9
  30. package/rancher-components/components/Form/Radio/RadioGroup.test.ts +30 -0
  31. package/rancher-components/components/Form/Radio/RadioGroup.vue +4 -0
  32. package/store/features.js +1 -0
  33. package/types/shell/index.d.ts +4 -1
  34. package/utils/__tests__/object.test.ts +67 -1
  35. package/utils/__tests__/version.test.ts +13 -23
  36. package/utils/cluster.js +1 -1
  37. package/utils/grafana.js +1 -2
  38. package/utils/monitoring.js +25 -1
  39. package/utils/object.js +4 -3
  40. package/utils/sort.js +1 -1
  41. package/utils/validators/formRules/index.ts +1 -1
  42. package/utils/validators/role-template.js +1 -1
  43. package/utils/version.js +0 -13
@@ -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"
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
- }