@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.
- package/assets/translations/en-us.yaml +24 -2
- package/assets/translations/zh-hans.yaml +13 -0
- package/components/ActionMenu.vue +7 -8
- package/components/ActionMenuShell.vue +19 -20
- package/components/Resource/Detail/Card/Scaler.vue +10 -2
- package/components/Resource/Detail/Card/StatusCard/index.vue +4 -1
- package/components/ResourceTable.vue +1 -1
- package/components/Tabbed/Tab.vue +4 -0
- package/components/Tabbed/index.vue +11 -3
- package/components/__tests__/ProjectRow.test.ts +102 -15
- package/components/form/ResourceQuota/Project.vue +59 -8
- package/components/form/ResourceQuota/ProjectRow.vue +116 -21
- package/components/form/ResourceQuota/shared.js +42 -18
- package/components/formatter/KubeconfigClusters.vue +74 -0
- package/components/formatter/LinkName.vue +3 -2
- package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
- package/config/product/explorer.js +1 -1
- package/config/product/manager.js +29 -2
- package/config/router/routes.js +4 -1
- package/config/table-headers.js +9 -7
- package/config/types.js +4 -1
- package/detail/management.cattle.io.oidcclient.vue +15 -4
- package/edit/__tests__/management.cattle.io.project.test.js +137 -0
- package/edit/management.cattle.io.project.vue +36 -6
- package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +16 -3
- 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/initialize/install-plugins.js +0 -2
- 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/management.cattle.io.cluster.js +22 -30
- package/models/provisioning.cattle.io.cluster.js +2 -2
- package/models/secret.js +1 -1
- package/package.json +2 -2
- package/pages/__tests__/diagnostic.test.ts +71 -0
- 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/pages/c/_cluster/explorer/tools/index.vue +23 -5
- package/pages/c/_cluster/monitoring/alertmanagerconfig/_alertmanagerconfigid/receiver.vue +18 -5
- package/pages/c/_cluster/uiplugins/index.vue +40 -8
- package/pages/diagnostic.vue +17 -3
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +1 -0
- package/rancher-components/RcItemCard/RcItemCard.test.ts +20 -8
- package/rancher-components/RcItemCard/RcItemCard.vue +38 -31
- package/store/__tests__/auth.test.ts +21 -5
- package/store/auth.js +6 -3
- package/types/shell/index.d.ts +177 -157
- 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
|
@@ -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 {
|
|
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: {
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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="
|
|
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[
|
|
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',
|
|
188
|
+
@update:value="updateQuotaLimit('resourceQuota', currentResourceType, $event)"
|
|
94
189
|
/>
|
|
95
190
|
<UnitInput
|
|
96
|
-
:value="namespaceDefaultResourceQuotaLimit[
|
|
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',
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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">, </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>
|
|
@@ -26,7 +26,7 @@ export default {
|
|
|
26
26
|
|
|
27
27
|
product: {
|
|
28
28
|
type: String,
|
|
29
|
-
default:
|
|
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:
|
|
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
|
-
|
|
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
|
}, {
|