@rancher/shell 0.3.23 → 0.3.25

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 (149) hide show
  1. package/assets/styles/base/_variables.scss +1 -0
  2. package/assets/styles/themes/_dark.scss +1 -0
  3. package/assets/styles/themes/_light.scss +6 -5
  4. package/assets/translations/en-us.yaml +44 -17
  5. package/assets/translations/zh-hans.yaml +2 -2
  6. package/components/ClusterIconMenu.vue +143 -0
  7. package/components/CruResource.vue +7 -1
  8. package/components/ExplorerProjectsNamespaces.vue +11 -1
  9. package/components/FixedBanner.vue +17 -1
  10. package/components/Loading.vue +1 -1
  11. package/components/Markdown.vue +1 -1
  12. package/components/Questions/__tests__/Yaml.test.ts +3 -2
  13. package/components/SideNav.vue +1 -1
  14. package/components/SortableTable/index.vue +3 -2
  15. package/components/auth/RoleDetailEdit.vue +15 -2
  16. package/components/auth/login/saml.vue +12 -1
  17. package/components/form/LabeledSelect.vue +12 -5
  18. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  19. package/components/form/Members/MembershipEditor.vue +6 -1
  20. package/components/form/SelectOrCreateAuthSecret.vue +7 -0
  21. package/components/form/__tests__/KeyValue.test.ts +6 -3
  22. package/components/form/__tests__/LabeledSelect.test.ts +18 -0
  23. package/components/formatter/PodsUsage.vue +11 -36
  24. package/components/formatter/PrincipalGroupBindings.vue +8 -5
  25. package/components/formatter/__tests__/PodsUsage.test.ts +36 -19
  26. package/components/nav/Group.vue +62 -34
  27. package/components/nav/Header.vue +13 -6
  28. package/components/nav/Pinned.vue +47 -0
  29. package/components/nav/TopLevelMenu.vue +673 -325
  30. package/components/nav/Type.vue +88 -8
  31. package/config/home-links.js +1 -1
  32. package/config/product/istio.js +15 -5
  33. package/config/router.js +3 -9
  34. package/config/table-headers.js +5 -6
  35. package/config/uiplugins.js +1 -0
  36. package/core/plugin-helpers.js +3 -0
  37. package/core/types.ts +6 -1
  38. package/creators/app/files/.vscode/settings.json +0 -1
  39. package/creators/pkg/init +2 -2
  40. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +118 -0
  41. package/detail/autoscaling.horizontalpodautoscaler/index.vue +4 -4
  42. package/detail/provisioning.cattle.io.cluster.vue +7 -5
  43. package/edit/__tests__/management.cattle.io.clusterroletemplatebinding.test.ts +58 -0
  44. package/edit/__tests__/namespace.test.ts +5 -3
  45. package/edit/fleet.cattle.io.gitrepo.vue +43 -15
  46. package/edit/logging.banzaicloud.io.output/index.vue +7 -0
  47. package/edit/management.cattle.io.clusterroletemplatebinding.vue +3 -11
  48. package/edit/namespace.vue +8 -4
  49. package/edit/provisioning.cattle.io.cluster/Basics.vue +662 -0
  50. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +9 -8
  51. package/edit/provisioning.cattle.io.cluster/DrainOptions.vue +13 -8
  52. package/edit/provisioning.cattle.io.cluster/MachinePool.vue +11 -2
  53. package/edit/provisioning.cattle.io.cluster/MemberRoles.vue +40 -0
  54. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +237 -0
  55. package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +71 -23
  56. package/edit/provisioning.cattle.io.cluster/__tests__/DrainOptions.test.ts +52 -0
  57. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -142
  58. package/edit/provisioning.cattle.io.cluster/rke2.vue +253 -582
  59. package/edit/workload/storage/ContainerMountPaths.vue +7 -5
  60. package/edit/workload/storage/__tests__/Storage.test.ts +2 -2
  61. package/edit/workload/storage/persistentVolumeClaim/__tests__/persistentvolumeclaim.test.ts +36 -0
  62. package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +15 -7
  63. package/initialize/App.js +2 -0
  64. package/initialize/client.js +63 -51
  65. package/initialize/index.js +7 -5
  66. package/layouts/default.vue +10 -2
  67. package/layouts/home.vue +6 -2
  68. package/layouts/plain.vue +9 -2
  69. package/list/fleet.cattle.io.cluster.vue +2 -2
  70. package/list/management.cattle.io.feature.vue +1 -1
  71. package/machine-config/amazonec2.vue +1 -0
  72. package/machine-config/vmwarevsphere.vue +48 -7
  73. package/mixins/brand.js +0 -8
  74. package/mixins/child-hook.js +2 -2
  75. package/mixins/create-edit-view/impl.js +3 -3
  76. package/mixins/fetch.client.js +3 -3
  77. package/models/__tests__/management.cattle.io.node.ts +96 -0
  78. package/models/__tests__/node.ts +74 -0
  79. package/models/cluster/node.js +6 -5
  80. package/models/cluster.x-k8s.io.machinedeployment.js +2 -2
  81. package/models/management.cattle.io.cluster.js +22 -1
  82. package/models/management.cattle.io.clusterroletemplatebinding.js +3 -3
  83. package/models/management.cattle.io.globalrole.js +17 -2
  84. package/models/management.cattle.io.node.js +6 -4
  85. package/models/management.cattle.io.projectroletemplatebinding.js +3 -3
  86. package/models/management.cattle.io.roletemplate.js +17 -2
  87. package/package.json +2 -6
  88. package/pages/__tests__/prefs.test.ts +1 -1
  89. package/pages/about.vue +2 -0
  90. package/pages/auth/setup.vue +5 -4
  91. package/pages/c/_cluster/explorer/ConfigBadge.vue +1 -0
  92. package/pages/c/_cluster/monitoring/index.vue +8 -3
  93. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +9 -66
  94. package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +182 -0
  95. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +15 -32
  96. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +8 -46
  97. package/pages/c/_cluster/uiplugins/index.vue +64 -64
  98. package/pages/diagnostic.vue +0 -39
  99. package/pages/home.vue +1 -1
  100. package/pages/prefs.vue +3 -13
  101. package/plugins/dashboard-store/normalize.js +4 -4
  102. package/plugins/dashboard-store/resource-class.js +1 -1
  103. package/plugins/int-number.js +5 -2
  104. package/plugins/positive-int-number.js +19 -0
  105. package/plugins/steve/__tests__/getters.spec.ts +15 -0
  106. package/plugins/steve/getters.js +22 -10
  107. package/public/index.html +4 -2
  108. package/rancher-components/BadgeState/BadgeState.vue +5 -1
  109. package/rancher-components/Banner/Banner.test.ts +51 -1
  110. package/rancher-components/Banner/Banner.vue +134 -53
  111. package/rancher-components/Card/Card.test.ts +37 -0
  112. package/rancher-components/Card/Card.vue +24 -7
  113. package/rancher-components/Form/Checkbox/Checkbox.test.ts +20 -29
  114. package/rancher-components/Form/Checkbox/Checkbox.vue +45 -20
  115. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +2 -8
  116. package/rancher-components/Form/LabeledInput/LabeledInput.vue +22 -10
  117. package/rancher-components/Form/Radio/RadioButton.test.ts +31 -0
  118. package/rancher-components/Form/Radio/RadioButton.vue +30 -13
  119. package/rancher-components/Form/Radio/RadioGroup.vue +26 -7
  120. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +7 -6
  121. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +25 -38
  122. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +23 -11
  123. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +19 -5
  124. package/rancher-components/StringList/StringList.test.ts +453 -49
  125. package/rancher-components/StringList/StringList.vue +92 -58
  126. package/scripts/extension/parse-tag-name +0 -0
  127. package/store/index.js +4 -0
  128. package/store/prefs.js +4 -4
  129. package/store/type-map.js +2 -16
  130. package/types/shell/index.d.ts +26 -14
  131. package/utils/__tests__/cluster.test.ts +55 -0
  132. package/utils/__tests__/object.test.ts +21 -2
  133. package/utils/__tests__/sort.test.ts +61 -0
  134. package/utils/cluster.js +47 -1
  135. package/utils/object.js +12 -5
  136. package/utils/string.js +12 -0
  137. package/utils/validators/formRules/__tests__/index.test.ts +13 -1
  138. package/utils/validators/formRules/index.ts +4 -0
  139. package/utils/validators/role-template.js +9 -1
  140. package/utils/version.js +1 -1
  141. package/vue.config.js +1 -4
  142. package/yarn-error.log +200 -0
  143. package/content/docs/en-us/getting-started.md +0 -224
  144. package/content/docs/en-us/whats-new.md +0 -29
  145. package/content/docs/zh-hans/getting-started.md +0 -224
  146. package/content/docs/zh-hans/whats-new.md +0 -28
  147. package/pages/docs/_doc.vue +0 -345
  148. package/pages/docs/toc.js +0 -27
  149. package/plugins/console.js +0 -34
@@ -75,9 +75,9 @@ async function $_fetch() { // eslint-disable-line camelcase
75
75
  try {
76
76
  await this.$options.fetch.call(this);
77
77
  } catch (err) {
78
- if (process.dev) {
79
- console.error('Error in fetch():', err); // eslint-disable-line no-console
80
- }
78
+ // In most cases we don't handle errors at all in `fetch`es. Lets always log to help in production
79
+ console.error('Error in fetch():', err); // eslint-disable-line no-console
80
+
81
81
  error = normalizeError(err);
82
82
  }
83
83
 
@@ -0,0 +1,96 @@
1
+ import MgmtNode from '@shell/models/management.cattle.io.node';
2
+
3
+ describe('class MgmtNode', () => {
4
+ const foo = 'foo';
5
+ const bar = 'bar';
6
+ const t = jest.fn(() => bar);
7
+ const ctx = { rootGetters: { 'i18n/t': t } };
8
+
9
+ const resetMocks = () => {
10
+ // Clear all mock function calls:
11
+ jest.clearAllMocks();
12
+ };
13
+
14
+ it('should not return addresses if they are not present in the resource status, the internalNodeStatus, or the rkeNode key in status', () => {
15
+ const mgmtNode = new MgmtNode({ status: {} });
16
+
17
+ expect(mgmtNode.addresses).toStrictEqual([]);
18
+ resetMocks();
19
+ });
20
+
21
+ describe('should return addresses', () => {
22
+ const addresses = [foo];
23
+
24
+ it('if they are present directly on the resource status', () => {
25
+ const mgmtNode = new MgmtNode({ status: { addresses } });
26
+
27
+ expect(mgmtNode.addresses).toStrictEqual(addresses);
28
+ });
29
+ it('if they are not present directly on the resource status but are on "status.internalNodeStatus"', () => {
30
+ const mgmtNode = new MgmtNode({ status: { internalNodeStatus: { addresses } } });
31
+
32
+ expect(mgmtNode.addresses).toStrictEqual(addresses);
33
+ });
34
+ });
35
+
36
+ describe('should return an internalIp', () => {
37
+ const addresses = [{ type: 'InternalIP', address: foo }];
38
+ const internalAddress = foo;
39
+
40
+ it('if addresses includes an object with an appropriate type and address', () => {
41
+ const mgmtNode = new MgmtNode({ status: { addresses } });
42
+
43
+ expect(mgmtNode.internalIp).toStrictEqual(foo);
44
+ });
45
+ it('if internalNodeStatus.addresses includes an object with an appropriate type and address', () => {
46
+ const mgmtNode = new MgmtNode({ status: { internalNodeStatus: { addresses } } });
47
+
48
+ expect(mgmtNode.internalIp).toStrictEqual(foo);
49
+ });
50
+ it('if addresses and internalNodeStatus.addresses do not provide an internal ip and the status includes an rkeNode key with an appropriate type and address', () => {
51
+ const mgmtNode = new MgmtNode({ status: { rkeNode: { internalAddress } } });
52
+
53
+ expect(mgmtNode.internalIp).toStrictEqual(internalAddress);
54
+ });
55
+ });
56
+
57
+ describe('should return an externalIp', () => {
58
+ const addresses = [{ type: 'ExternalIP', address: foo }];
59
+ const address = foo;
60
+
61
+ it('if addresses includes an object with an appropriate type and address', () => {
62
+ const mgmtNode = new MgmtNode({ status: { addresses } });
63
+
64
+ expect(mgmtNode.externalIp).toStrictEqual(foo);
65
+ });
66
+ it('if internalNodeStatus.addresses includes an object with an appropriate type and address', () => {
67
+ const mgmtNode = new MgmtNode({ status: { internalNodeStatus: { addresses } } });
68
+
69
+ expect(mgmtNode.externalIp).toStrictEqual(foo);
70
+ });
71
+ it('if addresses and internalNodeStatus.addresses do not provide an external ip and the status includes an rkeNode key with an appropriate type and address', () => {
72
+ const mgmtNode = new MgmtNode({ status: { rkeNode: { address } } });
73
+
74
+ expect(mgmtNode.externalIp).toStrictEqual(address);
75
+ });
76
+ });
77
+
78
+ describe('should return an appropriate message', () => {
79
+ it('if there is no internalIp to display', () => {
80
+ const mgmtNode = new MgmtNode({ status: {} }, ctx);
81
+
82
+ expect(mgmtNode.internalIp).toStrictEqual(bar);
83
+ expect(t).toHaveBeenCalledTimes(1);
84
+ expect(t).toHaveBeenCalledWith('generic.none');
85
+ resetMocks();
86
+ });
87
+ it('if there is no externalIp to display', () => {
88
+ const mgmtNode = new MgmtNode({ status: {} }, ctx);
89
+
90
+ expect(mgmtNode.externalIp).toStrictEqual(bar);
91
+ expect(t).toHaveBeenCalledTimes(1);
92
+ expect(t).toHaveBeenCalledWith('generic.none');
93
+ resetMocks();
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,74 @@
1
+ import Node from '@shell/models/management.cattle.io.node';
2
+
3
+ describe('class Node', () => {
4
+ const foo = 'foo';
5
+ const bar = 'bar';
6
+ const t = jest.fn(() => bar);
7
+ const ctx = { rootGetters: { 'i18n/t': t } };
8
+
9
+ const resetMocks = () => {
10
+ // Clear all mock function calls:
11
+ jest.clearAllMocks();
12
+ };
13
+
14
+ it('should not return addresses if they are not present in the resource status', () => {
15
+ const node = new Node({ status: {} });
16
+
17
+ expect(node.addresses).toStrictEqual([]);
18
+ resetMocks();
19
+ });
20
+
21
+ describe('should return addresses', () => {
22
+ const addresses = [foo];
23
+
24
+ it('if they are present directly on the resource status', () => {
25
+ const node = new Node({ status: { addresses } });
26
+
27
+ expect(node.addresses).toStrictEqual(addresses);
28
+ });
29
+ });
30
+
31
+ describe('should return an internalIp', () => {
32
+ const addresses = [{ type: 'InternalIP', address: foo }];
33
+
34
+ it('if addresses includes an object with an appropriate type and address', () => {
35
+ const node = new Node({ status: { addresses } });
36
+
37
+ expect(node.internalIp).toStrictEqual(foo);
38
+ });
39
+ });
40
+
41
+ describe('should return an externalIp', () => {
42
+ const addresses = [{ type: 'ExternalIP', address: foo }];
43
+
44
+ it('if addresses includes an object with an appropriate type and address', () => {
45
+ const node = new Node({ status: { addresses } });
46
+
47
+ expect(node.externalIp).toStrictEqual(foo);
48
+ });
49
+ it('if internalNodeStatus.addresses includes an object with an appropriate type and address', () => {
50
+ const node = new Node({ status: { internalNodeStatus: { addresses } } });
51
+
52
+ expect(node.externalIp).toStrictEqual(foo);
53
+ });
54
+ });
55
+
56
+ describe('should return an appropriate message', () => {
57
+ it('if there is no internalIp to display', () => {
58
+ const node = new Node({ status: {} }, ctx);
59
+
60
+ expect(node.internalIp).toStrictEqual(bar);
61
+ expect(t).toHaveBeenCalledTimes(1);
62
+ expect(t).toHaveBeenCalledWith('generic.none');
63
+ resetMocks();
64
+ });
65
+ it('if there is no externalIp to display', () => {
66
+ const node = new Node({ status: {} }, ctx);
67
+
68
+ expect(node.externalIp).toStrictEqual(bar);
69
+ expect(t).toHaveBeenCalledTimes(1);
70
+ expect(t).toHaveBeenCalledWith('generic.none');
71
+ resetMocks();
72
+ });
73
+ });
74
+ });
@@ -92,16 +92,17 @@ export default class ClusterNode extends SteveModel {
92
92
  return this.metadata.name;
93
93
  }
94
94
 
95
- get internalIp() {
96
- const addresses = this.status?.addresses || [];
95
+ get addresses() {
96
+ return this.status?.addresses || [];
97
+ }
97
98
 
98
- return findLast(addresses, (address) => address.type === 'InternalIP')?.address;
99
+ get internalIp() {
100
+ return findLast(this.addresses, (address) => address.type === 'InternalIP')?.address;
99
101
  }
100
102
 
101
103
  get externalIp() {
102
- const addresses = this.status?.addresses || [];
103
104
  const annotationAddress = this.metadata.annotations[RKE.EXTERNAL_IP];
104
- const statusAddress = findLast(addresses, (address) => address.type === 'ExternalIP')?.address;
105
+ const statusAddress = findLast(this.addresses, (address) => address.type === 'ExternalIP')?.address;
105
106
 
106
107
  return statusAddress || annotationAddress;
107
108
  }
@@ -127,11 +127,11 @@ export default class CapiMachineDeployment extends SteveModel {
127
127
  }
128
128
 
129
129
  this.scaleTimer = setTimeout(() => {
130
- this.cluster.save().catch((err) => {
130
+ this.cluster.save().catch(async(err) => {
131
131
  let errors = exceptionToErrorsArray(err);
132
132
 
133
133
  if ( err.status === 409 && depth < 2 ) {
134
- const conflicts = handleConflict(initialValue, value, liveModel, this.$rootGetters, this.$store);
134
+ const conflicts = await handleConflict(initialValue, value, liveModel, this.$rootGetters, { dispatch: this.$dispatch }, 'management');
135
135
 
136
136
  if ( conflicts === false ) {
137
137
  // It was automatically figured out, save again
@@ -1,7 +1,7 @@
1
1
  import Vue from 'vue';
2
2
  import { CATALOG, CLUSTER_BADGE } from '@shell/config/labels-annotations';
3
3
  import { NODE, FLEET, MANAGEMENT, CAPI } from '@shell/config/types';
4
- import { insertAt } from '@shell/utils/array';
4
+ import { insertAt, addObject, removeObject } from '@shell/utils/array';
5
5
  import { downloadFile } from '@shell/utils/download';
6
6
  import { parseSi } from '@shell/utils/units';
7
7
  import { parseColor, textColor } from '@shell/utils/color';
@@ -14,6 +14,7 @@ import { isHarvesterCluster } from '@shell/utils/cluster';
14
14
  import HybridModel from '@shell/plugins/steve/hybrid-class';
15
15
  import { LINUX, WINDOWS } from '@shell/store/catalog';
16
16
  import { KONTAINER_TO_DRIVER } from './management.cattle.io.kontainerdriver';
17
+ import { PINNED_CLUSTERS } from '@shell/store/prefs';
17
18
 
18
19
  // See translation file cluster.providers for list of providers
19
20
  // If the logo is not named with the provider name, add an override here
@@ -455,4 +456,24 @@ export default class MgmtCluster extends HybridModel {
455
456
 
456
457
  return findRelationship(verb === 'to' ? 'from' : 'to', CAPI.RANCHER_CLUSTER, this.metadata?.relationships);
457
458
  }
459
+
460
+ get pinned() {
461
+ return this.$rootGetters['prefs/get'](PINNED_CLUSTERS).includes(this.id);
462
+ }
463
+
464
+ pin() {
465
+ const types = this.$rootGetters['prefs/get'](PINNED_CLUSTERS) || [];
466
+
467
+ addObject(types, this.id);
468
+
469
+ this.$dispatch('prefs/set', { key: PINNED_CLUSTERS, value: types }, { root: true });
470
+ }
471
+
472
+ unpin() {
473
+ const types = this.$rootGetters['prefs/get'](PINNED_CLUSTERS) || [];
474
+
475
+ removeObject(types, this.id);
476
+
477
+ this.$dispatch('prefs/set', { key: PINNED_CLUSTERS, value: types }, { root: true });
478
+ }
458
479
  }
@@ -43,7 +43,7 @@ export default class CRTB extends HybridModel {
43
43
 
44
44
  get principalId() {
45
45
  // We've either set it ourselves or it's comes from native properties
46
- return this.principalName || this.userPrincipalName || this.groupPrincipalName;
46
+ return this.principalName || this.userPrincipalName || this.groupPrincipalName || '';
47
47
  }
48
48
 
49
49
  get nameDisplay() {
@@ -117,12 +117,12 @@ export default class CRTB extends HybridModel {
117
117
  get norman() {
118
118
  return (async() => {
119
119
  const principal = await this.principal;
120
- const principalProperty = principal.principalType === 'group' ? 'groupPrincipalId' : 'userPrincipalId';
120
+ const principalProperty = principal?.principalType === 'group' ? 'groupPrincipalId' : 'userPrincipalId';
121
121
 
122
122
  return this.$dispatch(`rancher/create`, {
123
123
  type: NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING,
124
124
  roleTemplateId: this.roleTemplateName,
125
- [principalProperty]: principal.id,
125
+ [principalProperty]: principal?.id,
126
126
  clusterId: this.clusterName,
127
127
  id: this.id?.replace('/', ':')
128
128
  }, { root: true });
@@ -4,7 +4,6 @@ import { CATTLE_API_GROUP, SUBTYPE_MAPPING, CREATE_VERBS } from '@shell/models/m
4
4
  import { uniq } from '@shell/utils/array';
5
5
  import { get } from '@shell/utils/object';
6
6
  import SteveDescriptionModel from '@shell/plugins/steve/steve-description-class';
7
- import Role from './rbac.authorization.k8s.io.role';
8
7
  import { AS, MODE, _CLONE, _UNFLAG } from '@shell/config/query-params';
9
8
 
10
9
  const BASE = 'user-base';
@@ -16,7 +15,14 @@ const GLOBAL = SUBTYPE_MAPPING.GLOBAL.key;
16
15
 
17
16
  export default class GlobalRole extends SteveDescriptionModel {
18
17
  get customValidationRules() {
19
- return Role.customValidationRules();
18
+ return [
19
+ {
20
+ path: 'rules',
21
+ validators: [`roleTemplateRules:${ this.type }`],
22
+ nullable: false,
23
+ type: 'array',
24
+ },
25
+ ];
20
26
  }
21
27
 
22
28
  get details() {
@@ -137,6 +143,15 @@ export default class GlobalRole extends SteveDescriptionModel {
137
143
  async save() {
138
144
  const norman = await this.norman;
139
145
 
146
+ for (const rule of norman.rules) {
147
+ if (rule.nonResourceURLs.length) {
148
+ delete rule.resources;
149
+ delete rule.apiGroups;
150
+ } else {
151
+ delete rule.nonResourceURLs;
152
+ }
153
+ }
154
+
140
155
  return norman.save();
141
156
  }
142
157
 
@@ -125,11 +125,14 @@ export default class MgmtNode extends HybridModel {
125
125
  return false;
126
126
  }
127
127
 
128
+ get addresses() {
129
+ return this.status?.addresses || this.status?.internalNodeStatus?.addresses || [];
130
+ }
131
+
128
132
  get internalIp() {
129
133
  // This shows in the IP address column for RKE1 nodes in the
130
134
  // list of nodes in the cluster detail page of Cluster Management.
131
-
132
- const internal = this.status?.addresses?.find(({ type }) => {
135
+ const internal = this.addresses.find(({ type }) => {
133
136
  return type === ADDRESSES.INTERNAL_IP;
134
137
  });
135
138
 
@@ -147,8 +150,7 @@ export default class MgmtNode extends HybridModel {
147
150
  }
148
151
 
149
152
  get externalIp() {
150
- const addresses = this.status?.addresses || [];
151
- const statusAddress = findLast(addresses, (address) => address.type === 'ExternalIP')?.address;
153
+ const statusAddress = findLast(this.addresses, (address) => address.type === 'ExternalIP')?.address;
152
154
 
153
155
  if (statusAddress) {
154
156
  return statusAddress;
@@ -31,7 +31,7 @@ export default class PRTB extends HybridModel {
31
31
 
32
32
  get principalId() {
33
33
  // We've either set it ourselves or it's comes from native properties
34
- return this.principalName || this.userPrincipalName || this.groupPrincipalName;
34
+ return this.principalName || this.userPrincipalName || this.groupPrincipalName || '';
35
35
  }
36
36
 
37
37
  get nameDisplay() {
@@ -123,12 +123,12 @@ export default class PRTB extends HybridModel {
123
123
  get norman() {
124
124
  return (async() => {
125
125
  const principal = await this.principal;
126
- const principalProperty = principal.principalType === 'group' ? 'groupPrincipalId' : 'userPrincipalId';
126
+ const principalProperty = principal?.principalType === 'group' ? 'groupPrincipalId' : 'userPrincipalId';
127
127
 
128
128
  return this.$dispatch(`rancher/create`, {
129
129
  type: NORMAN.PROJECT_ROLE_TEMPLATE_BINDING,
130
130
  roleTemplateId: this.roleTemplateName,
131
- [principalProperty]: principal.id,
131
+ [principalProperty]: principal?.id,
132
132
  projectId: this.projectName,
133
133
  projectRoleTemplateId: '',
134
134
  id: this.id?.replace('/', ':')
@@ -3,7 +3,6 @@ import { get } from '@shell/utils/object';
3
3
  import { DESCRIPTION } from '@shell/config/labels-annotations';
4
4
  import { NORMAN } from '@shell/config/types';
5
5
  import SteveDescriptionModel from '@shell/plugins/steve/steve-description-class';
6
- import Role from './rbac.authorization.k8s.io.role';
7
6
  import { AS, MODE, _CLONE, _UNFLAG } from '@shell/config/query-params';
8
7
 
9
8
  export const CATTLE_API_GROUP = '.cattle.io';
@@ -60,7 +59,14 @@ export const CREATE_VERBS = new Set(['PUT', 'blocked-PUT']);
60
59
 
61
60
  export default class RoleTemplate extends SteveDescriptionModel {
62
61
  get customValidationRules() {
63
- return Role.customValidationRules();
62
+ return [
63
+ {
64
+ path: 'rules',
65
+ validators: [`roleTemplateRules:${ this.type }`],
66
+ nullable: false,
67
+ type: 'array',
68
+ },
69
+ ];
64
70
  }
65
71
 
66
72
  get details() {
@@ -185,6 +191,15 @@ export default class RoleTemplate extends SteveDescriptionModel {
185
191
  async save() {
186
192
  const norman = await this.norman;
187
193
 
194
+ for (const rule of norman.rules) {
195
+ if (rule.nonResourceURLs.length) {
196
+ delete rule.resources;
197
+ delete rule.apiGroups;
198
+ } else {
199
+ delete rule.nonResourceURLs;
200
+ }
201
+ }
202
+
188
203
  return norman.save();
189
204
  }
190
205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancherlabs/dashboard",
6
6
  "license": "Apache-2.0",
@@ -17,7 +17,6 @@
17
17
  "lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .ts,.js,.vue .",
18
18
  "test": "./node_modules/.bin/nyc ava --serial --verbose",
19
19
  "dev": "./node_modules/.bin/vue-cli-service dev",
20
- "docker-dev": "docker run --rm --name dashboard-dev -p 8005:8005 -e API=$API -v $(pwd):/src -v dashboard_node:/src/node_modules rancher/dashboard:dev",
21
20
  "build": "./node_modules/.bin/vue-cli-service build",
22
21
  "analyze": "./node_modules/.bin/vue-cli-service build --report",
23
22
  "start": "./node_modules/.bin/vue-cli-service start",
@@ -38,7 +37,7 @@
38
37
  "@novnc/novnc": "1.2.0",
39
38
  "@nuxt/types": "2.14.6",
40
39
  "@nuxt/typescript-build": "2.1.0",
41
- "@nuxtjs/axios": "5.12.0",
40
+ "@nuxtjs/axios": "5.13.6",
42
41
  "@nuxtjs/eslint-config-typescript": "6.0.1",
43
42
  "@nuxtjs/webpack-profile": "0.1.0",
44
43
  "@popperjs/core": "2.4.4",
@@ -107,9 +106,6 @@
107
106
  "papaparse": "5.3.0",
108
107
  "portal-vue": "2.1.7",
109
108
  "rancher-icons": "rancher/icons#v2.0.16",
110
- "require-extension-hooks": "0.3.3",
111
- "require-extension-hooks-babel": "1.0.0",
112
- "require-extension-hooks-vue": "3.0.0",
113
109
  "sass": "1.51.0",
114
110
  "sass-loader": "10.2.1",
115
111
  "serve-static": "1.14.1",
@@ -1,5 +1,5 @@
1
- import { shallowMount } from '@vue/test-utils';
2
1
  import Preferences from '@shell/pages/prefs.vue';
2
+ import { shallowMount } from '@vue/test-utils';
3
3
 
4
4
  describe('page: prefs should', () => {
5
5
  it.each([
package/pages/about.vue CHANGED
@@ -101,6 +101,7 @@ export default {
101
101
  <n-link
102
102
  :to="{ name: 'diagnostic' }"
103
103
  class="btn role-primary"
104
+ data-testid="about__diagnostics_button"
104
105
  >
105
106
  {{ t('about.diagnostic.title') }}
106
107
  </n-link>
@@ -195,6 +196,7 @@ export default {
195
196
  <td>
196
197
  <a
197
198
  v-if="d.imageList"
199
+ :data-testid="`image_list_download_link__${d.label}`"
198
200
  @click="d.imageList"
199
201
  >
200
202
  {{ t('asyncButton.download.action') }}
@@ -17,6 +17,7 @@ import { exceptionToErrorsArray } from '@shell/utils/error';
17
17
  import Password from '@shell/components/form/Password';
18
18
  import { applyProducts } from '@shell/store/type-map';
19
19
  import BrandImage from '@shell/components/BrandImage';
20
+ import { waitFor } from '@shell/utils/async';
20
21
 
21
22
  const calcIsFirstLogin = (store) => {
22
23
  const firstLoginSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.FIRST_LOGIN);
@@ -250,10 +251,10 @@ export default {
250
251
 
251
252
  await Promise.all(promises);
252
253
 
253
- setTimeout(() => {
254
- buttonCb(true);
255
- this.done();
256
- }, 2000);
254
+ await waitFor(() => !calcIsFirstLogin(this.$store), 'first login to be completed', 10000, 1000, true);
255
+
256
+ buttonCb(true);
257
+ this.done();
257
258
  } catch (err) {
258
259
  console.error(err) ; // eslint-disable-line no-console
259
260
  buttonCb(false);
@@ -27,6 +27,7 @@ export default {
27
27
  <div>
28
28
  <a
29
29
  class="badge-install"
30
+ data-testid="add-custom-cluster-badge"
30
31
  @click="customBadgeDialog"
31
32
  >
32
33
  <i class="icon icon-cluster" />
@@ -94,12 +94,11 @@ export default {
94
94
  methods: {
95
95
  async fetchDeps() {
96
96
  const { $store, externalLinks } = this;
97
- const currentCluster = this.$store.getters['currentCluster'];
98
97
 
99
98
  this.v1Installed = await haveV1MonitoringWorkloads($store);
100
99
  const hash = await allHash({
100
+ apps: $store.dispatch('cluster/findAll', { type: CATALOG.APP }),
101
101
  endpoints: $store.dispatch('cluster/findAll', { type: ENDPOINTS }),
102
- app: $store.dispatch(`cluster/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' })
103
102
  });
104
103
 
105
104
  if (!isEmpty(hash.endpoints)) {
@@ -109,7 +108,13 @@ export default {
109
108
  (el) => el.group === 'prometheus'
110
109
  );
111
110
 
112
- grafanaMatch.link = `${ getClusterPrefix(hash.app?.currentVersion || '', currentCluster.id) }/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/`;
111
+ // Generate Grafana link
112
+ const currentCluster = this.$store.getters['currentCluster'];
113
+ const rancherMonitoring = !isEmpty(hash.apps) ? findBy(hash.apps, 'id', 'cattle-monitoring-system/rancher-monitoring') : '';
114
+ const clusterPrefix = getClusterPrefix(rancherMonitoring?.currentVersion || '', currentCluster.id);
115
+
116
+ grafanaMatch.link = `${ clusterPrefix }/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/`;
117
+
113
118
  const alertmanager = findBy(
114
119
  hash.endpoints,
115
120
  'id',
@@ -2,9 +2,7 @@
2
2
  import { mapGetters } from 'vuex';
3
3
  import isEmpty from 'lodash/isEmpty';
4
4
 
5
- import {
6
- CATALOG, SECRET, SERVICE, UI_PLUGIN, WORKLOAD_TYPES
7
- } from '@shell/config/types';
5
+ import { CATALOG, SECRET, SERVICE, WORKLOAD_TYPES } from '@shell/config/types';
8
6
  import { UI_PLUGIN_LABELS, UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins';
9
7
  import { TYPES as SECRET_TYPES } from '@shell/models/secret';
10
8
  import { allHash } from '@shell/utils/promise';
@@ -195,11 +193,15 @@ export default {
195
193
  }
196
194
 
197
195
  if (this.extensionRepo) {
198
- // Create uiplugin crd
199
- await this.loadPlugin(name, this.extensionUrl, image);
196
+ btnCb(true);
197
+ this.closeDialog();
198
+ this.$store.dispatch('growl/success', {
199
+ title: this.t('plugins.manageCatalog.imageLoad.success.title', { name }),
200
+ message: this.t('plugins.manageCatalog.imageLoad.success.message'),
201
+ timeout: 4000,
202
+ }, { root: true });
203
+ this.$emit('refresh');
200
204
  }
201
-
202
- btnCb(true);
203
205
  } else {
204
206
  throw new Error('Unable to determine image name');
205
207
  }
@@ -313,62 +315,6 @@ export default {
313
315
  }
314
316
  },
315
317
 
316
- async loadPlugin(name, url, image, btnCb) {
317
- // Try and parse version number from the image
318
- const version = this.extractImageVersion(image) || 'latest';
319
-
320
- if (!this.extractImageVersion(image)) {
321
- this.$store.dispatch('growl/warning', {
322
- title: this.t('plugins.manageCatalog.imageLoad.imageVersion.title'),
323
- message: this.t('plugins.manageCatalog.imageLoad.imageVersion.message', { image }),
324
- timeout: 4000,
325
- }, { root: true });
326
- }
327
-
328
- let crdName = name;
329
-
330
- const parts = name.split('-');
331
-
332
- if (parts.length >= 2) {
333
- crdName = parts.join('-');
334
- }
335
-
336
- this.extensionCrd = await this.$store.dispatch('management/create', {
337
- type: UI_PLUGIN,
338
- metadata: {
339
- name,
340
- namespace: UI_PLUGIN_NAMESPACE,
341
- labels: {
342
- [UI_PLUGIN_LABELS.CATALOG_IMAGE]: name,
343
- [UI_PLUGIN_LABELS.REPOSITORY]: this.extensionRepo.metadata.name
344
- }
345
- },
346
- spec: {
347
- plugin: {
348
- name: crdName,
349
- version,
350
- endpoint: url,
351
- noCache: false,
352
- metadata: { [UI_PLUGIN_LABELS.CATALOG]: 'true' }
353
- }
354
- }
355
- });
356
-
357
- try {
358
- await this.extensionCrd.save({ url: `/v1/${ UI_PLUGIN }`, method: 'POST' });
359
-
360
- this.closeDialog();
361
- this.$store.dispatch('growl/success', {
362
- title: this.t('plugins.manageCatalog.imageLoad.success.title', { name }),
363
- message: this.t('plugins.manageCatalog.imageLoad.success.message'),
364
- timeout: 4000,
365
- }, { root: true });
366
- } catch (e) {
367
- this.handleGrowlError(e, true);
368
- btnCb(false);
369
- }
370
- },
371
-
372
318
  parseDeploymentValues(name) {
373
319
  let out = {};
374
320
 
@@ -459,9 +405,6 @@ export default {
459
405
  if (this.extensionRepo) {
460
406
  this.extensionRepo.remove();
461
407
  }
462
- if (this.extensionCrd) {
463
- this.extensionCrd.remove();
464
- }
465
408
  },
466
409
 
467
410
  handleGrowlError(e, clean = false) {