@rancher/shell 3.0.9-rc.2 → 3.0.9-rc.4

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 (59) hide show
  1. package/assets/translations/en-us.yaml +24 -2
  2. package/assets/translations/zh-hans.yaml +13 -0
  3. package/components/ActionMenu.vue +7 -8
  4. package/components/ActionMenuShell.vue +19 -20
  5. package/components/Resource/Detail/Card/Scaler.vue +10 -2
  6. package/components/Resource/Detail/Card/StatusCard/index.vue +4 -1
  7. package/components/ResourceTable.vue +1 -1
  8. package/components/Tabbed/Tab.vue +4 -0
  9. package/components/Tabbed/index.vue +11 -3
  10. package/components/__tests__/ProjectRow.test.ts +102 -15
  11. package/components/form/ResourceQuota/Project.vue +59 -8
  12. package/components/form/ResourceQuota/ProjectRow.vue +116 -21
  13. package/components/form/ResourceQuota/shared.js +42 -18
  14. package/components/formatter/KubeconfigClusters.vue +74 -0
  15. package/components/formatter/LinkName.vue +3 -2
  16. package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
  17. package/config/product/explorer.js +1 -1
  18. package/config/product/manager.js +29 -2
  19. package/config/router/routes.js +4 -1
  20. package/config/table-headers.js +9 -7
  21. package/config/types.js +4 -1
  22. package/detail/management.cattle.io.oidcclient.vue +15 -4
  23. package/edit/__tests__/management.cattle.io.project.test.js +137 -0
  24. package/edit/management.cattle.io.project.vue +36 -6
  25. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +16 -3
  26. package/edit/provisioning.cattle.io.cluster/defaults.ts +1 -0
  27. package/edit/provisioning.cattle.io.cluster/rke2.vue +2 -1
  28. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +57 -2
  29. package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +109 -0
  30. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +1 -0
  31. package/initialize/install-plugins.js +0 -2
  32. package/list/ext.cattle.io.kubeconfig.vue +118 -0
  33. package/mixins/__tests__/chart.test.ts +147 -0
  34. package/mixins/chart.js +10 -8
  35. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
  36. package/models/__tests__/secret.test.ts +55 -0
  37. package/models/ext.cattle.io.kubeconfig.ts +97 -0
  38. package/models/management.cattle.io.cluster.js +22 -30
  39. package/models/provisioning.cattle.io.cluster.js +2 -2
  40. package/models/secret.js +1 -1
  41. package/package.json +2 -2
  42. package/pages/__tests__/diagnostic.test.ts +71 -0
  43. package/pages/about.vue +3 -2
  44. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
  45. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  46. package/pages/c/_cluster/explorer/tools/index.vue +23 -5
  47. package/pages/c/_cluster/monitoring/alertmanagerconfig/_alertmanagerconfigid/receiver.vue +18 -5
  48. package/pages/c/_cluster/uiplugins/index.vue +40 -8
  49. package/pages/diagnostic.vue +17 -3
  50. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +1 -0
  51. package/rancher-components/RcItemCard/RcItemCard.test.ts +20 -8
  52. package/rancher-components/RcItemCard/RcItemCard.vue +38 -31
  53. package/store/__tests__/auth.test.ts +21 -5
  54. package/store/auth.js +6 -3
  55. package/types/shell/index.d.ts +177 -157
  56. package/utils/__tests__/chart.test.ts +96 -0
  57. package/utils/__tests__/version.test.ts +1 -19
  58. package/utils/chart.js +64 -0
  59. package/utils/version.js +5 -17
@@ -1,5 +1,5 @@
1
1
  import { CATTLE_PUBLIC_ENDPOINTS, UI_PROJECT_SECRET_COPY } from '@shell/config/labels-annotations';
2
- import { NODE as NODE_TYPE } from '@shell/config/types';
2
+ import { NODE as NODE_TYPE, NAMESPACE as NAMESPACE_TYPE } from '@shell/config/types';
3
3
  import { COLUMN_BREAKPOINTS } from '@shell/types/store/type-map';
4
4
 
5
5
  // Note: 'id' is always the last sort, so you don't have to specify it here.
@@ -158,12 +158,14 @@ export const NAME_UNLINKED = {
158
158
  };
159
159
 
160
160
  export const NAMESPACE = {
161
- name: 'namespace',
162
- labelKey: 'tableHeaders.namespace',
163
- value: 'namespace',
164
- getValue: (row) => row.namespace,
165
- sort: 'namespace',
166
- dashIfEmpty: true,
161
+ name: 'namespace',
162
+ labelKey: 'tableHeaders.namespace',
163
+ value: 'namespace',
164
+ getValue: (row) => row.namespace,
165
+ sort: 'namespace',
166
+ dashIfEmpty: true,
167
+ formatter: 'LinkName',
168
+ formatterOpts: { type: NAMESPACE_TYPE },
167
169
  };
168
170
 
169
171
  export const NODE = {
package/config/types.js CHANGED
@@ -262,7 +262,10 @@ export const BRAND = {
262
262
  RGS: 'rgs',
263
263
  };
264
264
 
265
- export const EXT = { USER_ACTIVITY: 'ext.cattle.io.useractivity' };
265
+ export const EXT = {
266
+ USER_ACTIVITY: 'ext.cattle.io.useractivity',
267
+ KUBECONFIG: 'ext.cattle.io.kubeconfig',
268
+ };
266
269
 
267
270
  export const CAPI = {
268
271
  CAPI_CLUSTER: 'cluster.x-k8s.io.cluster',
@@ -7,7 +7,7 @@ import { defineComponent } from 'vue';
7
7
  import CopyToClipboardText from '@shell/components/CopyToClipboardText.vue';
8
8
  import DateComponent from '@shell/components/formatter/Date.vue';
9
9
  import { RcItemCard } from '@components/RcItemCard';
10
- import ActionMenu from '@shell/components/ActionMenuShell.vue';
10
+ import ActionMenu, { type ActionMenuSelection } from '@shell/components/ActionMenuShell.vue';
11
11
  import { Banner } from '@components/Banner';
12
12
 
13
13
  type SecretActionType = 'create-secret' | 'regen-secret' | 'remove-secret'
@@ -32,7 +32,6 @@ interface SecretManageData {
32
32
  const OIDC_SECRETS_NAMESPACE = 'cattle-oidc-client-secrets';
33
33
 
34
34
  export default defineComponent({
35
- emits: ['regenerateSecret', 'removeSecret'],
36
35
 
37
36
  components: {
38
37
  CopyToClipboardText,
@@ -100,6 +99,19 @@ export default defineComponent({
100
99
  },
101
100
 
102
101
  methods: {
102
+ handleSecretAction(secret: SecretManageData, payload?: ActionMenuSelection) {
103
+ switch (payload?.action) {
104
+ case 'regenerateSecret':
105
+ this.promptSecretsModal(OIDC_CLIENT_SECRET_ACTION.REGEN, secret);
106
+ break;
107
+ case 'removeSecret':
108
+ this.promptSecretsModal(OIDC_CLIENT_SECRET_ACTION.REMOVE, secret);
109
+ break;
110
+ default:
111
+ console.warn(`Unknown secret action: ${ payload?.action }`); // eslint-disable-line no-console
112
+ }
113
+ },
114
+
103
115
  promptSecretsModal(actionType: SecretActionType, secret: SecretManageData) {
104
116
  this.errors = [];
105
117
 
@@ -320,8 +332,7 @@ export default defineComponent({
320
332
  :data-testid="`oidc-client-secret-${i}-action-menu`"
321
333
  :resource="secret"
322
334
  :custom-actions="cardActions"
323
- @regenerateSecret="promptSecretsModal(OIDC_CLIENT_SECRET_ACTION.REGEN, secret)"
324
- @removeSecret="promptSecretsModal(OIDC_CLIENT_SECRET_ACTION.REMOVE, secret)"
335
+ @action-invoked="(payload) => handleSecretAction(secret, payload)"
325
336
  />
326
337
  </template>
327
338
  </rc-item-card>
@@ -0,0 +1,137 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import ManagementCattleIoProject from '@shell/edit/management.cattle.io.project.vue';
3
+ import { _EDIT } from '@shell/config/query-params';
4
+
5
+ const mockStore = {
6
+ getters: {
7
+ 'management/schemaFor': () => ({}),
8
+ currentCluster: () => ({ spec: { kubernetesVersion: 'v1.23.0' } }),
9
+ isStandaloneHarvester: () => false,
10
+ currentProduct: () => ({ inStore: 'rancher' }),
11
+ 'auth/principalId': () => 'local://user',
12
+ 'i18n/t': (key) => key,
13
+ },
14
+ dispatch: jest.fn(),
15
+ };
16
+
17
+ const defaultMountOptions = {
18
+ props: { mode: _EDIT },
19
+ global: { mocks: { $store: mockStore } }
20
+ };
21
+
22
+ describe('component: ManagementCattleIoProject', () => {
23
+ it('should remove a standard resource quota correctly', async() => {
24
+ const value = {
25
+ spec: {
26
+ resourceQuota: { limit: { limitsCpu: '100m', limitsMemory: '1Gi' } },
27
+ namespaceDefaultResourceQuota: { limit: { limitsCpu: '50m', limitsMemory: '500Mi' } }
28
+ },
29
+ metadata: { namespace: 'test-ns', annotations: {} },
30
+ save: jest.fn(),
31
+ listLocation: { name: 'list' }
32
+ };
33
+
34
+ const wrapper = shallowMount(
35
+ ManagementCattleIoProject,
36
+ {
37
+ ...defaultMountOptions,
38
+ props: {
39
+ ...defaultMountOptions.props,
40
+ value,
41
+ },
42
+ }
43
+ );
44
+
45
+ wrapper.vm.removeQuota('limitsCpu');
46
+
47
+ expect(wrapper.vm.value.spec.resourceQuota.limit.limitsCpu).toBeUndefined();
48
+ expect(wrapper.vm.value.spec.namespaceDefaultResourceQuota.limit.limitsCpu).toBeUndefined();
49
+ expect(wrapper.vm.value.spec.resourceQuota.limit.limitsMemory).toBe('1Gi'); // Ensure other quotas are not affected
50
+ });
51
+
52
+ it('should remove a custom resource quota with single period in name correctly', async() => {
53
+ const value = {
54
+ spec: {
55
+ resourceQuota: { limit: { extended: { test2: '1' } } },
56
+ namespaceDefaultResourceQuota: { limit: { extended: { test2: '2' } } }
57
+ },
58
+ metadata: { namespace: 'test-ns', annotations: {} },
59
+ save: jest.fn(),
60
+ listLocation: { name: 'list' }
61
+ };
62
+
63
+ const wrapper = shallowMount(
64
+ ManagementCattleIoProject,
65
+ {
66
+ ...defaultMountOptions,
67
+ props: {
68
+ ...defaultMountOptions.props,
69
+ value,
70
+ },
71
+ }
72
+ );
73
+
74
+ wrapper.vm.removeQuota(`extended.test2`);
75
+
76
+ expect(wrapper.vm.value.spec.resourceQuota.limit.extended).toBeUndefined();
77
+ expect(wrapper.vm.value.spec.namespaceDefaultResourceQuota.limit.extended).toBeUndefined();
78
+ });
79
+
80
+ it('should remove a custom resource quota with multiple periods in name correctly', async() => {
81
+ const value = {
82
+ spec: {
83
+ resourceQuota: { limit: { extended: { 'requests.nvidia.com/gpu': '4' } } },
84
+ namespaceDefaultResourceQuota: { limit: { extended: { 'requests.nvidia.com/gpu': '2' } } }
85
+ },
86
+ metadata: { namespace: 'test-ns', annotations: {} },
87
+ save: jest.fn(),
88
+ listLocation: { name: 'list' }
89
+ };
90
+
91
+ const wrapper = shallowMount(
92
+ ManagementCattleIoProject,
93
+ {
94
+ ...defaultMountOptions,
95
+ props: {
96
+ ...defaultMountOptions.props,
97
+ value,
98
+ },
99
+ }
100
+ );
101
+
102
+ wrapper.vm.removeQuota(`extended.requests.nvidia.com/gpu`);
103
+
104
+ expect(wrapper.vm.value.spec.resourceQuota.limit.extended).toBeUndefined();
105
+ expect(wrapper.vm.value.spec.namespaceDefaultResourceQuota.limit.extended).toBeUndefined();
106
+ });
107
+
108
+ it('should remove one of multiple custom resource quotas correctly', async() => {
109
+ const value = {
110
+ spec: {
111
+ resourceQuota: { limit: { extended: { test1: '1', test2: '2' } } },
112
+ namespaceDefaultResourceQuota: { limit: { extended: { test1: '3', test2: '4' } } }
113
+ },
114
+ metadata: { namespace: 'test-ns', annotations: {} },
115
+ save: jest.fn(),
116
+ listLocation: { name: 'list' }
117
+ };
118
+
119
+ const wrapper = shallowMount(
120
+ ManagementCattleIoProject,
121
+ {
122
+ ...defaultMountOptions,
123
+ props: {
124
+ ...defaultMountOptions.props,
125
+ value,
126
+ },
127
+ }
128
+ );
129
+
130
+ wrapper.vm.removeQuota('extended.test1');
131
+
132
+ expect(wrapper.vm.value.spec.resourceQuota.limit.extended.test1).toBeUndefined();
133
+ expect(wrapper.vm.value.spec.resourceQuota.limit.extended.test2).toBe('2');
134
+ expect(wrapper.vm.value.spec.namespaceDefaultResourceQuota.limit.extended.test1).toBeUndefined();
135
+ expect(wrapper.vm.value.spec.namespaceDefaultResourceQuota.limit.extended.test2).toBe('4');
136
+ });
137
+ });
@@ -6,7 +6,7 @@ import FormValidation from '@shell/mixins/form-validation';
6
6
  import CruResource from '@shell/components/CruResource';
7
7
  import Labels from '@shell/components/form/Labels';
8
8
  import ResourceQuota from '@shell/components/form/ResourceQuota/Project';
9
- import { HARVESTER_TYPES, RANCHER_TYPES } from '@shell/components/form/ResourceQuota/shared';
9
+ import { HARVESTER_TYPES, RANCHER_TYPES, TYPES } from '@shell/components/form/ResourceQuota/shared';
10
10
  import Tab from '@shell/components/Tabbed/Tab';
11
11
  import Tabbed from '@shell/components/Tabbed';
12
12
  import NameNsDescription from '@shell/components/form/NameNsDescription';
@@ -49,6 +49,7 @@ export default {
49
49
  HARVESTER_TYPES,
50
50
  RANCHER_TYPES,
51
51
  fvFormRuleSets: [{ path: 'spec.displayName', rules: ['required'] }],
52
+ resourceQuotaKey: 0,
52
53
  };
53
54
  },
54
55
  computed: {
@@ -157,14 +158,42 @@ export default {
157
158
  },
158
159
 
159
160
  removeQuota(key) {
161
+ const isExtended = key.startsWith(`${ TYPES.EXTENDED }.`);
162
+ let resourceKey = key;
163
+
164
+ if (isExtended) {
165
+ resourceKey = key.substring(`${ TYPES.EXTENDED }.`.length);
166
+ }
167
+
160
168
  ['resourceQuota', 'namespaceDefaultResourceQuota'].forEach((specProp) => {
161
- if (this.value?.spec[specProp]?.limit && this.value?.spec[specProp]?.limit[key]) {
162
- delete this.value?.spec[specProp]?.limit[key];
163
- }
164
- if (this.value?.spec[specProp]?.usedLimit && this.value?.spec[specProp]?.usedLimit[key]) {
165
- delete this.value?.spec[specProp]?.usedLimit[key];
169
+ const limit = this.value?.spec[specProp]?.limit;
170
+ const usedLimit = this.value?.spec[specProp]?.usedLimit;
171
+
172
+ if (isExtended) {
173
+ if (limit?.extended && typeof limit.extended[resourceKey] !== 'undefined') {
174
+ delete limit.extended[resourceKey];
175
+ if (Object.keys(limit.extended).length === 0) {
176
+ delete limit.extended;
177
+ }
178
+ }
179
+ if (usedLimit?.extended && typeof usedLimit.extended[resourceKey] !== 'undefined') {
180
+ delete usedLimit.extended[resourceKey];
181
+ if (Object.keys(usedLimit.extended).length === 0) {
182
+ delete usedLimit.extended;
183
+ }
184
+ }
185
+ } else {
186
+ if (limit && typeof limit[resourceKey] !== 'undefined') {
187
+ delete limit[resourceKey];
188
+ }
189
+ if (usedLimit && typeof usedLimit[resourceKey] !== 'undefined') {
190
+ delete usedLimit[resourceKey];
191
+ }
166
192
  }
167
193
  });
194
+
195
+ // Incrementing the key forces the ResourceQuota component to re-render
196
+ this.resourceQuotaKey++;
168
197
  }
169
198
  },
170
199
  };
@@ -224,6 +253,7 @@ export default {
224
253
  :weight="9"
225
254
  >
226
255
  <ResourceQuota
256
+ :key="resourceQuotaKey"
227
257
  :value="value"
228
258
  :mode="canEditTabElements"
229
259
  :types="isStandaloneHarvester ? HARVESTER_TYPES : RANCHER_TYPES"
@@ -177,6 +177,21 @@ export default {
177
177
  this.alertmanagerConfigResource.spec.receivers = receiversMinusDeletedItem;
178
178
  // After saving the AlertmanagerConfig, the resource has been deleted.
179
179
  this.alertmanagerConfigResource.save(...arguments);
180
+ },
181
+ handleReceiverAction(payload) {
182
+ switch (payload?.action) {
183
+ case 'goToEdit':
184
+ this.goToEdit();
185
+ break;
186
+ case 'goToEditYaml':
187
+ this.goToEditYaml();
188
+ break;
189
+ case 'promptRemove':
190
+ this.promptRemove();
191
+ break;
192
+ default:
193
+ console.warn(`Unknown receiver action: ${ payload?.action }`); // eslint-disable-line no-console
194
+ }
180
195
  }
181
196
  }
182
197
  };
@@ -263,9 +278,7 @@ export default {
263
278
  :custom-target-element="actionMenuTargetElement"
264
279
  :custom-target-event="actionMenuTargetEvent"
265
280
  @close="receiverActionMenuIsOpen = false"
266
- @goToEdit="goToEdit"
267
- @goToEditYaml="goToEditYaml"
268
- @promptRemove="promptRemove"
281
+ @action-invoked="handleReceiverAction"
269
282
  />
270
283
  </CruResource>
271
284
  </template>
@@ -0,0 +1 @@
1
+ export const RETENTION_DEFAULT = 5;
@@ -66,6 +66,7 @@ import { DEFAULT_COMMON_BASE_PATH, DEFAULT_SUBDIRS } from '@shell/edit/provision
66
66
  import ClusterAppearance from '@shell/components/form/ClusterAppearance';
67
67
  import AddOnAdditionalManifest from '@shell/edit/provisioning.cattle.io.cluster/tabs/AddOnAdditionalManifest';
68
68
  import VsphereUtils, { VMWARE_VSPHERE } from '@shell/utils/v-sphere';
69
+ import { RETENTION_DEFAULT } from '@shell/edit/provisioning.cattle.io.cluster/defaults';
69
70
  import { mapGetters } from 'vuex';
70
71
  const HARVESTER = 'harvester';
71
72
  const GOOGLE = 'google';
@@ -1061,7 +1062,7 @@ export default {
1061
1062
  this.rkeConfig.etcd = {
1062
1063
  disableSnapshots: false,
1063
1064
  s3: null,
1064
- snapshotRetention: 5,
1065
+ snapshotRetention: RETENTION_DEFAULT,
1065
1066
  snapshotScheduleCron: '0 */5 * * *',
1066
1067
  };
1067
1068
  } else if (typeof this.rkeConfig.etcd.disableSnapshots === 'undefined') {
@@ -4,6 +4,10 @@ import { LabeledInput } from '@components/Form/LabeledInput';
4
4
  import SelectOrCreateAuthSecret from '@shell/components/form/SelectOrCreateAuthSecret';
5
5
  import { NORMAN } from '@shell/config/types';
6
6
  import FormValidation from '@shell/mixins/form-validation';
7
+ import UnitInput from '@shell/components/form/UnitInput';
8
+ import RadioGroup from '@components/Form/Radio/RadioGroup.vue';
9
+ import { _CREATE } from '@shell/config/query-params';
10
+ import { RETENTION_DEFAULT } from '@shell/edit/provisioning.cattle.io.cluster/defaults';
7
11
 
8
12
  export default {
9
13
  emits: ['update:value', 'validationChanged'],
@@ -12,6 +16,8 @@ export default {
12
16
  LabeledInput,
13
17
  Checkbox,
14
18
  SelectOrCreateAuthSecret,
19
+ UnitInput,
20
+ RadioGroup
15
21
  },
16
22
  mixins: [FormValidation],
17
23
 
@@ -35,6 +41,10 @@ export default {
35
41
  type: Function,
36
42
  required: true,
37
43
  },
44
+ localRetentionCount: {
45
+ type: Number,
46
+ default: null,
47
+ }
38
48
  },
39
49
 
40
50
  data() {
@@ -47,9 +57,11 @@ export default {
47
57
  folder: '',
48
58
  region: '',
49
59
  skipSSLVerify: false,
60
+ retention: null,
50
61
  ...(this.value || {}),
51
62
  },
52
- fvFormRuleSets: [
63
+ differentRetention: false,
64
+ fvFormRuleSets: [
53
65
  {
54
66
  path: 'endpoint', rootObject: this.config, rules: ['awsStyleEndpoint']
55
67
  },
@@ -59,6 +71,9 @@ export default {
59
71
  ]
60
72
  };
61
73
  },
74
+ mounted() {
75
+ this.differentRetention = !(this.mode === _CREATE || this.value?.retention === this.localRetentionCount);
76
+ },
62
77
 
63
78
  computed: {
64
79
 
@@ -73,10 +88,25 @@ export default {
73
88
 
74
89
  return {};
75
90
  },
91
+
92
+ localCountToUse() {
93
+ return this.localRetentionCount === null || this.localRetentionCount === undefined ? RETENTION_DEFAULT : this.localRetentionCount;
94
+ },
95
+ retentionOptionsOptions() {
96
+ return [
97
+ { label: this.t('cluster.rke2.etcd.s3config.snapshotRetention.options.localDefined', { count: this.localCountToUse }), value: false }, { label: this.t('cluster.rke2.etcd.s3config.snapshotRetention.options.manual'), value: true }
98
+ ];
99
+ }
76
100
  },
77
101
  watch: {
78
102
  fvFormIsValid(newValue) {
79
103
  this.$emit('validationChanged', !!newValue);
104
+ },
105
+ localRetentionCount(neu) {
106
+ if (!this.differentRetention) {
107
+ this.config.retention = this.localCountToUse;
108
+ this.update();
109
+ }
80
110
  }
81
111
  },
82
112
 
@@ -86,6 +116,10 @@ export default {
86
116
 
87
117
  this.$emit('update:value', out);
88
118
  },
119
+ resetRetention() {
120
+ this.config.retention = this.localCountToUse;
121
+ this.update();
122
+ }
89
123
  },
90
124
  };
91
125
  </script>
@@ -150,7 +184,6 @@ export default {
150
184
  />
151
185
  </div>
152
186
  </div>
153
-
154
187
  <div
155
188
  v-if="!ccData.defaultSkipSSLVerify"
156
189
  class="mt-20"
@@ -172,5 +205,27 @@ export default {
172
205
  @update:value="update"
173
206
  />
174
207
  </div>
208
+ <div class="row mt-20">
209
+ <div class="col span-6">
210
+ <h4>{{ t('cluster.rke2.etcd.s3config.snapshotRetention.title') }}</h4>
211
+ <RadioGroup
212
+ v-model:value="differentRetention"
213
+ name="s3config-retention"
214
+ :mode="mode"
215
+ :options="retentionOptionsOptions"
216
+ :row="true"
217
+ @update:value="resetRetention"
218
+ />
219
+ <UnitInput
220
+ v-if="differentRetention"
221
+ v-model:value="config.retention"
222
+ :label="t('cluster.rke2.etcd.s3config.snapshotRetention.label')"
223
+ :mode="mode"
224
+ :suffix="t('cluster.rke2.snapshots.s3Suffix')"
225
+ class="mt-10"
226
+ @update:value="update"
227
+ />
228
+ </div>
229
+ </div>
175
230
  </div>
176
231
  </template>
@@ -0,0 +1,109 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import S3Config from '@shell/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue';
3
+
4
+ describe('s3Config', () => {
5
+ const defaultProps = {
6
+ mode: 'create',
7
+ namespace: 'test-ns',
8
+ registerBeforeHook: jest.fn(),
9
+ localRetentionCount: 5,
10
+ value: {}
11
+ };
12
+
13
+ const mockStore = { getters: { 'rancher/byId': jest.fn() } };
14
+
15
+ const createWrapper = (props = {}) => {
16
+ return shallowMount(S3Config, {
17
+ propsData: {
18
+ ...defaultProps,
19
+ ...props
20
+ },
21
+ mocks: {
22
+ $store: mockStore,
23
+ t: (key) => key,
24
+ },
25
+ stubs: {
26
+ LabeledInput: true,
27
+ Checkbox: true,
28
+ SelectOrCreateAuthSecret: true,
29
+ UnitInput: true,
30
+ RadioGroup: true,
31
+ }
32
+ });
33
+ };
34
+
35
+ it('renders correctly', () => {
36
+ const wrapper = createWrapper();
37
+
38
+ expect(wrapper.exists()).toBe(true);
39
+ });
40
+
41
+ it('initializes config with default values', () => {
42
+ const wrapper = createWrapper();
43
+
44
+ expect(wrapper.vm.config.bucket).toBe('');
45
+ expect(wrapper.vm.config.skipSSLVerify).toBe(false);
46
+ });
47
+
48
+ it('initializes config with provided value prop', () => {
49
+ const value = {
50
+ bucket: 'test',
51
+ region: 'us-east-1',
52
+ retention: 2
53
+ };
54
+ const wrapper = createWrapper({ value });
55
+
56
+ expect(wrapper.vm.config.bucket).toBe('test');
57
+ expect(wrapper.vm.config.region).toBe('us-east-1');
58
+ expect(wrapper.vm.config.retention).toBe(2);
59
+ });
60
+
61
+ describe('retention Logic', () => {
62
+ it('computes localCountToUse correctly', () => {
63
+ const wrapper = createWrapper({ localRetentionCount: 3 });
64
+
65
+ expect(wrapper.vm.localCountToUse).toBe(3);
66
+ });
67
+
68
+ it('uses default retention if localRetentionCount is null', () => {
69
+ const wrapper = createWrapper({ localRetentionCount: null });
70
+
71
+ expect(wrapper.vm.localCountToUse).toBe(5);
72
+ });
73
+
74
+ it('sets differentRetention to false in create mode', () => {
75
+ const wrapper = createWrapper({ mode: 'create' });
76
+
77
+ expect(wrapper.vm.differentRetention).toBe(false);
78
+ });
79
+
80
+ it('sets differentRetention to false in edit mode if retention matches', () => {
81
+ const wrapper = createWrapper({
82
+ mode: 'edit',
83
+ value: { retention: 5 },
84
+ localRetentionCount: 5
85
+ });
86
+
87
+ expect(wrapper.vm.differentRetention).toBe(false);
88
+ });
89
+
90
+ it('sets differentRetention to true in edit mode if retention differs', () => {
91
+ const wrapper = createWrapper({
92
+ mode: 'edit',
93
+ value: { retention: 2 },
94
+ localRetentionCount: 5
95
+ });
96
+
97
+ expect(wrapper.vm.differentRetention).toBe(true);
98
+ });
99
+
100
+ it('updates retention when localRetentionCount changes and differentRetention is false', async() => {
101
+ const wrapper = createWrapper({ localRetentionCount: 5 });
102
+
103
+ // differentRetention is false by default in create mode
104
+ await wrapper.setProps({ localRetentionCount: 10 });
105
+ expect(wrapper.vm.config.retention).toBe(10);
106
+ expect(wrapper.emitted('update:value')).toBeTruthy();
107
+ });
108
+ });
109
+ });
@@ -124,6 +124,7 @@ export default {
124
124
  <S3Config
125
125
  v-if="s3Backup"
126
126
  v-model:value="etcd.s3"
127
+ :local-retention-count="etcd.snapshotRetention"
127
128
  :namespace="value.metadata.namespace"
128
129
  :register-before-hook="registerBeforeHook"
129
130
  :mode="mode"
@@ -1,7 +1,6 @@
1
1
  import PortalVue from 'portal-vue';
2
2
  import Vue3Resize from 'vue3-resize';
3
3
  import FloatingVue from 'floating-vue';
4
- import vSelect from 'vue-select';
5
4
  import 'vue3-resize/dist/vue3-resize.css';
6
5
 
7
6
  // import '@shell/plugins/extend-router';
@@ -43,7 +42,6 @@ export async function installPlugins(vueApp) {
43
42
  preventContainer: ['#modal-container-element']
44
43
  });
45
44
  vueApp.use(InstallCodeMirror);
46
- vueApp.component('v-select', vSelect);
47
45
  }
48
46
 
49
47
  export async function installInjectedPlugins(app, vueApp) {