@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.
@@ -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: Snapshot retention count
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">,&nbsp;</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
+ &mdash;
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
- weightType(CATALOG.CLUSTER_REPO, 1, true);
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, 0, true);
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
  }
@@ -514,7 +514,10 @@ export default [
514
514
  name: 'c-cluster-product-resource-id',
515
515
  meta: { asyncSetup: true }
516
516
  }, {
517
- path: `/c/:cluster/:product/${ VIRTUAL_TYPES.PROJECT_SECRETS }/:namespace/:id`,
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: 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"
@@ -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>