@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,12 +1,17 @@
1
1
  <script>
2
2
  import Select from '@shell/components/form/Select';
3
3
  import UnitInput from '@shell/components/form/UnitInput';
4
- import { ROW_COMPUTED } from './shared';
4
+ import { LabeledInput } from '@components/Form/LabeledInput';
5
+ import { ROW_COMPUTED, TYPES } from './shared';
5
6
 
6
7
  export default {
7
8
  emits: ['type-change'],
8
9
 
9
- components: { Select, UnitInput },
10
+ components: {
11
+ Select,
12
+ UnitInput,
13
+ LabeledInput,
14
+ },
10
15
 
11
16
  props: {
12
17
  mode: {
@@ -26,35 +31,95 @@ export default {
26
31
  default: () => {
27
32
  return {};
28
33
  }
34
+ },
35
+ index: {
36
+ type: Number,
37
+ required: true,
38
+ }
39
+ },
40
+
41
+ data() {
42
+ return { customType: '' };
43
+ },
44
+
45
+ created() {
46
+ if (this.type.startsWith(TYPES.EXTENDED)) {
47
+ this.customType = this.type.substring(`${ TYPES.EXTENDED }.`.length);
48
+ } else {
49
+ this.customType = this.type;
29
50
  }
30
51
  },
31
52
 
32
53
  computed: {
33
54
  ...ROW_COMPUTED,
34
55
 
35
- resourceQuotaLimit: {
36
- get() {
37
- return this.value.spec.resourceQuota?.limit || {};
38
- },
56
+ localType() {
57
+ return this.type.startsWith(TYPES.EXTENDED) ? this.type.split('.')[0] : this.type;
39
58
  },
40
59
 
41
- namespaceDefaultResourceQuotaLimit: {
42
- get() {
43
- return this.value.spec.namespaceDefaultResourceQuota?.limit || {};
44
- },
60
+ isCustom() {
61
+ return this.localType === TYPES.EXTENDED;
62
+ },
63
+
64
+ resourceQuotaLimit() {
65
+ if (this.isCustom) {
66
+ return this.value.spec.resourceQuota?.limit.extended || {};
67
+ }
68
+
69
+ return this.value.spec.resourceQuota?.limit || {};
70
+ },
71
+
72
+ namespaceDefaultResourceQuotaLimit() {
73
+ if (this.isCustom) {
74
+ return this.value.spec.namespaceDefaultResourceQuota?.limit.extended || {};
75
+ }
76
+
77
+ return this.value.spec.namespaceDefaultResourceQuota?.limit || {};
78
+ },
79
+
80
+ currentResourceType() {
81
+ return this.isCustom ? this.customType : this.localType;
82
+ },
83
+
84
+ customTypeRules() {
85
+ // Return a validation rule that makes the field required when isCustom is true
86
+ if (this.isCustom) {
87
+ return [
88
+ (value) => {
89
+ if (!value) {
90
+ return this.t('resourceQuota.errors.customTypeRequired');
91
+ }
92
+
93
+ return undefined;
94
+ }
95
+ ];
96
+ }
97
+
98
+ return [];
45
99
  }
46
100
  },
47
101
 
48
102
  methods: {
49
103
  updateType(type) {
50
- if (typeof this.value.spec.resourceQuota?.limit[this.type] !== 'undefined') {
51
- delete this.value.spec.resourceQuota.limit[this.type];
52
- }
53
- if (typeof this.value.spec.namespaceDefaultResourceQuota?.limit[this.type] !== 'undefined') {
54
- delete this.value.spec.namespaceDefaultResourceQuota.limit[this.type];
104
+ const oldResourceKey = this.isCustom ? this.customType : this.localType;
105
+
106
+ this.deleteResourceLimits(oldResourceKey);
107
+
108
+ if (type === TYPES.EXTENDED) {
109
+ this.customType = '';
110
+ } else {
111
+ this.customType = type;
55
112
  }
56
113
 
57
- this.$emit('type-change', type);
114
+ this.$emit('type-change', { index: this.index, type });
115
+ },
116
+
117
+ updateCustomType(type) {
118
+ const oldType = this.customType;
119
+
120
+ this.deleteResourceLimits(oldType);
121
+
122
+ this.customType = type;
58
123
  },
59
124
 
60
125
  updateQuotaLimit(prop, type, val) {
@@ -62,7 +127,26 @@ export default {
62
127
  this.value.spec[prop] = { limit: { } };
63
128
  }
64
129
 
130
+ if (this.isCustom) {
131
+ if (!this.value.spec[prop].limit.extended) {
132
+ this.value.spec[prop].limit.extended = { };
133
+ }
134
+
135
+ this.value.spec[prop].limit.extended[type] = val;
136
+
137
+ return;
138
+ }
139
+
65
140
  this.value.spec[prop].limit[type] = val;
141
+ },
142
+
143
+ deleteResourceLimits(resourceKey) {
144
+ if (typeof this.value.spec.resourceQuota?.limit[resourceKey] !== 'undefined') {
145
+ delete this.value.spec.resourceQuota.limit[resourceKey];
146
+ }
147
+ if (typeof this.value.spec.namespaceDefaultResourceQuota?.limit[resourceKey] !== 'undefined') {
148
+ delete this.value.spec.namespaceDefaultResourceQuota.limit[resourceKey];
149
+ }
66
150
  }
67
151
  },
68
152
  };
@@ -73,15 +157,26 @@ export default {
73
157
  class="row"
74
158
  >
75
159
  <Select
76
- :value="type"
160
+ :value="localType"
77
161
  class="mr-10"
78
162
  :mode="mode"
79
163
  :options="types"
80
164
  data-testid="projectrow-type-input"
81
165
  @update:value="updateType($event)"
82
166
  />
167
+ <LabeledInput
168
+ :value="customType"
169
+ :disabled="!isCustom"
170
+ :required="isCustom"
171
+ :mode="mode"
172
+ :placeholder="t('resourceQuota.resourceIdentifier.placeholder')"
173
+ :rules="customTypeRules"
174
+ class="mr-10"
175
+ data-testid="projectrow-custom-type-input"
176
+ @update:value="updateCustomType($event)"
177
+ />
83
178
  <UnitInput
84
- :value="resourceQuotaLimit[type]"
179
+ :value="resourceQuotaLimit[currentResourceType]"
85
180
  class="mr-10"
86
181
  :mode="mode"
87
182
  :placeholder="typeOption.placeholder"
@@ -90,10 +185,10 @@ export default {
90
185
  :base-unit="typeOption.baseUnit"
91
186
  :output-modifier="true"
92
187
  data-testid="projectrow-project-quota-input"
93
- @update:value="updateQuotaLimit('resourceQuota', type, $event)"
188
+ @update:value="updateQuotaLimit('resourceQuota', currentResourceType, $event)"
94
189
  />
95
190
  <UnitInput
96
- :value="namespaceDefaultResourceQuotaLimit[type]"
191
+ :value="namespaceDefaultResourceQuotaLimit[currentResourceType]"
97
192
  :mode="mode"
98
193
  :placeholder="typeOption.placeholder"
99
194
  :increment="typeOption.increment"
@@ -101,7 +196,7 @@ export default {
101
196
  :base-unit="typeOption.baseUnit"
102
197
  :output-modifier="true"
103
198
  data-testid="projectrow-namespace-quota-input"
104
- @update:value="updateQuotaLimit('namespaceDefaultResourceQuota', type, $event)"
199
+ @update:value="updateQuotaLimit('namespaceDefaultResourceQuota', currentResourceType, $event)"
105
200
  />
106
201
  </div>
107
202
  </template>
@@ -1,62 +1,86 @@
1
+ export const TYPES = {
2
+ EXTENDED: 'extended',
3
+ CONFIG_MAPS: 'configMaps',
4
+ LIMITS_CPU: 'limitsCpu',
5
+ LIMITS_MEM: 'limitsMemory',
6
+ PVC: 'persistentVolumeClaims',
7
+ PODS: 'pods',
8
+ REPLICATION_CONTROLLERS: 'replicationControllers',
9
+ REQUESTS_CPU: 'requestsCpu',
10
+ REQUESTS_MEMORY: 'requestsMemory',
11
+ REQUESTS_STORAGE: 'requestsStorage',
12
+ SECRETS: 'secrets',
13
+ SERVICES: 'services',
14
+ SERVICES_LOAD_BALANCERS: 'servicesLoadBalancers',
15
+ SERVICES_NODE_PORTS: 'servicesNodePorts',
16
+ };
17
+
1
18
  export const RANCHER_TYPES = [
2
19
  {
3
- value: 'configMaps',
20
+ value: TYPES.EXTENDED,
21
+ inputExponent: 0,
22
+ baseUnit: '',
23
+ labelKey: 'resourceQuota.custom',
24
+ placeholderKey: 'resourceQuota.projectLimit.unitlessPlaceholder'
25
+ },
26
+ {
27
+ value: TYPES.CONFIG_MAPS,
4
28
  inputExponent: 0,
5
29
  baseUnit: '',
6
30
  labelKey: 'resourceQuota.configMaps',
7
31
  placeholderKey: 'resourceQuota.projectLimit.unitlessPlaceholder'
8
32
  },
9
33
  {
10
- value: 'limitsCpu',
34
+ value: TYPES.LIMITS_CPU,
11
35
  inputExponent: -1,
12
36
  baseUnitKey: 'suffix.cpus',
13
37
  labelKey: 'resourceQuota.limitsCpu',
14
38
  placeholderKey: 'resourceQuota.projectLimit.cpuPlaceholder'
15
39
  },
16
40
  {
17
- value: 'limitsMemory',
41
+ value: TYPES.LIMITS_MEM,
18
42
  inputExponent: 2,
19
43
  increment: 1024,
20
44
  labelKey: 'resourceQuota.limitsMemory',
21
45
  placeholderKey: 'resourceQuota.projectLimit.memoryPlaceholder'
22
46
  },
23
47
  {
24
- value: 'persistentVolumeClaims',
48
+ value: TYPES.PVC,
25
49
  inputExponent: 0,
26
50
  baseUnit: '',
27
51
  labelKey: 'resourceQuota.persistentVolumeClaims',
28
52
  placeholderKey: 'resourceQuota.projectLimit.unitlessPlaceholder'
29
53
  },
30
54
  {
31
- value: 'pods',
55
+ value: TYPES.PODS,
32
56
  inputExponent: 0,
33
57
  baseUnit: '',
34
58
  labelKey: 'resourceQuota.pods',
35
59
  placeholderKey: 'resourceQuota.projectLimit.unitlessPlaceholder'
36
60
  },
37
61
  {
38
- value: 'replicationControllers',
62
+ value: TYPES.REPLICATION_CONTROLLERS,
39
63
  inputExponent: 0,
40
64
  baseUnit: '',
41
65
  labelKey: 'resourceQuota.replicationControllers',
42
66
  placeholderKey: 'resourceQuota.projectLimit.unitlessPlaceholder'
43
67
  },
44
68
  {
45
- value: 'requestsCpu',
69
+ value: TYPES.REQUESTS_CPU,
46
70
  inputExponent: -1,
47
71
  baseUnitKey: 'suffix.cpus',
48
72
  labelKey: 'resourceQuota.requestsCpu',
49
73
  placeholderKey: 'resourceQuota.projectLimit.cpuPlaceholder'
50
74
  },
51
75
  {
52
- value: 'requestsMemory',
76
+ value: TYPES.REQUESTS_MEMORY,
53
77
  inputExponent: 2,
54
78
  increment: 1024,
55
79
  labelKey: 'resourceQuota.requestsMemory',
56
80
  placeholderKey: 'resourceQuota.projectLimit.memoryPlaceholder'
57
81
  },
58
82
  {
59
- value: 'requestsStorage',
83
+ value: TYPES.REQUESTS_STORAGE,
60
84
  units: 'storage',
61
85
  inputExponent: 2,
62
86
  increment: 1024,
@@ -64,7 +88,7 @@ export const RANCHER_TYPES = [
64
88
  placeholderKey: 'resourceQuota.projectLimit.storagePlaceholder'
65
89
  },
66
90
  {
67
- value: 'secrets',
91
+ value: TYPES.SECRETS,
68
92
  units: 'unitless',
69
93
  inputExponent: 0,
70
94
  baseUnit: '',
@@ -72,7 +96,7 @@ export const RANCHER_TYPES = [
72
96
  placeholderKey: 'resourceQuota.projectLimit.unitlessPlaceholder'
73
97
  },
74
98
  {
75
- value: 'services',
99
+ value: TYPES.SERVICES,
76
100
  units: 'unitless',
77
101
  inputExponent: 0,
78
102
  baseUnit: '',
@@ -80,7 +104,7 @@ export const RANCHER_TYPES = [
80
104
  placeholderKey: 'resourceQuota.projectLimit.unitlessPlaceholder'
81
105
  },
82
106
  {
83
- value: 'servicesLoadBalancers',
107
+ value: TYPES.SERVICES_LOAD_BALANCERS,
84
108
  units: 'unitless',
85
109
  inputExponent: 0,
86
110
  baseUnit: '',
@@ -88,7 +112,7 @@ export const RANCHER_TYPES = [
88
112
  placeholderKey: 'resourceQuota.projectLimit.unitlessPlaceholder'
89
113
  },
90
114
  {
91
- value: 'servicesNodePorts',
115
+ value: TYPES.SERVICES_NODE_PORTS,
92
116
  units: 'unitless',
93
117
  inputExponent: 0,
94
118
  baseUnit: '',
@@ -99,28 +123,28 @@ export const RANCHER_TYPES = [
99
123
 
100
124
  export const HARVESTER_TYPES = [
101
125
  {
102
- value: 'limitsCpu',
126
+ value: TYPES.LIMITS_CPU,
103
127
  inputExponent: -1,
104
128
  baseUnitKey: 'suffix.cpus',
105
129
  labelKey: 'resourceQuota.limitsCpu',
106
130
  placeholderKey: 'resourceQuota.projectLimit.cpuPlaceholder'
107
131
  },
108
132
  {
109
- value: 'limitsMemory',
133
+ value: TYPES.LIMITS_MEM,
110
134
  inputExponent: 2,
111
135
  increment: 1024,
112
136
  labelKey: 'resourceQuota.limitsMemory',
113
137
  placeholderKey: 'resourceQuota.projectLimit.memoryPlaceholder'
114
138
  },
115
139
  {
116
- value: 'requestsCpu',
140
+ value: TYPES.REQUESTS_CPU,
117
141
  inputExponent: -1,
118
142
  baseUnitKey: 'suffix.cpus',
119
143
  labelKey: 'resourceQuota.requestsCpu',
120
144
  placeholderKey: 'resourceQuota.projectLimit.cpuPlaceholder'
121
145
  },
122
146
  {
123
- value: 'requestsMemory',
147
+ value: TYPES.REQUESTS_MEMORY,
124
148
  inputExponent: 2,
125
149
  increment: 1024,
126
150
  labelKey: 'resourceQuota.requestsMemory',
@@ -130,7 +154,7 @@ export const HARVESTER_TYPES = [
130
154
 
131
155
  export const ROW_COMPUTED = {
132
156
  typeOption() {
133
- return this.types.find((type) => type.value === this.type);
157
+ return this.types.find((type) => type.value === this.type.split('.')[0]);
134
158
  }
135
159
  };
136
160
 
@@ -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>
@@ -26,7 +26,7 @@ export default {
26
26
 
27
27
  product: {
28
28
  type: String,
29
- default: EXPLORER,
29
+ default: '',
30
30
  }
31
31
  },
32
32
 
@@ -35,10 +35,11 @@ export default {
35
35
  const name = `c-cluster-product-resource${ this.namespace ? '-namespace' : '' }-id`;
36
36
 
37
37
  const params = {
38
+ cluster: this.$store.getters['clusterId'],
39
+ product: this.product || this.$store.getters['productId'] || EXPLORER,
38
40
  resource: this.type,
39
41
  namespace: this.namespace,
40
42
  id: this.objectId ? this.objectId : this.value,
41
- product: this.product || EXPLORER,
42
43
  };
43
44
 
44
45
  // Having an undefined param can yield a console warning like [Vue Router warn]: Discarded invalid param(s) "namespace" when navigating
@@ -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
+ });
@@ -404,7 +404,7 @@ export function init(store) {
404
404
  [STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(6), STEVE_WORKLOAD_ENDPOINTS, 'Completions', {
405
405
  ...DURATION,
406
406
  value: 'metadata.fields.3',
407
- sort: false,
407
+ sort: 'metadata.fields.3',
408
408
  search: 'metadata.fields.3',
409
409
  formatter: undefined, // Now that sort/search is remote we're not doing weird things with start time (see `duration` in model)
410
410
  }, STEVE_AGE_COL],
@@ -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
  }, {