@rancher/shell 3.0.3 → 3.0.5-rc.1

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 (139) hide show
  1. package/assets/styles/base/_basic.scss +6 -0
  2. package/assets/styles/global/_button.scss +1 -0
  3. package/assets/translations/en-us.yaml +38 -3
  4. package/cloud-credential/aws.vue +2 -0
  5. package/components/AssignTo.vue +25 -11
  6. package/components/AsyncButton.vue +24 -7
  7. package/components/BannerGraphic.vue +1 -0
  8. package/components/CommunityLinks.vue +3 -3
  9. package/components/CopyToClipboardText.vue +2 -1
  10. package/components/DetailText.vue +5 -0
  11. package/components/DisableAuthProviderModal.vue +1 -0
  12. package/components/ExplorerMembers.vue +1 -1
  13. package/components/ExplorerProjectsNamespaces.vue +56 -14
  14. package/components/LandingPagePreference.vue +5 -3
  15. package/components/LocaleSelector.vue +38 -94
  16. package/components/ModalWithCard.vue +1 -0
  17. package/components/MoveModal.vue +1 -0
  18. package/components/PromptRemove.vue +2 -1
  19. package/components/PromptRestore.vue +1 -0
  20. package/components/ResourceCancelModal.vue +1 -0
  21. package/components/SortableTable/index.vue +35 -10
  22. package/components/StatusBadge.vue +10 -4
  23. package/components/__tests__/AsyncButton.test.ts +2 -2
  24. package/components/auth/Principal.vue +9 -3
  25. package/components/auth/__tests__/RoleDetailEdit.test.ts +3 -2
  26. package/components/form/ArrayList.vue +75 -54
  27. package/components/form/Command.vue +6 -15
  28. package/components/form/EnvVars.vue +15 -8
  29. package/components/form/HealthCheck.vue +3 -3
  30. package/components/form/HookOption.vue +11 -16
  31. package/components/form/KeyValue.vue +1 -1
  32. package/components/form/LabeledSelect.vue +2 -1
  33. package/components/form/LifecycleHooks.vue +3 -3
  34. package/components/form/MatchExpressions.vue +10 -7
  35. package/components/form/NameNsDescription.vue +123 -103
  36. package/components/form/Networking.vue +20 -12
  37. package/components/form/NodeAffinity.vue +31 -23
  38. package/components/form/NodeScheduling.vue +13 -3
  39. package/components/form/PodAffinity.vue +43 -43
  40. package/components/form/Probe.vue +67 -66
  41. package/components/form/ResourceQuota/Project.vue +5 -1
  42. package/components/form/ResourceSelector.vue +7 -9
  43. package/components/form/SSHKnownHosts/KnownHostsEditDialog.vue +6 -3
  44. package/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts +12 -1
  45. package/components/form/SSHKnownHosts/index.vue +16 -2
  46. package/components/form/Security.vue +54 -56
  47. package/components/form/Select.vue +31 -6
  48. package/components/form/ShellInput.vue +5 -1
  49. package/components/form/Tolerations.vue +5 -1
  50. package/components/form/ValueFromResource.vue +134 -121
  51. package/components/form/WorkloadPorts.vue +18 -18
  52. package/components/form/__tests__/ArrayList.test.ts +3 -0
  53. package/components/form/__tests__/MatchExpressions.test.ts +12 -12
  54. package/components/form/__tests__/NameNsDescription.test.ts +115 -14
  55. package/components/form/__tests__/Probe.test.ts +12 -8
  56. package/components/form/__tests__/SSHKnownHosts.test.ts +11 -0
  57. package/components/form/__tests__/Select.test.ts +37 -0
  58. package/components/formatter/InternalExternalIP.vue +2 -0
  59. package/components/formatter/SecretData.vue +20 -7
  60. package/components/nav/Group.vue +15 -1
  61. package/components/nav/Header.vue +1 -0
  62. package/components/nav/Type.vue +12 -1
  63. package/components/templates/blank.vue +4 -1
  64. package/components/templates/default.vue +2 -0
  65. package/components/templates/home.vue +4 -1
  66. package/components/templates/plain.vue +4 -1
  67. package/composables/useRuntimeFlag.ts +29 -0
  68. package/config/router/routes.js +20 -13
  69. package/core/types.ts +5 -0
  70. package/dialog/AddCustomBadgeDialog.vue +1 -0
  71. package/dialog/DeactivateDriverDialog.vue +5 -4
  72. package/dialog/ForceMachineRemoveDialog.vue +4 -1
  73. package/dialog/GitRepoForceUpdateDialog.vue +1 -1
  74. package/edit/__tests__/monitoring.coreos.com.prometheusrule.test.ts +16 -3
  75. package/edit/auth/__tests__/oidc.test.ts +152 -109
  76. package/edit/auth/azuread.vue +1 -0
  77. package/edit/auth/googleoauth.vue +4 -0
  78. package/edit/auth/oidc.vue +37 -4
  79. package/edit/cloudcredential.vue +1 -0
  80. package/edit/fleet.cattle.io.gitrepo.vue +1 -0
  81. package/edit/logging.banzaicloud.io.output/__tests__/logging.banzaicloud.io.output.test.ts +40 -9
  82. package/edit/networking.k8s.io.ingress/IngressClass.vue +7 -3
  83. package/edit/networking.k8s.io.ingress/__tests__/IngressClass.test.ts +58 -0
  84. package/edit/persistentvolume/__tests__/persistentvolume.test.ts +14 -2
  85. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +1 -0
  86. package/edit/provisioning.cattle.io.cluster/rke2.vue +25 -34
  87. package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +6 -1
  88. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +29 -1
  89. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +2 -2
  90. package/edit/token.vue +2 -0
  91. package/edit/workload/index.vue +1 -0
  92. package/edit/workload/mixins/workload.js +0 -2
  93. package/list/management.cattle.io.feature.vue +1 -0
  94. package/list/provisioning.cattle.io.cluster.vue +20 -12
  95. package/machine-config/__tests__/vmwarevsphere.test.ts +48 -3
  96. package/machine-config/vmwarevsphere.vue +16 -0
  97. package/models/__tests__/namespace.test.ts +25 -1
  98. package/models/cloudcredential.js +5 -0
  99. package/models/kontainerdriver.js +6 -3
  100. package/models/management.cattle.io.node.js +3 -3
  101. package/models/namespace.js +4 -5
  102. package/models/nodedriver.js +6 -3
  103. package/models/workload.js +4 -1
  104. package/package.json +4 -4
  105. package/pages/about.vue +16 -8
  106. package/pages/account/index.vue +4 -1
  107. package/pages/auth/login.vue +11 -3
  108. package/pages/auth/logout.vue +4 -1
  109. package/pages/auth/setup.vue +1 -0
  110. package/pages/auth/verify.vue +4 -1
  111. package/pages/c/_cluster/apps/charts/chart.vue +1 -1
  112. package/pages/diagnostic.vue +47 -2
  113. package/pages/fail-whale.vue +6 -3
  114. package/pages/home.vue +24 -18
  115. package/pages/support/index.vue +4 -1
  116. package/promptRemove/management.cattle.io.fleetworkspace.vue +1 -1
  117. package/promptRemove/management.cattle.io.globalrole.vue +1 -1
  118. package/promptRemove/management.cattle.io.project.vue +2 -2
  119. package/promptRemove/management.cattle.io.roletemplate.vue +1 -1
  120. package/promptRemove/pod.vue +1 -1
  121. package/rancher-components/Form/Radio/RadioGroup.vue +25 -23
  122. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +3 -3
  123. package/rancher-components/RcDropdown/RcDropdown.vue +6 -5
  124. package/rancher-components/RcDropdown/RcDropdownMenu.vue +4 -2
  125. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +12 -2
  126. package/rancher-components/RcDropdown/useDropdownCollection.ts +8 -0
  127. package/rancher-components/RcDropdown/useDropdownContext.ts +9 -3
  128. package/scripts/extension/publish +1 -0
  129. package/server/har-file.js +25 -3
  130. package/store/features.js +2 -1
  131. package/store/type-map.js +4 -0
  132. package/types/shell/index.d.ts +9 -2
  133. package/utils/__tests__/string.test.ts +2 -2
  134. package/utils/cluster.js +35 -0
  135. package/utils/string.js +1 -3
  136. package/utils/validators/machine-pool.ts +20 -0
  137. package/components/formatter/ExtensionCache.vue +0 -74
  138. package/components/formatter/Port.vue +0 -24
  139. package/components/formatter/SecretType.vue +0 -41
@@ -0,0 +1,58 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import IngressClass from '@shell/edit/networking.k8s.io.ingress/IngressClass.vue';
3
+ import LabeledSelect from '@shell/components/form/LabeledSelect';
4
+ import { _EDIT } from '@shell/config/query-params';
5
+
6
+ jest.mock('@shell/components/form/LabeledSelect', () => ({
7
+ name: 'LabeledSelect',
8
+ template: '<div></div>',
9
+ props: ['value', 'taggable', 'searchable', 'mode', 'label', 'options', 'optionLabel'],
10
+ emits: ['update:value'],
11
+ }));
12
+
13
+ describe('ingressClass.vue', () => {
14
+ it('renders correctly', () => {
15
+ const wrapper = shallowMount(IngressClass, {
16
+ props: {
17
+ value: {},
18
+ ingressClasses: [],
19
+ mode: _EDIT,
20
+ },
21
+ });
22
+
23
+ expect(wrapper.exists()).toBe(true);
24
+ expect(wrapper.findComponent(LabeledSelect).exists()).toBe(true);
25
+ });
26
+
27
+ it('updates ingressClassName correctly', async() => {
28
+ const wrapper = shallowMount(IngressClass, {
29
+ props: {
30
+ value: {},
31
+ ingressClasses: [{ label: 'nginx', value: 'nginx' }],
32
+ mode: _EDIT,
33
+ },
34
+ });
35
+
36
+ await wrapper.findComponent(LabeledSelect).vm.$emit('update:value', 'nginx');
37
+
38
+ expect(wrapper.vm.ingressClassName).toBe('nginx');
39
+ expect(wrapper.props('value').spec.ingressClassName).toBe('nginx');
40
+ expect(wrapper.emitted()['update:value']).toBeTruthy();
41
+ });
42
+
43
+ it('removes ingressClassName when none is selected', async() => {
44
+ const wrapper = shallowMount(IngressClass, {
45
+ props: {
46
+ value: { spec: { ingressClassName: 'nginx' } },
47
+ ingressClasses: [{ label: 'nginx', value: 'nginx' }],
48
+ mode: _EDIT,
49
+ },
50
+ });
51
+
52
+ await wrapper.findComponent(LabeledSelect).vm.$emit('update:value', '');
53
+
54
+ expect(wrapper.vm.ingressClassName).toBe('');
55
+ expect(wrapper.props('value').spec.ingressClassName).toBeUndefined();
56
+ expect(wrapper.emitted()['update:value']).toBeTruthy();
57
+ });
58
+ });
@@ -9,7 +9,13 @@ describe('view: PersistentVolume', () => {
9
9
  };
10
10
  const resource = 'PersistentVolume';
11
11
  const wrapper = mount(PersistentVolume as ExtendedVue<Vue, {}, {}, {}, PersistentVolume>, {
12
- props: { value: { spec: { } } },
12
+ props: {
13
+ value: {
14
+ setAnnotation: jest.fn(),
15
+ spec: {},
16
+ metadata: {},
17
+ }
18
+ },
13
19
 
14
20
  global: {
15
21
  mocks: {
@@ -54,7 +60,13 @@ describe('view: PersistentVolume', () => {
54
60
  const plugin = 'csi';
55
61
  const resource = 'PersistentVolume';
56
62
  const wrapper = mount(PersistentVolume as ExtendedVue<Vue, {}, {}, {}, PersistentVolume>, {
57
- props: { value: { spec: { [plugin]: { value: plugin } } } },
63
+ props: {
64
+ value: {
65
+ setAnnotation: jest.fn(),
66
+ spec: { [plugin]: { value: plugin } },
67
+ metadata: {},
68
+ }
69
+ },
58
70
  global: {
59
71
  mocks: {
60
72
  $store: {
@@ -236,6 +236,7 @@ export default {
236
236
  <Loading v-if="$fetchState.pending" />
237
237
  <CruResource
238
238
  v-else
239
+ :done-params="$attrs['done-params'] /* Without this, changes to the validationPassed prop end up propagating all the way to the root of the app and force a re-render when the input becomes valid. I haven't found a reasonable explanation for why this happens. */"
239
240
  :mode="mode"
240
241
  :validation-passed="validationPassed"
241
242
  :resource="newCredential"
@@ -25,7 +25,9 @@ import {
25
25
  clone, diff, set, get, isEmpty, mergeWithReplaceArrays
26
26
  } from '@shell/utils/object';
27
27
  import { allHash } from '@shell/utils/promise';
28
- import { getAllOptionsAfterCurrentVersion, filterOutDeprecatedPatchVersions, isHarvesterSatisfiesVersion, labelForAddon } from '@shell/utils/cluster';
28
+ import {
29
+ getAllOptionsAfterCurrentVersion, filterOutDeprecatedPatchVersions, isHarvesterSatisfiesVersion, labelForAddon, initSchedulingCustomization
30
+ } from '@shell/utils/cluster';
29
31
 
30
32
  import { BadgeState } from '@components/BadgeState';
31
33
  import { Banner } from '@components/Banner';
@@ -63,7 +65,6 @@ import ClusterAppearance from '@shell/components/form/ClusterAppearance';
63
65
  import AddOnAdditionalManifest from '@shell/edit/provisioning.cattle.io.cluster/tabs/AddOnAdditionalManifest';
64
66
  import VsphereUtils, { VMWARE_VSPHERE } from '@shell/utils/v-sphere';
65
67
  import { mapGetters } from 'vuex';
66
- import { SCHEDULING_CUSTOMIZATION } from '@shell/store/features';
67
68
  const HARVESTER = 'harvester';
68
69
  const HARVESTER_CLOUD_PROVIDER = 'harvester-cloud-provider';
69
70
  const NETBIOS_TRUNCATION_LENGTH = 15;
@@ -149,7 +150,13 @@ export default {
149
150
  await this.initSpecs();
150
151
  await this.initAddons();
151
152
  await this.initRegistry();
152
- await this.initSchedulingCustomization();
153
+ const sc = await initSchedulingCustomization(this.value.spec, this.features, this.$store, this.mode);
154
+
155
+ this.clusterAgentDefaultPC = sc.clusterAgentDefaultPC;
156
+ this.clusterAgentDefaultPDB = sc.clusterAgentDefaultPDB;
157
+ this.schedulingCustomizationFeatureEnabled = sc.schedulingCustomizationFeatureEnabled;
158
+ this.schedulingCustomizationOriginallyEnabled = sc.schedulingCustomizationOriginallyEnabled;
159
+ this.errors = this.errors.concat(sc.errors);
153
160
 
154
161
  Object.entries(this.chartValues).forEach(([name, value]) => {
155
162
  const key = this.chartVersionKey(name);
@@ -243,20 +250,21 @@ export default {
243
250
  fvFormRuleSets: [{
244
251
  path: 'metadata.name', rules: ['subDomain'], translationKey: 'nameNsDescription.name.label'
245
252
  }],
246
- harvesterVersionRange: {},
247
- cisOverride: false,
253
+ harvesterVersionRange: {},
254
+ cisOverride: false,
248
255
  truncateLimit,
249
- busy: false,
250
- machinePoolValidation: {}, // map of validation states for each machine pool
251
- machinePoolErrors: {},
252
- addonConfigValidation: {}, // validation state of each addon config (boolean of whether codemirror's yaml lint passed)
253
- allNamespaces: [],
254
- extensionTabs: getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.CLUSTER_CREATE_RKE2, this.$route, this),
255
- clusterAgentDeploymentCustomization: null,
256
- schedulingCustomizationFeatureEnabled: false,
257
- clusterAgentDefaultPC: null,
258
- clusterAgentDefaultPDB: null,
259
- activeTab: null,
256
+ busy: false,
257
+ machinePoolValidation: {}, // map of validation states for each machine pool
258
+ machinePoolErrors: {},
259
+ addonConfigValidation: {}, // validation state of each addon config (boolean of whether codemirror's yaml lint passed)
260
+ allNamespaces: [],
261
+ extensionTabs: getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.CLUSTER_CREATE_RKE2, this.$route, this),
262
+ clusterAgentDeploymentCustomization: null,
263
+ schedulingCustomizationFeatureEnabled: false,
264
+ schedulingCustomizationOriginallyEnabled: false,
265
+ clusterAgentDefaultPC: null,
266
+ clusterAgentDefaultPDB: null,
267
+ activeTab: null,
260
268
  REGISTRIES_TAB_NAME,
261
269
  labelForAddon
262
270
 
@@ -1062,24 +1070,6 @@ export default {
1062
1070
  }
1063
1071
  },
1064
1072
 
1065
- async initSchedulingCustomization() {
1066
- this.schedulingCustomizationFeatureEnabled = this.features(SCHEDULING_CUSTOMIZATION);
1067
- try {
1068
- this.clusterAgentDefaultPC = JSON.parse((await this.$store.dispatch('management/find', { type: MANAGEMENT.SETTING, id: SETTING.CLUSTER_AGENT_DEFAULT_PRIORITY_CLASS })).value) || null;
1069
- } catch (e) {
1070
- this.errors.push(e);
1071
- }
1072
- try {
1073
- this.clusterAgentDefaultPDB = JSON.parse((await this.$store.dispatch('management/find', { type: MANAGEMENT.SETTING, id: SETTING.CLUSTER_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET })).value) || null;
1074
- } catch (e) {
1075
- this.errors.push(e);
1076
- }
1077
-
1078
- if (this.schedulingCustomizationFeatureEnabled && this.mode === _CREATE && isEmpty(this.value?.spec?.clusterAgentDeploymentCustomization?.schedulingCustomization)) {
1079
- set(this.value, 'spec.clusterAgentDeploymentCustomization.schedulingCustomization', { priorityClass: this.clusterAgentDefaultPC, podDisruptionBudget: this.clusterAgentDefaultPDB });
1080
- }
1081
- },
1082
-
1083
1073
  setSchedulingCustomization(val) {
1084
1074
  if (val) {
1085
1075
  set(this.value, 'spec.clusterAgentDeploymentCustomization.schedulingCustomization', { priorityClass: this.clusterAgentDefaultPC, podDisruptionBudget: this.clusterAgentDefaultPDB });
@@ -2450,6 +2440,7 @@ export default {
2450
2440
  :scheduling-customization-feature-enabled="schedulingCustomizationFeatureEnabled"
2451
2441
  :default-p-c="clusterAgentDefaultPC"
2452
2442
  :default-p-d-b="clusterAgentDefaultPDB"
2443
+ :scheduling-customization-originally-enabled="schedulingCustomizationOriginallyEnabled"
2453
2444
  @scheduling-customization-changed="setSchedulingCustomization"
2454
2445
  />
2455
2446
  </Tab>
@@ -48,6 +48,11 @@ export default {
48
48
  type: Boolean,
49
49
  required: false
50
50
  },
51
+ schedulingCustomizationOriginallyEnabled: {
52
+ type: Boolean,
53
+ default: false
54
+ },
55
+
51
56
  defaultPC: {
52
57
  type: Object,
53
58
  default: () => {},
@@ -149,7 +154,7 @@ export default {
149
154
  },
150
155
 
151
156
  schedulingCustomizationVisible() {
152
- return this.schedulingCustomizationFeatureEnabled || (this.isEdit && this.value.schedulingCustomization );
157
+ return this.schedulingCustomizationFeatureEnabled || this.schedulingCustomizationOriginallyEnabled;
153
158
  },
154
159
 
155
160
  affinityOptions() {
@@ -8,6 +8,8 @@ import AdvancedSection from '@shell/components/AdvancedSection.vue';
8
8
  import { Banner } from '@components/Banner';
9
9
  import UnitInput from '@shell/components/form/UnitInput.vue';
10
10
  import { randomStr } from '@shell/utils/string';
11
+ import FormValidation from '@shell/mixins/form-validation';
12
+ import { MACHINE_POOL_VALIDATION } from '@shell/utils/validators/machine-pool';
11
13
 
12
14
  export default {
13
15
 
@@ -25,6 +27,8 @@ export default {
25
27
  UnitInput
26
28
  },
27
29
 
30
+ mixins: [FormValidation],
31
+
28
32
  props: {
29
33
  value: {
30
34
  type: Object,
@@ -122,6 +126,12 @@ export default {
122
126
  uuid: randomStr(),
123
127
 
124
128
  unhealthyNodeTimeoutInteger: this.value.pool.unhealthyNodeTimeout ? parseDuration(this.value.pool.unhealthyNodeTimeout) : 0,
129
+
130
+ validationErrors: [],
131
+
132
+ MACHINE_POOL_VALIDATION,
133
+
134
+ fvFormRuleSets: MACHINE_POOL_VALIDATION.RULESETS,
125
135
  };
126
136
  },
127
137
 
@@ -136,7 +146,7 @@ export default {
136
146
 
137
147
  isWindows() {
138
148
  return this.value?.config?.os === 'windows';
139
- },
149
+ }
140
150
  },
141
151
 
142
152
  watch: {
@@ -148,6 +158,20 @@ export default {
148
158
  } else {
149
159
  this.value.pool.machineOS = 'linux';
150
160
  }
161
+ },
162
+
163
+ validationErrors: {
164
+ handler(newValue) {
165
+ this.$emit('validationChanged', newValue.length === 0);
166
+ },
167
+ deep: true
168
+ },
169
+
170
+ fvFormIsValid: {
171
+ handler(newValue) {
172
+ this.$emit('validationChanged', newValue);
173
+ },
174
+ deep: true
151
175
  }
152
176
  },
153
177
 
@@ -227,6 +251,8 @@ export default {
227
251
  :label="t('cluster.machinePool.name.label')"
228
252
  :required="true"
229
253
  :disabled="!value.config || !!value.config.id || busy"
254
+ :rules="fvGetAndReportPathRules(MACHINE_POOL_VALIDATION.FIELDS.NAME)"
255
+ data-testid="machine-pool-name-input"
230
256
  />
231
257
  </div>
232
258
  <div class="col span-4">
@@ -238,6 +264,8 @@ export default {
238
264
  type="number"
239
265
  min="0"
240
266
  :required="true"
267
+ :rules="fvGetAndReportPathRules(MACHINE_POOL_VALIDATION.FIELDS.QUANTITY)"
268
+ data-testid="machine-pool-quantity-input"
241
269
  />
242
270
  </div>
243
271
  <div class="col span-4 pt-5">
@@ -90,8 +90,8 @@ export default {
90
90
  v-model:value="etcd.snapshotRetention"
91
91
  :mode="mode"
92
92
  :label="t('cluster.rke2.etcd.snapshotRetention.label')"
93
- :suffix="t('cluster.rke2.snapshots.suffix')"
94
- :tooltip="t('cluster.rke2.etcd.snapshotRetention.tooltip')"
93
+ :suffix="s3Backup ? t('cluster.rke2.snapshots.s3Suffix') : t('cluster.rke2.snapshots.suffix')"
94
+ :tooltip="s3Backup ? t('cluster.rke2.etcd.snapshotRetention.tooltip') : undefined"
95
95
  />
96
96
  </div>
97
97
  </div>
package/edit/token.vue CHANGED
@@ -210,6 +210,7 @@ export default {
210
210
  :disabled="form.expiryType !== 'custom'"
211
211
  type="number"
212
212
  :mode="mode"
213
+ :aria-label="t('accountAndKeys.apiKeys.add.ariaLabel.expiration')"
213
214
  >
214
215
  <Select
215
216
  v-model:value="form.customExpiryUnits"
@@ -217,6 +218,7 @@ export default {
217
218
  :options="expiryUnitsOptions"
218
219
  :clearable="false"
219
220
  :reduce="opt=>opt.value"
221
+ :aria-label="t('accountAndKeys.apiKeys.add.ariaLabel.expirationUnits')"
220
222
  />
221
223
  </div>
222
224
  </div>
@@ -599,6 +599,7 @@ export default {
599
599
  v-if="!isView"
600
600
  type="button"
601
601
  class="btn-sm role-link"
602
+ data-testid="workload-button-add-container"
602
603
  @click="addContainerBtn"
603
604
  >
604
605
  <i class="icon icon-plus pr-5" /> {{ t('workload.container.addContainer') }}
@@ -618,8 +618,6 @@ export default {
618
618
  this.registerBeforeHook(this.getPorts, 'getPorts');
619
619
 
620
620
  this.registerAfterHook(this.saveService, 'saveService');
621
-
622
- this.selectContainer(this.container);
623
621
  },
624
622
 
625
623
  methods: {
@@ -225,6 +225,7 @@ export default {
225
225
  height="auto"
226
226
  styles="max-height: 100vh;"
227
227
  :click-to-close="!restart || !waiting"
228
+ :trigger-focus-trap="true"
228
229
  @close="close"
229
230
  >
230
231
  <Card
@@ -113,24 +113,32 @@ export default {
113
113
  },
114
114
 
115
115
  createLocation() {
116
- return {
117
- name: 'c-cluster-product-resource-create',
118
- params: {
119
- product: this.$store.getters['currentProduct'].name,
120
- resource: this.resource
121
- },
116
+ const options = this.$store.getters[`type-map/optionsFor`](this.resource)?.custom || {};
117
+ const params = {
118
+ product: this.$store.getters['currentProduct'].name,
119
+ resource: this.resource
120
+ };
121
+ const defaultLocation = {
122
+ name: 'c-cluster-product-resource-create',
123
+ params
122
124
  };
125
+
126
+ return options.createLocation ? options.createLocation(params) : defaultLocation;
123
127
  },
124
128
 
125
129
  importLocation() {
126
- return {
127
- name: 'c-cluster-product-resource-create',
128
- params: {
129
- product: this.$store.getters['currentProduct'].name,
130
- resource: this.resource
131
- },
130
+ const options = this.$store.getters[`type-map/optionsFor`](this.resource)?.custom || {};
131
+ const params = {
132
+ product: this.$store.getters['currentProduct'].name,
133
+ resource: this.resource
134
+ };
135
+ const defaultLocation = {
136
+ name: 'c-cluster-product-resource-create',
137
+ params,
132
138
  query: { [MODE]: _IMPORT }
133
139
  };
140
+
141
+ return options.importLocation ? options.importLocation(params) : defaultLocation;
134
142
  },
135
143
 
136
144
  canImport() {
@@ -1,4 +1,4 @@
1
- import { mount } from '@vue/test-utils';
1
+ import { mount, shallowMount } from '@vue/test-utils';
2
2
  import vmwarevsphere from '@shell/machine-config/vmwarevsphere.vue';
3
3
  import { DEFAULT_VALUES, SENTINEL } from '@shell/machine-config/vmwarevsphere-config';
4
4
 
@@ -172,12 +172,12 @@ describe('component: vmwarevsphere', () => {
172
172
  name: 'tag_name',
173
173
  category: 'tag_category',
174
174
  };
175
- const expectedReslut = [{
175
+ const expectedResult = [{
176
176
  ...tag, label: `${ tag.category } / ${ tag.name }`, value: tag.id
177
177
  }];
178
178
  const wrapper = mount(vmwarevsphere, defaultCreateSetup);
179
179
 
180
- expect(wrapper.vm.mapTagsToContent([tag])).toStrictEqual(expectedReslut);
180
+ expect(wrapper.vm.mapTagsToContent([tag])).toStrictEqual(expectedResult);
181
181
  });
182
182
  });
183
183
 
@@ -254,4 +254,49 @@ describe('component: vmwarevsphere', () => {
254
254
  });
255
255
  });
256
256
  });
257
+
258
+ describe('syncNetworkValueForLegacyLabels', () => {
259
+ it('should update the current network value properly', () => {
260
+ const legacyName = 'legacy_name';
261
+ const legacyValue = 'legacy_value';
262
+ const networkLabel = 'network_label';
263
+
264
+ const wrapper = shallowMount(vmwarevsphere, {
265
+ ...defaultEditSetup,
266
+ propsData: {
267
+ ...defaultEditSetup.propsData,
268
+ value: {
269
+ ...defaultEditSetup.propsData.value,
270
+ network: [legacyName]
271
+ }
272
+ },
273
+ computed: {
274
+ networks: () => [
275
+ {
276
+ name: legacyName, label: networkLabel, value: legacyValue
277
+ },
278
+ {
279
+ name: 'name1', label: 'label1', value: 'value1'
280
+ },
281
+ {
282
+ name: 'name2', label: 'label2', value: 'value2'
283
+ },
284
+ {
285
+ name: 'name3', label: 'label3', value: 'value3'
286
+ },
287
+ {
288
+ name: 'name4', label: 'label4', value: 'value4'
289
+ },
290
+ ]
291
+ }
292
+ });
293
+
294
+ // check the current network before updating
295
+ expect(wrapper.vm.value.network).toStrictEqual([legacyName]);
296
+
297
+ wrapper.vm.syncNetworkValueForLegacyLabels();
298
+
299
+ expect(wrapper.vm.value.network).toStrictEqual([legacyValue]);
300
+ });
301
+ });
257
302
  });
@@ -558,6 +558,7 @@ export default {
558
558
  this.resetValueIfNecessary('network', content, options, true);
559
559
 
560
560
  set(this, 'networksResults', content);
561
+ this.syncNetworkValueForLegacyLabels();
561
562
  this.vappMode = this.getInitialVappMode(this.value);
562
563
  },
563
564
 
@@ -669,6 +670,21 @@ export default {
669
670
  }
670
671
  },
671
672
 
673
+ // Network labels have been updated to include the MOID.
674
+ // To ensure previously selected networks remain consistent with this change,
675
+ // we update the current network value to allow correct selection from the network list.
676
+ syncNetworkValueForLegacyLabels() {
677
+ const currentNetwork = this.value.network[0];
678
+
679
+ if (this.mode !== _CREATE && currentNetwork) {
680
+ const networkMatch = this.networks.find((network) => currentNetwork === network.name && currentNetwork !== network.label);
681
+
682
+ if (networkMatch) {
683
+ this.value.network = [networkMatch.value];
684
+ }
685
+ }
686
+ },
687
+
672
688
  mapPathOptionsToContent(pathOptions) {
673
689
  return (pathOptions || []).map((pathOption) => {
674
690
  return {
@@ -84,7 +84,31 @@ describe('class Namespace', () => {
84
84
  });
85
85
 
86
86
  it.todo('should return the project');
87
- it.todo('should return the groupByLabel with i18n');
87
+
88
+ describe('handling groupById', () => {
89
+ it('should return the groupById if have project id', () => {
90
+ const namespace = new Namespace({});
91
+
92
+ jest.spyOn(namespace, 'project', 'get').mockReturnValue({
93
+ id: 'mock-project-id',
94
+ type: 'project',
95
+ name: 'mock-project',
96
+ });
97
+
98
+ expect(namespace.groupById).toStrictEqual('mock-project-id');
99
+ });
100
+
101
+ it('should return the groupById if project id undefined', () => {
102
+ const t = jest.fn(() => 'Not in a Project');
103
+ const ctx = { rootGetters: { 'i18n/t': t } };
104
+ const namespace = new Namespace({}, ctx);
105
+
106
+ jest.spyOn(namespace, 'project', 'get').mockReturnValue({});
107
+
108
+ expect(namespace.groupById).toStrictEqual('Not in a Project');
109
+ });
110
+ });
111
+
88
112
  it.todo('should return the project name with i18n');
89
113
  it.todo('should return the projectNameSort');
90
114
  it.todo('should check if istioInstalled');
@@ -275,6 +275,11 @@ export default class CloudCredential extends NormanModel {
275
275
  expired: false,
276
276
  expiring: true,
277
277
  };
278
+ } else if (this.expiresIn) {
279
+ return {
280
+ expired: false,
281
+ expiring: false,
282
+ };
278
283
  }
279
284
 
280
285
  return null;
@@ -72,14 +72,17 @@ export default class KontainerDriver extends Driver {
72
72
  return this.$dispatch('rancher/request', {
73
73
  url: `v3/kontainerDrivers/${ escape(this.id) }?action=activate`,
74
74
  method: 'post',
75
- }, { root: true });
75
+ }, { root: true }).catch((err) => {
76
+ this.$dispatch('growl/fromError', { title: this.t('drivers.error.activate', { name: this.nameDisplay }), err }, { root: true });
77
+ });
76
78
  }
77
79
 
78
80
  async activateBulk(resources) {
79
81
  await Promise.all(resources.map((resource) => this.$dispatch('rancher/request', {
80
82
  url: `v3/kontainerDrivers/${ escape(resource.id) }?action=activate`,
81
83
  method: 'post',
82
- }, { root: true }
83
- )));
84
+ }, { root: true }).catch((err) => {
85
+ this.$dispatch('growl/fromError', { title: this.t('drivers.error.activate', { name: resource.nameDisplay }), err }, { root: true });
86
+ })));
84
87
  }
85
88
  }
@@ -168,12 +168,12 @@ export default class MgmtNode extends HybridModel {
168
168
  }
169
169
 
170
170
  get canScaleDown() {
171
- if (!this.isEtcd && !this.isControlPlane) {
171
+ const hasAction = this.norman?.actions?.scaledown;
172
+
173
+ if (!this.isEtcd && !this.isControlPlane && hasAction) {
172
174
  return true;
173
175
  }
174
176
 
175
- const hasAction = this.norman?.actions?.scaledown;
176
-
177
177
  return hasAction && notOnlyOfRole(this, this.provisioningCluster?.nodes);
178
178
  }
179
179
  }
@@ -5,7 +5,6 @@ import {
5
5
  import { ISTIO, MANAGEMENT } from '@shell/config/types';
6
6
 
7
7
  import { get, set } from '@shell/utils/object';
8
- import { escapeHtml } from '@shell/utils/string';
9
8
  import { insertAt, isArray } from '@shell/utils/array';
10
9
  import SteveModel from '@shell/plugins/steve/steve-class';
11
10
  import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
@@ -124,11 +123,11 @@ export default class Namespace extends SteveModel {
124
123
  return project;
125
124
  }
126
125
 
127
- get groupByLabel() {
128
- const name = this.project?.nameDisplay;
126
+ get groupById() {
127
+ const projectId = this.project?.id;
129
128
 
130
- if ( name ) {
131
- return this.$rootGetters['i18n/t']('resourceTable.groupLabel.project', { name: escapeHtml(name) });
129
+ if ( projectId ) {
130
+ return projectId;
132
131
  } else {
133
132
  return this.$rootGetters['i18n/t']('resourceTable.groupLabel.notInAProject');
134
133
  }
@@ -80,14 +80,17 @@ export default class NodeDriver extends Driver {
80
80
  return this.$dispatch('rancher/request', {
81
81
  url: `v3/nodeDrivers/${ escape(this.id) }?action=activate`,
82
82
  method: 'post',
83
- }, { root: true });
83
+ }, { root: true }).catch((err) => {
84
+ this.$dispatch('growl/fromError', { title: this.t('drivers.error.activate', { name: this.nameDisplay }), err }, { root: true });
85
+ });
84
86
  }
85
87
 
86
88
  async activateBulk(resources) {
87
89
  await Promise.all(resources.map((resource) => this.$dispatch('rancher/request', {
88
90
  url: `v3/nodeDrivers/${ escape(resource.id) }?action=activate`,
89
91
  method: 'post',
90
- }, { root: true }
91
- )));
92
+ }, { root: true }).catch((err) => {
93
+ this.$dispatch('growl/fromError', { title: this.t('drivers.error.activate', { name: resource.nameDisplay }), err }, { root: true });
94
+ })));
92
95
  }
93
96
  }
@@ -34,7 +34,10 @@ export default class Workload extends WorkloadService {
34
34
  enabled: !!this.links.update,
35
35
  });
36
36
 
37
- if (type !== WORKLOAD_TYPES.JOB && type !== WORKLOAD_TYPES.CRON_JOB) {
37
+ if (type !== WORKLOAD_TYPES.JOB &&
38
+ type !== WORKLOAD_TYPES.CRON_JOB &&
39
+ type !== WORKLOAD_TYPES.REPLICA_SET
40
+ ) {
38
41
  insertAt(out, 0, {
39
42
  action: 'toggleRollbackModal',
40
43
  label: this.t('action.rollback'),