@rancher/shell 3.0.9-rc.3 → 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.
- package/assets/translations/en-us.yaml +14 -2
- package/components/formatter/KubeconfigClusters.vue +74 -0
- package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
- package/config/product/manager.js +29 -2
- package/config/router/routes.js +4 -1
- package/edit/provisioning.cattle.io.cluster/defaults.ts +1 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +2 -1
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +57 -2
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +109 -0
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +1 -0
- package/list/ext.cattle.io.kubeconfig.vue +118 -0
- package/mixins/__tests__/chart.test.ts +147 -0
- package/mixins/chart.js +10 -8
- package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
- package/models/__tests__/secret.test.ts +55 -0
- package/models/ext.cattle.io.kubeconfig.ts +97 -0
- package/models/secret.js +1 -1
- package/package.json +2 -2
- package/pages/about.vue +3 -2
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
- package/pages/c/_cluster/apps/charts/index.vue +1 -0
- package/rancher-components/RcItemCard/RcItemCard.test.ts +4 -2
- package/rancher-components/RcItemCard/RcItemCard.vue +27 -10
- package/types/shell/index.d.ts +14 -1
- package/utils/__tests__/chart.test.ts +96 -0
- package/utils/__tests__/version.test.ts +1 -19
- package/utils/chart.js +64 -0
- package/utils/version.js +5 -17
|
@@ -1183,7 +1183,7 @@ catalog:
|
|
|
1183
1183
|
subHeaderItem:
|
|
1184
1184
|
missingVersionDate: Last updated date is not available for this chart
|
|
1185
1185
|
footerItem:
|
|
1186
|
-
ariaLabel: Apply filter
|
|
1186
|
+
ariaLabel: 'Apply {filter} filter'
|
|
1187
1187
|
statusFilterCautions:
|
|
1188
1188
|
installation: Installation status cannot be determined with 100% accuracy
|
|
1189
1189
|
upgradeable: Upgradeable status cannot be determined with 100% accuracy
|
|
@@ -2543,7 +2543,7 @@ cluster:
|
|
|
2543
2543
|
snapshotScheduleCron:
|
|
2544
2544
|
label: Cron Schedule
|
|
2545
2545
|
snapshotRetention:
|
|
2546
|
-
label:
|
|
2546
|
+
label: Local snapshot retention count
|
|
2547
2547
|
tooltip: Each backup records 1 snapshot per etcd node. If you specify 3 snapshots and you have 3 etcd nodes you will only retain 1 full backup.
|
|
2548
2548
|
exportMetric:
|
|
2549
2549
|
label: Metrics
|
|
@@ -2552,6 +2552,13 @@ cluster:
|
|
|
2552
2552
|
s3backups:
|
|
2553
2553
|
label: Save Backups to S3
|
|
2554
2554
|
s3config:
|
|
2555
|
+
snapshotRetention:
|
|
2556
|
+
title: S3 Snapshot Retention
|
|
2557
|
+
label: S3 Snapshot Retention Count
|
|
2558
|
+
options:
|
|
2559
|
+
manual: Set manually
|
|
2560
|
+
localDefined: Same as local ({count} Snaphots)
|
|
2561
|
+
localUndefined: Same as local
|
|
2555
2562
|
bucket:
|
|
2556
2563
|
label: Bucket
|
|
2557
2564
|
folder:
|
|
@@ -6888,6 +6895,7 @@ tableHeaders:
|
|
|
6888
6895
|
targetPort: Target
|
|
6889
6896
|
template: Template
|
|
6890
6897
|
totalSnapshotQuota: Total Snapshot Quota
|
|
6898
|
+
ttl: TTL
|
|
6891
6899
|
type: Type
|
|
6892
6900
|
updated: Updated
|
|
6893
6901
|
up-to-date: Up To Date
|
|
@@ -9308,3 +9316,7 @@ autoscaler:
|
|
|
9308
9316
|
unavailable: Unavailable
|
|
9309
9317
|
tab:
|
|
9310
9318
|
title: Autoscaler
|
|
9319
|
+
|
|
9320
|
+
ext.cattle.io.kubeconfig:
|
|
9321
|
+
deleted: "{name} (deleted)"
|
|
9322
|
+
moreClusterCount: " + {remainingCount} more"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
interface ClusterReference {
|
|
5
|
+
label: string;
|
|
6
|
+
location?: object;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const MAX_DISPLAY = 25;
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
row: { id: string; sortedReferencedClusters?: ClusterReference[] };
|
|
13
|
+
value?: unknown[];
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const allClusters = computed<ClusterReference[]>(() => {
|
|
17
|
+
return props.row?.sortedReferencedClusters || [];
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const clusters = computed<ClusterReference[]>(() => {
|
|
21
|
+
return allClusters.value.slice(0, MAX_DISPLAY);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const remainingCount = computed<number>(() => {
|
|
25
|
+
return Math.max(0, allClusters.value.length - MAX_DISPLAY);
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<span class="kubeconfig-clusters">
|
|
31
|
+
<template
|
|
32
|
+
v-for="(cluster, index) in clusters"
|
|
33
|
+
>
|
|
34
|
+
<template v-if="index > 0">, </template>
|
|
35
|
+
<router-link
|
|
36
|
+
v-if="cluster.location"
|
|
37
|
+
:key="`${row.id}-${cluster.label}`"
|
|
38
|
+
:to="cluster.location"
|
|
39
|
+
>
|
|
40
|
+
{{ cluster.label }}
|
|
41
|
+
</router-link>
|
|
42
|
+
<span
|
|
43
|
+
v-else
|
|
44
|
+
:key="`${row.id}-${cluster.label}-deleted`"
|
|
45
|
+
class="text-muted"
|
|
46
|
+
>
|
|
47
|
+
{{ cluster.label }}
|
|
48
|
+
</span>
|
|
49
|
+
</template>
|
|
50
|
+
<span
|
|
51
|
+
v-if="remainingCount > 0"
|
|
52
|
+
class="text-muted"
|
|
53
|
+
>
|
|
54
|
+
{{ t('ext.cattle.io.kubeconfig.moreClusterCount', { remainingCount: remainingCount }) }}
|
|
55
|
+
</span>
|
|
56
|
+
<span
|
|
57
|
+
v-if="allClusters.length === 0"
|
|
58
|
+
class="text-muted"
|
|
59
|
+
>
|
|
60
|
+
—
|
|
61
|
+
</span>
|
|
62
|
+
</span>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<style lang="scss" scoped>
|
|
66
|
+
.kubeconfig-clusters {
|
|
67
|
+
display: block;
|
|
68
|
+
width: 0;
|
|
69
|
+
min-width: 100%;
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
text-overflow: ellipsis;
|
|
72
|
+
white-space: nowrap;
|
|
73
|
+
}
|
|
74
|
+
</style>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { mount, RouterLinkStub } from '@vue/test-utils';
|
|
2
|
+
import KubeconfigClusters from '@shell/components/formatter/KubeconfigClusters.vue';
|
|
3
|
+
|
|
4
|
+
describe('component: KubeconfigClusters', () => {
|
|
5
|
+
const MAX_DISPLAY = 25;
|
|
6
|
+
|
|
7
|
+
const createCluster = (label: string, hasLocation = true) => ({
|
|
8
|
+
label,
|
|
9
|
+
location: hasLocation ? { name: 'cluster-detail', params: { cluster: label } } : null
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const createClusters = (count: number, hasLocation = true) => {
|
|
13
|
+
return Array.from({ length: count }, (_, i) => createCluster(`cluster-${ i + 1 }`, hasLocation));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const defaultMocks = { t: (key: string, args: Record<string, unknown>) => `+ ${ args.remainingCount } more` };
|
|
17
|
+
|
|
18
|
+
const mountComponent = (clusters: unknown[] = [], mocks = defaultMocks) => {
|
|
19
|
+
return mount(KubeconfigClusters, {
|
|
20
|
+
props: { row: { id: 'test-row', sortedReferencedClusters: clusters } },
|
|
21
|
+
global: {
|
|
22
|
+
mocks,
|
|
23
|
+
stubs: { 'router-link': RouterLinkStub }
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('displaying clusters', () => {
|
|
29
|
+
it('should display a dash when there are no clusters', () => {
|
|
30
|
+
const wrapper = mountComponent([]);
|
|
31
|
+
const emptySpan = wrapper.find('.text-muted');
|
|
32
|
+
|
|
33
|
+
expect(emptySpan.text()).toBe('—');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should display cluster labels with router-links when clusters have locations', () => {
|
|
37
|
+
const clusters = [createCluster('local'), createCluster('downstream')];
|
|
38
|
+
const wrapper = mountComponent(clusters);
|
|
39
|
+
const links = wrapper.findAllComponents(RouterLinkStub);
|
|
40
|
+
|
|
41
|
+
expect(links).toHaveLength(2);
|
|
42
|
+
expect(links[0].text()).toBe('local');
|
|
43
|
+
expect(links[1].text()).toBe('downstream');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should display cluster labels as text-muted spans when clusters have no location', () => {
|
|
47
|
+
const clusters = [createCluster('deleted-cluster', false)];
|
|
48
|
+
const wrapper = mountComponent(clusters);
|
|
49
|
+
const mutedSpan = wrapper.find('.text-muted');
|
|
50
|
+
|
|
51
|
+
expect(mutedSpan.text()).toBe('deleted-cluster');
|
|
52
|
+
expect(wrapper.findComponent(RouterLinkStub).exists()).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should separate clusters with commas', () => {
|
|
56
|
+
const clusters = [createCluster('cluster-1'), createCluster('cluster-2')];
|
|
57
|
+
const wrapper = mountComponent(clusters);
|
|
58
|
+
|
|
59
|
+
expect(wrapper.text()).toContain(',');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('max display limit', () => {
|
|
64
|
+
it('should display all clusters when count is at or below the limit', () => {
|
|
65
|
+
const clusters = createClusters(MAX_DISPLAY);
|
|
66
|
+
const wrapper = mountComponent(clusters);
|
|
67
|
+
const links = wrapper.findAllComponents(RouterLinkStub);
|
|
68
|
+
|
|
69
|
+
expect(links).toHaveLength(MAX_DISPLAY);
|
|
70
|
+
expect(wrapper.text()).not.toContain('more');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should limit displayed clusters to MAX_DISPLAY', () => {
|
|
74
|
+
const clusters = createClusters(MAX_DISPLAY + 10);
|
|
75
|
+
const wrapper = mountComponent(clusters);
|
|
76
|
+
const links = wrapper.findAllComponents(RouterLinkStub);
|
|
77
|
+
|
|
78
|
+
expect(links).toHaveLength(MAX_DISPLAY);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should show remaining count when clusters exceed the limit', () => {
|
|
82
|
+
const totalClusters = MAX_DISPLAY + 5;
|
|
83
|
+
const clusters = createClusters(totalClusters);
|
|
84
|
+
const wrapper = mountComponent(clusters);
|
|
85
|
+
|
|
86
|
+
expect(wrapper.text()).toContain('+ 5 more');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should show correct remaining count for large cluster lists', () => {
|
|
90
|
+
const totalClusters = MAX_DISPLAY + 100;
|
|
91
|
+
const clusters = createClusters(totalClusters);
|
|
92
|
+
const wrapper = mountComponent(clusters);
|
|
93
|
+
|
|
94
|
+
expect(wrapper.text()).toContain('+ 100 more');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('computed properties', () => {
|
|
99
|
+
it('should return empty array for allClusters when row has no sortedReferencedClusters', () => {
|
|
100
|
+
const wrapper = mount(KubeconfigClusters, {
|
|
101
|
+
props: { row: { id: 'test-row' } },
|
|
102
|
+
global: {
|
|
103
|
+
mocks: defaultMocks,
|
|
104
|
+
stubs: { 'router-link': RouterLinkStub }
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(wrapper.vm.allClusters).toStrictEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should calculate remainingCount as 0 when clusters are at or below limit', () => {
|
|
112
|
+
const clusters = createClusters(MAX_DISPLAY);
|
|
113
|
+
const wrapper = mountComponent(clusters);
|
|
114
|
+
|
|
115
|
+
expect(wrapper.vm.remainingCount).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should calculate correct remainingCount when clusters exceed limit', () => {
|
|
119
|
+
const clusters = createClusters(MAX_DISPLAY + 15);
|
|
120
|
+
const wrapper = mountComponent(clusters);
|
|
121
|
+
|
|
122
|
+
expect(wrapper.vm.remainingCount).toBe(15);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -2,6 +2,7 @@ import { AGE, NAME as NAME_COL, STATE } from '@shell/config/table-headers';
|
|
|
2
2
|
import {
|
|
3
3
|
CAPI,
|
|
4
4
|
CATALOG,
|
|
5
|
+
EXT,
|
|
5
6
|
NORMAN,
|
|
6
7
|
HCI,
|
|
7
8
|
MANAGEMENT,
|
|
@@ -125,14 +126,17 @@ export function init(store) {
|
|
|
125
126
|
weightType(CAPI.MACHINE_DEPLOYMENT, 4, true);
|
|
126
127
|
weightType(CAPI.MACHINE_SET, 3, true);
|
|
127
128
|
weightType(CAPI.MACHINE, 2, true);
|
|
128
|
-
|
|
129
|
+
configureType(EXT.KUBECONFIG, { canYaml: false });
|
|
130
|
+
weightType(EXT.KUBECONFIG, 1, true);
|
|
131
|
+
weightType(CATALOG.CLUSTER_REPO, 0, true);
|
|
129
132
|
weightType(MANAGEMENT.PSA, 5, true);
|
|
130
|
-
weightType(VIRTUAL_TYPES.JWT_AUTHENTICATION,
|
|
133
|
+
weightType(VIRTUAL_TYPES.JWT_AUTHENTICATION, -1, true);
|
|
131
134
|
|
|
132
135
|
basicType([
|
|
133
136
|
CAPI.MACHINE_DEPLOYMENT,
|
|
134
137
|
CAPI.MACHINE_SET,
|
|
135
138
|
CAPI.MACHINE,
|
|
139
|
+
EXT.KUBECONFIG,
|
|
136
140
|
CATALOG.CLUSTER_REPO,
|
|
137
141
|
MANAGEMENT.PSA,
|
|
138
142
|
VIRTUAL_TYPES.JWT_AUTHENTICATION
|
|
@@ -192,4 +196,27 @@ export function init(store) {
|
|
|
192
196
|
MACHINE_SUMMARY,
|
|
193
197
|
AGE
|
|
194
198
|
]);
|
|
199
|
+
|
|
200
|
+
headers(EXT.KUBECONFIG, [
|
|
201
|
+
STATE,
|
|
202
|
+
{
|
|
203
|
+
name: 'clusters',
|
|
204
|
+
labelKey: 'tableHeaders.clusters',
|
|
205
|
+
value: 'spec.clusters',
|
|
206
|
+
sort: ['referencedClustersSortable'],
|
|
207
|
+
search: ['referencedClustersSortable'],
|
|
208
|
+
formatter: 'KubeconfigClusters',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: 'ttl',
|
|
212
|
+
labelKey: 'tableHeaders.ttl',
|
|
213
|
+
value: 'expiresAt',
|
|
214
|
+
formatter: 'LiveDate',
|
|
215
|
+
formatterOpts: { isCountdown: true },
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
...AGE,
|
|
219
|
+
defaultSort: true,
|
|
220
|
+
},
|
|
221
|
+
]);
|
|
195
222
|
}
|
package/config/router/routes.js
CHANGED
|
@@ -514,7 +514,10 @@ export default [
|
|
|
514
514
|
name: 'c-cluster-product-resource-id',
|
|
515
515
|
meta: { asyncSetup: true }
|
|
516
516
|
}, {
|
|
517
|
-
path
|
|
517
|
+
// Used this regex syntax in order to strict match the 'projectsecret' path segment
|
|
518
|
+
// while simultaneously capturing it as the 'resource' parameter.
|
|
519
|
+
// This is required because the Side Navigation relies on route.params.resource to determine which menu item to highlight.
|
|
520
|
+
path: `/c/:cluster/:product/:resource(${ VIRTUAL_TYPES.PROJECT_SECRETS })/:namespace/:id`,
|
|
518
521
|
component: () => interopDefault(import(`@shell/pages/c/_cluster/explorer/${ VIRTUAL_TYPES.PROJECT_SECRETS }.vue`)),
|
|
519
522
|
name: `c-cluster-product-${ VIRTUAL_TYPES.PROJECT_SECRETS }-namespace-id`,
|
|
520
523
|
}, {
|
|
@@ -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:
|
|
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
|
-
|
|
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
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { useStore } from 'vuex';
|
|
4
|
+
import PaginatedResourceTable from '@shell/components/PaginatedResourceTable.vue';
|
|
5
|
+
import { CAPI, MANAGEMENT } from '@shell/config/types';
|
|
6
|
+
import { ActionFindPageArgs } from '@shell/types/store/dashboard-store.types';
|
|
7
|
+
import { FilterArgs, PaginationFilterField, PaginationParamFilter } from '@shell/types/store/pagination.types';
|
|
8
|
+
import { PagTableFetchPageSecondaryResourcesOpts, PagTableFetchSecondaryResourcesOpts, PagTableFetchSecondaryResourcesReturns } from '@shell/types/components/paginatedResourceTable';
|
|
9
|
+
|
|
10
|
+
defineProps({
|
|
11
|
+
schema: {
|
|
12
|
+
type: Object,
|
|
13
|
+
required: true
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
useQueryParamsForSimpleFiltering: {
|
|
17
|
+
type: Boolean,
|
|
18
|
+
default: false
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const store = useStore();
|
|
23
|
+
|
|
24
|
+
const canViewProvClusters = computed<boolean>(() => {
|
|
25
|
+
return !!store.getters['management/canList'](CAPI.RANCHER_CLUSTER);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const canViewMgmtClusters = computed<boolean>(() => {
|
|
29
|
+
return !!store.getters['management/canList'](MANAGEMENT.CLUSTER);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fetch all clusters when not using pagination
|
|
34
|
+
*/
|
|
35
|
+
async function fetchSecondaryResources({ canPaginate }: PagTableFetchSecondaryResourcesOpts): PagTableFetchSecondaryResourcesReturns {
|
|
36
|
+
if (canPaginate) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const promises = [];
|
|
41
|
+
|
|
42
|
+
if (canViewProvClusters.value) {
|
|
43
|
+
promises.push(store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (canViewMgmtClusters.value) {
|
|
47
|
+
promises.push(store.dispatch('management/findAll', { type: MANAGEMENT.CLUSTER }));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await Promise.all(promises);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fetch only the clusters referenced by kubeconfigs on the current page
|
|
55
|
+
*
|
|
56
|
+
* NOTE: For the time being this isn't validated because ext.cattle.io.kubeconfig is not one of the indexed resources. I'm putting this in for future support since secondary resources are needed.
|
|
57
|
+
*/
|
|
58
|
+
async function fetchPageSecondaryResources({ force, page }: PagTableFetchPageSecondaryResourcesOpts) {
|
|
59
|
+
if (!page?.length) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const uniqueClusterIds = new Set<string>();
|
|
64
|
+
|
|
65
|
+
page.forEach((kubeconfig: any) => {
|
|
66
|
+
const ids = kubeconfig.spec?.clusters || [];
|
|
67
|
+
|
|
68
|
+
ids.forEach((id: string) => uniqueClusterIds.add(id));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (uniqueClusterIds.size === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const clusterIdArray = Array.from(uniqueClusterIds);
|
|
76
|
+
|
|
77
|
+
if (canViewProvClusters.value) {
|
|
78
|
+
const opt: ActionFindPageArgs = {
|
|
79
|
+
force,
|
|
80
|
+
pagination: new FilterArgs({
|
|
81
|
+
filters: PaginationParamFilter.createMultipleFields(
|
|
82
|
+
clusterIdArray.map((id) => new PaginationFilterField({
|
|
83
|
+
field: 'status.clusterName', // Verified it's one of the attribute fields listed in the schema, according to steve-pagination-utils that means it should be filterable
|
|
84
|
+
value: id
|
|
85
|
+
}))
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
store.dispatch('management/findPage', { type: CAPI.RANCHER_CLUSTER, opt });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (canViewMgmtClusters.value) {
|
|
94
|
+
const opt: ActionFindPageArgs = {
|
|
95
|
+
force,
|
|
96
|
+
pagination: new FilterArgs({
|
|
97
|
+
filters: PaginationParamFilter.createMultipleFields(
|
|
98
|
+
clusterIdArray.map((id) => new PaginationFilterField({
|
|
99
|
+
field: 'metadata.name', // Verified it's one of the attribute fields listed in the schema, according to steve-pagination-utils that means it should be filterable
|
|
100
|
+
value: id
|
|
101
|
+
}))
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
store.dispatch('management/findPage', { type: MANAGEMENT.CLUSTER, opt });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<template>
|
|
112
|
+
<PaginatedResourceTable
|
|
113
|
+
:schema="schema"
|
|
114
|
+
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
|
|
115
|
+
:fetch-secondary-resources="fetchSecondaryResources"
|
|
116
|
+
:fetch-page-secondary-resources="fetchPageSecondaryResources"
|
|
117
|
+
/>
|
|
118
|
+
</template>
|