@rancher/shell 3.0.1-rc.2 → 3.0.1-rc.3

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.
@@ -32,4 +32,3 @@
32
32
 
33
33
  @import "./vendor/vue-select";
34
34
  @import "./vendor/code-mirror";
35
- @import 'node_modules/xterm/css/xterm.css';
@@ -1065,7 +1065,7 @@ catalog:
1065
1065
  target:
1066
1066
  git: Git repository containing Helm chart or cluster template definitions
1067
1067
  http: http(s) URL to an index generated by Helm
1068
- oci: OCI Repository <span class="oci-experimental-badge text-bold ml-5">Experimental</span>
1068
+ oci: OCI Repository
1069
1069
  label: Target
1070
1070
  url:
1071
1071
  label: Index URL
@@ -4556,9 +4556,6 @@ rbac:
4556
4556
  admin:
4557
4557
  label: 管理员
4558
4558
  description: 管理员可以完全控制整个安装以及所有集群中的所有资源。
4559
- restricted-admin:
4560
- label: 受限管理员
4561
- description: 受限管理员可以完全控制所有下游集群的所有资源,但不能访问本地集群。
4562
4559
  user:
4563
4560
  label: 普通用户
4564
4561
  description: 普通用户可以创建集群,并管理其授权访问的集群和项目。
@@ -108,7 +108,6 @@ export default {
108
108
  // This not only identifies global roles but the order here is the order we want to display them in the UI
109
109
  globalPermissions: [
110
110
  'admin',
111
- 'restricted-admin',
112
111
  'user',
113
112
  'user-base',
114
113
  ],
@@ -121,7 +120,6 @@ export default {
121
120
  };
122
121
  },
123
122
  computed: {
124
- ...mapGetters(['releaseNotesUrl']),
125
123
  ...mapGetters({ t: 'i18n/t' }),
126
124
 
127
125
  isCreate() {
@@ -375,11 +373,6 @@ export default {
375
373
  </div>
376
374
  </template>
377
375
  </Checkbox>
378
- <p
379
- v-if="role.id === 'restricted-admin'"
380
- v-clean-html="t('rbac.globalRoles.role.restricted-admin.deprecation', { releaseNotesUrl }, true)"
381
- class="deprecation-notice"
382
- />
383
376
  </div>
384
377
  </div>
385
378
  </template>
@@ -429,7 +429,7 @@ export default {
429
429
  color: var(--body-text);
430
430
 
431
431
  .table-header-container {
432
- display: flex;
432
+ display: inline-flex;
433
433
 
434
434
  .content {
435
435
  display: flex;
@@ -1,5 +1,4 @@
1
1
  <script>
2
- import { mapGetters } from 'vuex';
3
2
  import { MANAGEMENT, RBAC } from '@shell/config/types';
4
3
  import CruResource from '@shell/components/CruResource';
5
4
  import CreateEditView from '@shell/mixins/create-edit-view';
@@ -15,7 +14,6 @@ import { ucFirst } from '@shell/utils/string';
15
14
  import SortableTable from '@shell/components/SortableTable';
16
15
  import { _CLONE, _DETAIL } from '@shell/config/query-params';
17
16
  import { SCOPED_RESOURCES, SCOPED_RESOURCE_GROUPS } from '@shell/config/roles';
18
- import { Banner } from '@components/Banner';
19
17
  import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
20
18
 
21
19
  import { SUBTYPE_MAPPING, VERBS } from '@shell/models/management.cattle.io.roletemplate';
@@ -66,7 +64,6 @@ export default {
66
64
  SortableTable,
67
65
  Loading,
68
66
  Error,
69
- Banner,
70
67
  LabeledInput
71
68
  },
72
69
 
@@ -163,12 +160,6 @@ export default {
163
160
  },
164
161
 
165
162
  computed: {
166
- ...mapGetters(['releaseNotesUrl']),
167
-
168
- showRestrictedAdminDeprecationBanner() {
169
- return this.value.subtype === GLOBAL && this.value.id === 'restricted-admin';
170
- },
171
-
172
163
  label() {
173
164
  return this.t(`rbac.roletemplate.subtypes.${ this.value.subtype }.label`);
174
165
  },
@@ -559,13 +550,6 @@ export default {
559
550
  @finish="save"
560
551
  @cancel="cancel"
561
552
  >
562
- <Banner
563
- v-if="showRestrictedAdminDeprecationBanner"
564
- color="warning"
565
- class="mb-20"
566
- >
567
- <span v-clean-html="t('rbac.globalRoles.role.restricted-admin.deprecation', { releaseNotesUrl }, true)" />
568
- </Banner>
569
553
  <template v-if="isDetail">
570
554
  <SortableTable
571
555
  key-field="index"
@@ -287,12 +287,3 @@ export default {
287
287
  />
288
288
  </form>
289
289
  </template>
290
-
291
- <style lang="scss">
292
- span.oci-experimental-badge {
293
- background-color: var(--warning);
294
- color: var(--darker-active-bg);
295
- font-size: 12px;
296
- padding: 2px 6px;
297
- }
298
- </style>
@@ -22,11 +22,10 @@ import { _CREATE, _EDIT, _VIEW } from '@shell/config/query-params';
22
22
  import { findBy, removeObject, clear } from '@shell/utils/array';
23
23
  import { createYaml } from '@shell/utils/create-yaml';
24
24
  import {
25
- clone, diff, set, get, isEmpty
25
+ clone, diff, set, get, isEmpty, mergeWithReplaceArrays
26
26
  } from '@shell/utils/object';
27
27
  import { allHash } from '@shell/utils/promise';
28
28
  import { sortBy } from '@shell/utils/sort';
29
- import { vspherePoolConfigMerge } from '@shell/machine-config/vmwarevsphere-pool-config-merge';
30
29
 
31
30
  import { compare, sortable } from '@shell/utils/version';
32
31
  import { isHarvesterSatisfiesVersion, labelForAddon } from '@shell/utils/cluster';
@@ -1260,7 +1259,7 @@ export default {
1260
1259
  delete clonedCurrentConfig.metadata;
1261
1260
 
1262
1261
  if (this.provider === VMWARE_VSPHERE) {
1263
- machinePool.config = vspherePoolConfigMerge(clonedLatestConfig, clonedCurrentConfig);
1262
+ machinePool.config = mergeWithReplaceArrays(clonedLatestConfig, clonedCurrentConfig);
1264
1263
  } else {
1265
1264
  machinePool.config = merge(clonedLatestConfig, clonedCurrentConfig);
1266
1265
  }
@@ -1644,7 +1643,7 @@ export default {
1644
1643
  const defaultChartValue = this.versionInfo[name];
1645
1644
  const key = this.chartVersionKey(name);
1646
1645
 
1647
- return merge({}, defaultChartValue?.values || {}, this.userChartValues[key] || {});
1646
+ return mergeWithReplaceArrays(defaultChartValue?.values, this.userChartValues[key]);
1648
1647
  },
1649
1648
 
1650
1649
  initServerAgentArgs() {
@@ -4,28 +4,6 @@ jest.mock('@shell/utils/clipboard', () => {
4
4
  return { copyTextToClipboard: jest.fn(() => Promise.resolve({})) };
5
5
  });
6
6
 
7
- const importedRKE2ClusterInfo = { status: { driver: 'rke2', provider: 'rke2' } };
8
-
9
- const provisionedRKE2ClusterInfo = { status: { driver: 'rke2', provider: 'imported' } };
10
-
11
- const importedK3sClusterInfo = { status: { driver: 'k3s', provider: 'k3s' } };
12
-
13
- const provisionedK3sClusterInfo = { status: { driver: 'k3s', provider: 'imported' } };
14
-
15
- const importedAksClusterInfo = { spec: { aksConfig: { imported: true } }, status: { provider: 'aks', driver: 'AKS' } };
16
-
17
- const provisionedAksClusterInfo = { spec: { aksConfig: { imported: false } }, status: { provider: 'aks', driver: 'AKS' } };
18
-
19
- const importedRKE1ClusterInfo = { status: { provider: 'rke', driver: 'imported' } };
20
-
21
- const provisionedRKE1ClusterInfo = { status: { provider: 'rke', driver: 'rancherKubernetesEngine' } };
22
-
23
- const localRKE1ClusterInfo = { status: { provider: 'rke', driver: 'imported' } };
24
-
25
- const localRKE2ClusterInfo = { status: { provider: 'rke2', driver: 'rke2' } };
26
-
27
- const localEKSClusterInfo = { status: { provider: 'eks', driver: 'imported' } };
28
-
29
7
  describe('class MgmtCluster', () => {
30
8
  describe('provisioner', () => {
31
9
  const testCases = [
@@ -42,24 +20,4 @@ describe('class MgmtCluster', () => {
42
20
  }
43
21
  );
44
22
  });
45
-
46
- describe('isImported', () => {
47
- it.each([
48
- [importedRKE2ClusterInfo, true],
49
- [provisionedRKE2ClusterInfo, false],
50
- [importedK3sClusterInfo, true],
51
- [provisionedK3sClusterInfo, false],
52
- [importedAksClusterInfo, true],
53
- [provisionedAksClusterInfo, false],
54
- [importedRKE1ClusterInfo, true],
55
- [provisionedRKE1ClusterInfo, false],
56
- [localRKE1ClusterInfo, true],
57
- [localRKE2ClusterInfo, true],
58
- [localEKSClusterInfo, true]
59
- ])('should return isImported based on props data', (clusterData, expected) => {
60
- const cluster = new MgmtCluster(clusterData);
61
-
62
- expect(cluster.isImported).toBe(expected);
63
- });
64
- });
65
23
  });
@@ -89,30 +89,6 @@ export default class MgmtCluster extends SteveModel {
89
89
  return pools.filter((x) => x.spec?.clusterName === this.id);
90
90
  }
91
91
 
92
- get isImported() {
93
- if (this.isLocal) {
94
- return false;
95
- }
96
- // imported rke2 and k3s have status.driver === rke2 and k3s respectively
97
- // Provisioned rke2 and k3s have status.driver === imported
98
- if (this.status?.provider === 'k3s' || this.status?.provider === 'rke2') {
99
- return this.status?.driver === this.status?.provider;
100
- }
101
-
102
- // imported KEv2
103
- const kontainerConfigs = ['aksConfig', 'eksConfig', 'gkeConfig'];
104
-
105
- const isImportedKontainer = kontainerConfigs.filter((key) => {
106
- return this.spec?.[key]?.imported === true;
107
- }).length;
108
-
109
- if (isImportedKontainer) {
110
- return true;
111
- }
112
-
113
- return this.provisioner === 'imported';
114
- }
115
-
116
92
  get provisioner() {
117
93
  // For imported K3s clusters, this.status.driver is 'k3s.'
118
94
  return this.status?.driver ? this.status.driver : 'imported';
@@ -117,6 +117,7 @@ export default class GlobalRole extends SteveDescriptionModel {
117
117
  norman.id = this.id;
118
118
  norman.name = this.displayName;
119
119
  norman.description = this.description;
120
+ norman.inheritedClusterRoles = this.inheritedClusterRoles;
120
121
 
121
122
  return norman;
122
123
  })();
@@ -282,7 +282,29 @@ export default class ProvCluster extends SteveModel {
282
282
  }
283
283
 
284
284
  get isImported() {
285
- return this.mgmt?.isImported;
285
+ if (this.isLocal) {
286
+ return false;
287
+ }
288
+
289
+ // imported rke2 and k3s have status.driver === rke2 and k3s respectively
290
+ // Provisioned rke2 and k3s have status.driver === imported
291
+ if (this.mgmt?.status?.provider === 'k3s' || this.mgmt?.status?.provider === 'rke2') {
292
+ return this.mgmt?.status?.driver === this.mgmt?.status?.provider;
293
+ }
294
+
295
+ // imported KEv2
296
+ // we can't rely on this.provisioner to determine imported-ness for these clusters, as it will return 'aks' 'eks' 'gke' for both provisioned and imported clusters
297
+ const kontainerConfigs = ['aksConfig', 'eksConfig', 'gkeConfig'];
298
+
299
+ const isImportedKontainer = kontainerConfigs.filter((key) => {
300
+ return this.mgmt?.spec?.[key]?.imported === true;
301
+ }).length;
302
+
303
+ if (isImportedKontainer) {
304
+ return true;
305
+ }
306
+
307
+ return this.provisioner === 'imported';
286
308
  }
287
309
 
288
310
  get isCustom() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "3.0.1-rc.2",
3
+ "version": "3.0.1-rc.3",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancherlabs/dashboard",
6
6
  "license": "Apache-2.0",
@@ -1,5 +1,4 @@
1
1
  <script>
2
- import { mapGetters } from 'vuex';
3
2
  import Tab from '@shell/components/Tabbed/Tab';
4
3
  import Tabbed from '@shell/components/Tabbed';
5
4
  import { MANAGEMENT } from '@shell/config/types';
@@ -8,7 +7,6 @@ import Loading from '@shell/components/Loading';
8
7
  import { SUBTYPE_MAPPING, CREATE_VERBS } from '@shell/models/management.cattle.io.roletemplate';
9
8
  import { NAME } from '@shell/config/product/auth';
10
9
  import { BLANK_CLUSTER } from '@shell/store/store-types.js';
11
- import { Banner } from '@components/Banner';
12
10
 
13
11
  const GLOBAL = SUBTYPE_MAPPING.GLOBAL.key;
14
12
  const CLUSTER = SUBTYPE_MAPPING.CLUSTER.key;
@@ -33,7 +31,7 @@ export default {
33
31
  name: 'Roles',
34
32
 
35
33
  components: {
36
- Tab, Tabbed, ResourceTable, Loading, Banner
34
+ Tab, Tabbed, ResourceTable, Loading
37
35
  },
38
36
 
39
37
  async fetch() {
@@ -100,8 +98,6 @@ export default {
100
98
  },
101
99
 
102
100
  computed: {
103
- ...mapGetters(['releaseNotesUrl']),
104
-
105
101
  globalResources() {
106
102
  return this.globalRoles;
107
103
  },
@@ -183,12 +179,6 @@ export default {
183
179
  :weight="tabs[GLOBAL].weight"
184
180
  :label-key="tabs[GLOBAL].labelKey"
185
181
  >
186
- <Banner
187
- color="warning"
188
- class="mb-20"
189
- >
190
- <span v-clean-html="t('rbac.globalRoles.role.restricted-admin.deprecation', { releaseNotesUrl }, true)" />
191
- </Banner>
192
182
  <ResourceTable
193
183
  :schema="tabs[GLOBAL].schema"
194
184
  :rows="globalResources"
@@ -3,6 +3,7 @@ import Dashboard from '@shell/pages/c/_cluster/explorer/index.vue';
3
3
  import { shallowMount } from '@vue/test-utils';
4
4
  import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
5
5
  import { NODE_ARCHITECTURE } from '@shell/config/labels-annotations';
6
+ import { WORKLOAD_TYPES } from '@shell/config/types';
6
7
 
7
8
  describe('page: cluster dashboard', () => {
8
9
  const mountOptions = {
@@ -97,12 +98,81 @@ describe('page: cluster dashboard', () => {
97
98
  [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 1],
98
99
  [STATES_ENUM.HEALTHY, 'icon-checkmark', true, false, false, [{ status: 'True' }], 1, 0],
99
100
  ]]
100
- ])('%p cluster - %p agent health box', (_, agentId, isLocal, agentResources, statuses) => {
101
+ ])('%p cluster - %p agent health box :', (_, agentId, isLocal, agentResources, statuses) => {
102
+ it.each(statuses)('should NOT show %p status due to missing canList permissions', (status, iconClass, isLoaded, disconnected, error, conditions, readyReplicas, unavailableReplicas) => {
103
+ const options = clone(mountOptions);
104
+
105
+ options.global.mocks.$store.getters.currentCluster.isLocal = isLocal;
106
+
107
+ const resources = agentResources.reduce((acc, r) => {
108
+ const agent = {
109
+ metadata: { state: { error } },
110
+ spec: { replicas: 1 },
111
+ status: {
112
+ readyReplicas,
113
+ unavailableReplicas,
114
+ conditions
115
+ }
116
+ };
117
+
118
+ return isLoaded ? {
119
+ ...acc,
120
+ [r]: agent
121
+ } : 'loading';
122
+ }, {});
123
+
124
+ const wrapper = shallowMount(Dashboard, {
125
+ ...options,
126
+ data: () => ({
127
+ ...resources,
128
+ disconnected,
129
+ canViewAgents: true
130
+ })
131
+ });
132
+
133
+ const box = wrapper.find(`[data-testid="k8s-service-${ agentId }"]`);
134
+
135
+ expect(box.exists()).toBe(false);
136
+ });
137
+ });
138
+
139
+ describe.each([
140
+ ['local', 'fleet', true, ['fleetDeployment', 'fleetStatefulSet'], [
141
+ [STATES_ENUM.IN_PROGRESS, 'icon-spinner', false, false, false, '', 0, 0],
142
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, false, [{ status: 'False' }], 0, 0],
143
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, true, [{ status: 'True' }], 0, 0],
144
+ [STATES_ENUM.WARNING, 'icon-warning', true, true, false, [{ status: 'True' }], 0, 0],
145
+ [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 0],
146
+ [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 1],
147
+ [STATES_ENUM.HEALTHY, 'icon-checkmark', true, false, false, [{ status: 'True' }], 1, 0],
148
+ ]],
149
+ ['downstream RKE2', 'fleet', false, ['fleetStatefulSet'], [
150
+ [STATES_ENUM.IN_PROGRESS, 'icon-spinner', false, false, false, '', 0, 0],
151
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, false, [{ status: 'False' }], 0, 0],
152
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, true, [{ status: 'True' }], 0, 0],
153
+ [STATES_ENUM.WARNING, 'icon-warning', true, true, false, [{ status: 'True' }], 0, 0],
154
+ [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 0],
155
+ [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 1],
156
+ [STATES_ENUM.HEALTHY, 'icon-checkmark', true, false, false, [{ status: 'True' }], 1, 0],
157
+ ]],
158
+ ['downstream RKE2', 'cattle', false, ['cattleDeployment'], [
159
+ [STATES_ENUM.IN_PROGRESS, 'icon-spinner', false, false, false, '', 0, 0],
160
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, false, [{ status: 'False' }], 0, 0],
161
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, true, false, [{ status: 'True' }], 0, 0],
162
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, true, [{ status: 'True' }], 0, 0],
163
+ [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 0],
164
+ [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 1],
165
+ [STATES_ENUM.HEALTHY, 'icon-checkmark', true, false, false, [{ status: 'True' }], 1, 0],
166
+ ]]
167
+ ])('%p cluster - %p agent health box ::', (_, agentId, isLocal, agentResources, statuses) => {
101
168
  it.each(statuses)('should show %p status', (status, iconClass, isLoaded, disconnected, error, conditions, readyReplicas, unavailableReplicas) => {
102
169
  const options = clone(mountOptions);
103
170
 
104
171
  options.global.mocks.$store.getters.currentCluster.isLocal = isLocal;
105
172
 
173
+ // let's pass the canList now
174
+ options.global.mocks.$store.getters['cluster/canList'] = (type: string) => !!(type === WORKLOAD_TYPES.DEPLOYMENT) || !!(type === WORKLOAD_TYPES.STATEFUL_SET);
175
+
106
176
  const resources = agentResources.reduce((acc, r) => {
107
177
  const agent = {
108
178
  metadata: { state: { error } },
@@ -188,7 +188,11 @@ export default {
188
188
  },
189
189
 
190
190
  fleetAgentNamespace() {
191
- return this.$store.getters['cluster/byId'](NAMESPACE, 'cattle-fleet-system');
191
+ if (this.currentCluster.isLocal) {
192
+ return this.$store.getters['cluster/canList'](WORKLOAD_TYPES.DEPLOYMENT) && this.$store.getters['cluster/canList'](WORKLOAD_TYPES.STATEFUL_SET) && this.$store.getters['cluster/byId'](NAMESPACE, 'cattle-fleet-system');
193
+ }
194
+
195
+ return this.$store.getters['cluster/canList'](WORKLOAD_TYPES.STATEFUL_SET) && this.$store.getters['cluster/byId'](NAMESPACE, 'cattle-fleet-system');
192
196
  },
193
197
 
194
198
  cattleAgentNamespace() {
@@ -196,7 +200,7 @@ export default {
196
200
  return;
197
201
  }
198
202
 
199
- return this.$store.getters['cluster/byId'](NAMESPACE, 'cattle-system');
203
+ return this.$store.getters['cluster/canList'](WORKLOAD_TYPES.DEPLOYMENT) && this.$store.getters['cluster/byId'](NAMESPACE, 'cattle-system');
200
204
  },
201
205
 
202
206
  canViewAgents() {
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { defineComponent, inject, PropType } from 'vue';
3
- import debounce from 'lodash/debounce';
3
+ import { debounce } from 'lodash';
4
4
  import { _EDIT, _VIEW } from '@shell/config/query-params';
5
5
 
6
6
  interface NonReactiveProps {
@@ -4035,6 +4035,26 @@ export function dropKeys(obj: any, keys: any): void;
4035
4035
  * @returns
4036
4036
  */
4037
4037
  export function deepToRaw(obj: any, cache?: any): any;
4038
+ /**
4039
+ * Helper function to alter Lodash merge function default behaviour on merging arrays while updating machine pool configuration.
4040
+ *
4041
+ * In rke2.vue, the syncMachineConfigWithLatest function updates machine pool configuration by
4042
+ * merging the latest configuration received from the backend with the current configuration updated by the user.
4043
+ * However, Lodash's merge function treats arrays like object so index values are merged and not appended to arrays
4044
+ * resulting in undesired outcomes for us, Example:
4045
+ *
4046
+ * const lastSavedConfigFromBE = { a: ["test"] };
4047
+ * const currentConfigByUser = { a: [] };
4048
+ * merge(lastSavedConfigFromBE, currentConfigByUser); // returns { a: ["test"] }; but we expect { a: [] };
4049
+ *
4050
+ * More info: https://github.com/lodash/lodash/issues/1313
4051
+ *
4052
+ * This helper function addresses the issue by always replacing the old array with the new array during the merge process.
4053
+ *
4054
+ * This helper is used for another case in rke2.vue to handle merging addon chart default values with the user's current values.
4055
+ * It fixed https://github.com/rancher/dashboard/issues/12418
4056
+ */
4057
+ export function mergeWithReplaceArrays(obj1?: {}, obj2?: {}): any;
4038
4058
  export { isEqualBasic as isEqual };
4039
4059
  export function toDictionary(array: any, callback: any): any;
4040
4060
  /**
@@ -1,6 +1,6 @@
1
1
  import { reactive, isReactive } from 'vue';
2
2
  import {
3
- clone, get, getter, isEmpty, toDictionary, remove, diff, definedKeys, deepToRaw
3
+ clone, get, getter, isEmpty, toDictionary, remove, diff, definedKeys, deepToRaw, mergeWithReplaceArrays
4
4
  } from '@shell/utils/object';
5
5
 
6
6
  describe('fx: get', () => {
@@ -378,3 +378,32 @@ describe('fx: deepToRaw', () => {
378
378
  });
379
379
  });
380
380
  });
381
+
382
+ describe('fx: mergeWithReplaceArrays', () => {
383
+ const testCases: Array<[object?, object?, object?]> = [
384
+ // Some array test cases, an array from the first object should be replaced with the array from the second object
385
+ [{ a: ['one'] }, { a: [] }, { a: [] }],
386
+ [{ a: ['one', 'two'] }, { a: ['one', 'two', 'three'] }, { a: ['one', 'two', 'three'] }],
387
+ [{ a: ['one', 'two'], b: ['three', 'four'] }, { a: ['one'], b: [] }, { a: ['one'], b: [] }],
388
+ [{
389
+ a: ['one', 'two'], b: ['three', 'four'], c: 'five'
390
+ }, { a: ['one'], b: [] }, {
391
+ a: ['one'], b: [], c: 'five'
392
+ }],
393
+ // Some other test cases
394
+ [{ a: 'one' }, { b: 'two' }, { a: 'one', b: 'two' }],
395
+ [{ a: 'one' }, { a: '', b: 'two' }, { a: '', b: 'two' }],
396
+ [{ a: 'one', b: 'two' }, { a: 1, c: { d: null } }, {
397
+ a: 1, b: 'two', c: { d: null }
398
+ }],
399
+ [undefined, undefined, {}],
400
+ [{}, undefined, {}],
401
+ [undefined, {}, {}],
402
+ ];
403
+
404
+ it.each(testCases)('should merge properly', (obj1, obj2, expected) => {
405
+ const result = mergeWithReplaceArrays(obj1, obj2);
406
+
407
+ expect(result).toStrictEqual(expected);
408
+ });
409
+ });
package/utils/object.js CHANGED
@@ -8,6 +8,7 @@ import isObject from 'lodash/isObject';
8
8
  import isArray from 'lodash/isArray';
9
9
  import isEqual from 'lodash/isEqual';
10
10
  import difference from 'lodash/difference';
11
+ import mergeWith from 'lodash/mergeWith';
11
12
  import { splitObjectPath, joinObjectPath } from '@shell/utils/string';
12
13
  import { addObject } from '@shell/utils/array';
13
14
 
@@ -471,3 +472,30 @@ export function deepToRaw(obj, cache = new WeakSet()) {
471
472
  return result;
472
473
  }
473
474
  }
475
+
476
+ /**
477
+ * Helper function to alter Lodash merge function default behaviour on merging arrays while updating machine pool configuration.
478
+ *
479
+ * In rke2.vue, the syncMachineConfigWithLatest function updates machine pool configuration by
480
+ * merging the latest configuration received from the backend with the current configuration updated by the user.
481
+ * However, Lodash's merge function treats arrays like object so index values are merged and not appended to arrays
482
+ * resulting in undesired outcomes for us, Example:
483
+ *
484
+ * const lastSavedConfigFromBE = { a: ["test"] };
485
+ * const currentConfigByUser = { a: [] };
486
+ * merge(lastSavedConfigFromBE, currentConfigByUser); // returns { a: ["test"] }; but we expect { a: [] };
487
+ *
488
+ * More info: https://github.com/lodash/lodash/issues/1313
489
+ *
490
+ * This helper function addresses the issue by always replacing the old array with the new array during the merge process.
491
+ *
492
+ * This helper is used for another case in rke2.vue to handle merging addon chart default values with the user's current values.
493
+ * It fixed https://github.com/rancher/dashboard/issues/12418
494
+ */
495
+ export function mergeWithReplaceArrays(obj1 = {}, obj2 = {}) {
496
+ return mergeWith(obj1, obj2, (obj1Value, obj2Value) => {
497
+ if (Array.isArray(obj1Value) && Array.isArray(obj2Value)) {
498
+ return obj2Value;
499
+ }
500
+ });
501
+ }
package/vue.config.js CHANGED
@@ -462,7 +462,7 @@ const getWatcherIgnored = (excludes) => {
462
462
  /dist-pkg/,
463
463
  /scripts\/standalone/,
464
464
  ];
465
- const pathExcludedPkg = excludes.map((excluded) => new RegExp(`/pkg.${ excluded }`));
465
+ const pathExcludedPkg = excludes.map((excluded) => new RegExp(`/pkg.${ excluded }/`));
466
466
  const pathsCombined = [...paths, ...pathExcludedPkg];
467
467
  const regexCombined = new RegExp(pathsCombined.map(({ source }) => source).join('|'));
468
468
 
@@ -516,6 +516,7 @@ module.exports = function(dir, _appConfig) {
516
516
  @import "~shell/assets/styles/base/_variables.scss";
517
517
  @import "~shell/assets/styles/base/_functions.scss";
518
518
  @import "~shell/assets/styles/base/_mixins.scss";
519
+ @import 'node_modules/xterm/css/xterm.css';
519
520
  `
520
521
  }
521
522
  }
@@ -1,30 +0,0 @@
1
- import { vspherePoolConfigMerge } from '@shell/machine-config/vmwarevsphere-pool-config-merge';
2
-
3
- describe('vspherePoolConfigMerge', () => {
4
- const testCases: Array<[object?, object?, object?]> = [
5
- // Some array test cases, an array from the first object should be replaced with the array from the second object
6
- [{ a: ['one'] }, { a: [] }, { a: [] }],
7
- [{ a: ['one', 'two'] }, { a: ['one', 'two', 'three'] }, { a: ['one', 'two', 'three'] }],
8
- [{ a: ['one', 'two'], b: ['three', 'four'] }, { a: ['one'], b: [] }, { a: ['one'], b: [] }],
9
- [{
10
- a: ['one', 'two'], b: ['three', 'four'], c: 'five'
11
- }, { a: ['one'], b: [] }, {
12
- a: ['one'], b: [], c: 'five'
13
- }],
14
- // Some other test cases
15
- [{ a: 'one' }, { b: 'two' }, { a: 'one', b: 'two' }],
16
- [{ a: 'one' }, { a: '', b: 'two' }, { a: '', b: 'two' }],
17
- [{ a: 'one', b: 'two' }, { a: 1, c: { d: null } }, {
18
- a: 1, b: 'two', c: { d: null }
19
- }],
20
- [undefined, undefined, {}],
21
- [{}, undefined, {}],
22
- [undefined, {}, {}],
23
- ];
24
-
25
- it.each(testCases)('should merge properly', (obj1, obj2, expected) => {
26
- const result = vspherePoolConfigMerge(obj1, obj2);
27
-
28
- expect(result).toStrictEqual(expected);
29
- });
30
- });
@@ -1,25 +0,0 @@
1
- import mergeWith from 'lodash/mergeWith';
2
-
3
- /**
4
- * Helper function to alter Lodash merge function default behaviour on merging arrays while updating machine pool configuration.
5
- *
6
- * In rke2.vue, the syncMachineConfigWithLatest function updates machine pool configuration by
7
- * merging the latest configuration received from the backend with the current configuration updated by the user.
8
- * However, Lodash's merge function treats arrays like object so index values are merged and not appended to arrays
9
- * resulting in undesired outcomes for us, Example:
10
- *
11
- * const lastSavedConfigFromBE = { a: ["test"] };
12
- * const currentConfigByUser = { a: [] };
13
- * merge(lastSavedConfigFromBE, currentConfigByUser); // returns { a: ["test"] }; but we expect { a: [] };
14
- *
15
- * More info: https://github.com/lodash/lodash/issues/1313
16
- *
17
- * This helper function addresses the issue by always replacing the old array with the new array during the merge process.
18
- */
19
- export function vspherePoolConfigMerge(obj1 = {}, obj2 = {}): Object {
20
- return mergeWith(obj1, obj2, (obj1Value, obj2Value) => {
21
- if (Array.isArray(obj1Value) && Array.isArray(obj2Value)) {
22
- return obj2Value;
23
- }
24
- });
25
- }