@rancher/shell 3.0.9-rc.3 → 3.0.9-rc.5
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/brand/suse/metadata.json +2 -1
- package/assets/translations/en-us.yaml +105 -5
- package/components/ActionMenuShell.vue +1 -1
- package/components/Inactivity.vue +2 -2
- package/components/Resource/Detail/Card/ExtrasCard.vue +49 -15
- package/components/Resource/Detail/Card/__tests__/ExtrasCard.test.ts +111 -0
- package/components/Resource/Detail/Masthead/__tests__/index.test.ts +0 -17
- package/components/Resource/Detail/Masthead/index.vue +11 -4
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +3 -1
- package/components/Resource/Detail/Metadata/index.vue +1 -1
- package/components/Resource/Detail/ResourceRow.vue +1 -1
- package/components/ResourceDetail/Masthead/latest.vue +12 -2
- package/components/ResourceList/index.vue +9 -0
- package/components/ResourceTable.vue +38 -4
- package/components/Tabbed/Tab.vue +4 -0
- package/components/Tabbed/index.vue +4 -1
- package/components/__tests__/ProjectRow.test.ts +60 -0
- package/components/form/ChangePassword.vue +41 -35
- package/components/form/ResourceQuota/Project.vue +42 -1
- package/components/form/ResourceQuota/ProjectRow.vue +71 -4
- package/components/form/ResourceQuota/__tests__/Project.test.ts +63 -0
- package/components/form/SelectOrCreateAuthSecret.vue +6 -1
- package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +35 -0
- package/components/formatter/KubeconfigClusters.vue +74 -0
- package/components/formatter/MachineSummaryGraph.vue +10 -2
- package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
- package/components/nav/TopLevelMenu.helper.ts +50 -2
- package/components/nav/TopLevelMenu.vue +14 -0
- package/components/nav/Type.vue +5 -0
- package/components/nav/__tests__/TopLevelMenu.test.ts +3 -3
- package/components/nav/__tests__/Type.test.ts +6 -4
- package/config/product/explorer.js +4 -3
- package/config/product/manager.js +47 -3
- package/config/router/navigation-guards/authentication.js +8 -9
- package/config/router/routes.js +4 -1
- package/config/types.js +10 -2
- package/detail/auditlog.cattle.io.auditpolicy.vue +19 -0
- package/detail/management.cattle.io.user.vue +1 -2
- package/detail/node.vue +0 -1
- package/detail/provisioning.cattle.io.cluster.vue +2 -1
- package/dialog/ChangePasswordDialog.vue +8 -0
- package/dialog/GenericPrompt.vue +20 -3
- package/dialog/ScaleMachineDownDialog.vue +65 -15
- package/dialog/SearchDialog.vue +10 -2
- package/dialog/__tests__/ScaleMachineDownDialog.test.ts +184 -0
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +89 -0
- package/edit/__tests__/management.cattle.io.project.test.js +56 -1
- package/edit/auditlog.cattle.io.auditpolicy/AdditionalRedactions.vue +114 -0
- package/edit/auditlog.cattle.io.auditpolicy/Filters.vue +119 -0
- package/edit/auditlog.cattle.io.auditpolicy/General.vue +180 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/AdditionalRedactions.test.ts +327 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/Filters.test.ts +449 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/General.test.ts +472 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/AdditionalRedactions.test.ts.snap +27 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/Filters.test.ts.snap +39 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +174 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +29 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/index.test.ts +215 -0
- package/edit/auditlog.cattle.io.auditpolicy/index.vue +104 -0
- package/edit/auditlog.cattle.io.auditpolicy/types.ts +28 -0
- package/edit/fleet.cattle.io.gitrepo.vue +16 -1
- package/edit/management.cattle.io.project.vue +8 -2
- package/edit/management.cattle.io.user.vue +29 -34
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +178 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +22 -2
- package/edit/provisioning.cattle.io.cluster/shared.ts +4 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
- 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/auditlog.cattle.io.auditpolicy.vue +63 -0
- package/list/ext.cattle.io.kubeconfig.vue +118 -0
- package/list/group.principal.vue +11 -15
- package/list/management.cattle.io.user.vue +11 -21
- package/machine-config/azure.vue +14 -0
- package/mixins/__tests__/chart.test.ts +147 -0
- package/mixins/browser-tab-visibility.js +5 -4
- package/mixins/chart.js +10 -8
- package/mixins/fetch.client.js +6 -0
- package/models/__tests__/auditlog.cattle.io.auditpolicy.test.ts +117 -0
- package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
- package/models/__tests__/secret.test.ts +55 -0
- package/models/__tests__/workload.test.ts +49 -6
- package/models/auditlog.cattle.io.auditpolicy.js +46 -0
- package/models/cluster.x-k8s.io.machine.js +1 -1
- package/models/cluster.x-k8s.io.machinedeployment.js +5 -5
- package/models/event.js +5 -0
- package/models/ext.cattle.io.groupmembershiprefreshrequest.js +15 -0
- package/models/ext.cattle.io.kubeconfig.ts +97 -0
- package/models/ext.cattle.io.passwordchangerequest.js +15 -0
- package/models/ext.cattle.io.selfuser.js +15 -0
- package/models/fleet-application.js +17 -7
- package/models/management.cattle.io.user.js +28 -31
- package/models/schema.js +18 -0
- package/models/secret.js +28 -25
- package/models/steve-schema.ts +39 -2
- package/models/workload.js +3 -2
- package/package.json +2 -2
- package/pages/about.vue +3 -2
- package/pages/account/index.vue +23 -16
- package/pages/auth/login.vue +15 -8
- package/pages/auth/setup.vue +52 -15
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
- package/pages/c/_cluster/apps/charts/index.vue +1 -0
- package/pages/home.vue +9 -3
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -3
- package/plugins/dashboard-store/actions.js +7 -0
- package/plugins/dashboard-store/getters.js +23 -1
- package/plugins/dashboard-store/index.js +3 -2
- package/plugins/dashboard-store/mutations.js +4 -0
- package/plugins/dashboard-store/resource-class.js +12 -5
- package/plugins/steve/__tests__/steve-class.test.ts +167 -0
- package/plugins/steve/schema.d.ts +5 -0
- package/plugins/steve/steve-class.js +19 -0
- package/plugins/steve/steve-pagination-utils.ts +2 -1
- package/rancher-components/RcItemCard/RcItemCard.test.ts +4 -2
- package/rancher-components/RcItemCard/RcItemCard.vue +27 -10
- package/store/auth.js +57 -19
- package/store/notifications.ts +1 -1
- package/store/type-map.js +12 -1
- package/types/shell/index.d.ts +24 -15
- package/types/store/dashboard-store.types.ts +7 -0
- package/utils/__tests__/chart.test.ts +96 -0
- package/utils/__tests__/version.test.ts +1 -19
- package/utils/chart.js +64 -0
- package/utils/pagination-wrapper.ts +11 -3
- package/utils/version.js +5 -17
- package/vue.config.js +26 -13
|
@@ -359,7 +359,7 @@ export default {
|
|
|
359
359
|
>{{ tab.badge }}</span>
|
|
360
360
|
<i
|
|
361
361
|
v-if="hasErrorIcon(tab)"
|
|
362
|
-
v-clean-tooltip="t('validation.tab')"
|
|
362
|
+
v-clean-tooltip="tab.errorIconTooltip || t('validation.tab')"
|
|
363
363
|
class="conditions-alert-icon icon-error"
|
|
364
364
|
/>
|
|
365
365
|
</a>
|
|
@@ -417,11 +417,13 @@ export default {
|
|
|
417
417
|
:name="tab.name"
|
|
418
418
|
:label="tab.label"
|
|
419
419
|
:label-key="tab.labelKey"
|
|
420
|
+
:label-icon="tab.labelIcon"
|
|
420
421
|
:weight="tab.weight"
|
|
421
422
|
:tooltip="tab.tooltip"
|
|
422
423
|
:show-header="tab.showHeader"
|
|
423
424
|
:display-alert-icon="tab.displayAlertIcon"
|
|
424
425
|
:error="tab.error"
|
|
426
|
+
:error-icon-tooltip="tab.errorIconTooltip"
|
|
425
427
|
:badge="tab.badge"
|
|
426
428
|
>
|
|
427
429
|
<component
|
|
@@ -504,6 +506,7 @@ export default {
|
|
|
504
506
|
.conditions-alert-icon {
|
|
505
507
|
color: var(--error);
|
|
506
508
|
padding-left: 4px;
|
|
509
|
+
margin-left: auto;
|
|
507
510
|
}
|
|
508
511
|
|
|
509
512
|
&:last-child {
|
|
@@ -143,4 +143,64 @@ describe('component: ProjectRow.vue', () => {
|
|
|
143
143
|
expect(wrapper.vm.isCustom).toBe(true);
|
|
144
144
|
expect(wrapper.vm.customType).toBe('requests.nvidia.com/gpu');
|
|
145
145
|
});
|
|
146
|
+
|
|
147
|
+
it('should emit update:resource-identifier when updateCustomType is called', () => {
|
|
148
|
+
const wrapper: any = shallowMount(ProjectRow, {
|
|
149
|
+
props: {
|
|
150
|
+
...defaultMountOptions.props,
|
|
151
|
+
type: TYPES.EXTENDED,
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
wrapper.vm.updateCustomType('my-custom-resource');
|
|
156
|
+
|
|
157
|
+
expect(wrapper.emitted('update:resource-identifier')).toBeTruthy();
|
|
158
|
+
expect(wrapper.emitted('update:resource-identifier')[0]).toStrictEqual([{
|
|
159
|
+
type: TYPES.EXTENDED,
|
|
160
|
+
customType: 'my-custom-resource',
|
|
161
|
+
index: 0
|
|
162
|
+
}]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should not delete resource limits if there are duplicate keys in localTypeValues', () => {
|
|
166
|
+
const value = {
|
|
167
|
+
spec: {
|
|
168
|
+
resourceQuota: { limit: { extended: { 'my-resource': '10' } } },
|
|
169
|
+
namespaceDefaultResourceQuota: { limit: { extended: { 'my-resource': '5' } } }
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const wrapper: any = shallowMount(ProjectRow, {
|
|
173
|
+
props: {
|
|
174
|
+
...defaultMountOptions.props,
|
|
175
|
+
value,
|
|
176
|
+
typeValues: ['extended.my-resource', 'extended.my-resource']
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
wrapper.vm.deleteResourceLimits('my-resource', true);
|
|
181
|
+
|
|
182
|
+
expect(value.spec.resourceQuota.limit.extended['my-resource']).toStrictEqual('10');
|
|
183
|
+
expect(value.spec.namespaceDefaultResourceQuota.limit.extended['my-resource']).toStrictEqual('5');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should delete resource limits if there is only one key in localTypeValues', () => {
|
|
187
|
+
const value = {
|
|
188
|
+
spec: {
|
|
189
|
+
resourceQuota: { limit: { extended: { 'my-resource': '10' } } },
|
|
190
|
+
namespaceDefaultResourceQuota: { limit: { extended: { 'my-resource': '5' } } }
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const wrapper: any = shallowMount(ProjectRow, {
|
|
194
|
+
props: {
|
|
195
|
+
...defaultMountOptions.props,
|
|
196
|
+
value,
|
|
197
|
+
typeValues: ['extended.my-resource']
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
wrapper.vm.deleteResourceLimits('my-resource', true);
|
|
202
|
+
|
|
203
|
+
expect(value.spec.resourceQuota.limit.extended['my-resource']).toBeUndefined();
|
|
204
|
+
expect(value.spec.namespaceDefaultResourceQuota.limit.extended['my-resource']).toBeUndefined();
|
|
205
|
+
});
|
|
146
206
|
});
|
|
@@ -3,7 +3,7 @@ import { mapGetters } from 'vuex';
|
|
|
3
3
|
import { Banner } from '@components/Banner';
|
|
4
4
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
5
5
|
import Password from '@shell/components/form/Password';
|
|
6
|
-
import { NORMAN } from '@shell/config/types';
|
|
6
|
+
import { NORMAN, EXT } from '@shell/config/types';
|
|
7
7
|
import { _CREATE, _EDIT } from '@shell/config/query-params';
|
|
8
8
|
|
|
9
9
|
// Component handles three use cases
|
|
@@ -21,27 +21,22 @@ export default {
|
|
|
21
21
|
type: String,
|
|
22
22
|
default: null
|
|
23
23
|
},
|
|
24
|
+
user: {
|
|
25
|
+
type: Object,
|
|
26
|
+
default: null,
|
|
27
|
+
required: true
|
|
28
|
+
},
|
|
24
29
|
mustChangePassword: {
|
|
25
30
|
type: Boolean,
|
|
26
31
|
default: false
|
|
27
32
|
}
|
|
28
33
|
},
|
|
29
34
|
async fetch() {
|
|
30
|
-
|
|
31
|
-
// Fetch the username for hidden input fields. The value itself is not needed if create or changing another user's password
|
|
32
|
-
const users = await this.$store.dispatch('rancher/findAll', {
|
|
33
|
-
type: NORMAN.USER,
|
|
34
|
-
opt: { url: '/v3/users', filter: { me: true } }
|
|
35
|
-
});
|
|
36
|
-
const user = users?.[0];
|
|
37
|
-
|
|
38
|
-
this.username = user?.username;
|
|
39
|
-
}
|
|
40
|
-
this.userChangeOnLogin = this.mustChangePassword;
|
|
35
|
+
this.passwordChangeRequest = await this.$store.dispatch('management/create', { type: EXT.PASSWORD_CHANGE_REQUESTS });
|
|
41
36
|
},
|
|
42
|
-
data(
|
|
37
|
+
data() {
|
|
43
38
|
return {
|
|
44
|
-
|
|
39
|
+
passwordChangeRequest: undefined,
|
|
45
40
|
errorMessages: [],
|
|
46
41
|
pCanShowMismatchedPassword: false,
|
|
47
42
|
pIsRandomGenerated: false,
|
|
@@ -58,6 +53,18 @@ export default {
|
|
|
58
53
|
computed: {
|
|
59
54
|
...mapGetters({ t: 'i18n/t' }),
|
|
60
55
|
|
|
56
|
+
userId() {
|
|
57
|
+
return this.user?.id;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
username() {
|
|
61
|
+
return this.user?.username;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
canChangePassword() {
|
|
65
|
+
return !!this.passwordChangeRequest?.canChangePassword;
|
|
66
|
+
},
|
|
67
|
+
|
|
61
68
|
isRandomGenerated: {
|
|
62
69
|
get() {
|
|
63
70
|
return this.pIsRandomGenerated;
|
|
@@ -227,37 +234,36 @@ export default {
|
|
|
227
234
|
});
|
|
228
235
|
},
|
|
229
236
|
|
|
230
|
-
async save(
|
|
237
|
+
async save() {
|
|
231
238
|
if (this.isChange) {
|
|
232
|
-
await this.changePassword();
|
|
239
|
+
await this.changePassword('change');
|
|
233
240
|
if (this.form.deleteKeys) {
|
|
234
241
|
await this.deleteKeys();
|
|
235
242
|
}
|
|
236
243
|
} else if (this.isEdit) {
|
|
237
|
-
return this.
|
|
244
|
+
return this.changePassword('set');
|
|
238
245
|
}
|
|
239
246
|
},
|
|
240
247
|
|
|
241
|
-
async
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
async changePassword(mode) {
|
|
249
|
+
if (!this.canChangePassword) {
|
|
250
|
+
this.errorMessages = [this.t('changePassword.errors.cannotChange')];
|
|
251
|
+
throw new Error(this.t('changePassword.errors.cannotChange'));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const spec = {
|
|
255
|
+
newPassword: this.isRandomGenerated ? this.form.genP : this.form.newP,
|
|
256
|
+
userID: this.userId
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (mode === 'change') {
|
|
260
|
+
spec.currentPassword = this.form.currentP;
|
|
261
|
+
}
|
|
250
262
|
|
|
251
|
-
async changePassword() {
|
|
252
263
|
try {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
body: {
|
|
257
|
-
currentPassword: this.form.currentP,
|
|
258
|
-
newPassword: this.isRandomGenerated ? this.form.genP : this.form.newP
|
|
259
|
-
},
|
|
260
|
-
});
|
|
264
|
+
this.passwordChangeRequest.spec = spec;
|
|
265
|
+
|
|
266
|
+
await this.passwordChangeRequest.save();
|
|
261
267
|
} catch (err) {
|
|
262
268
|
this.errorMessages = [err.message || this.t('changePassword.errors.failedToChange')];
|
|
263
269
|
throw err;
|
|
@@ -5,7 +5,11 @@ import { QUOTA_COMPUTED, TYPES } from './shared';
|
|
|
5
5
|
import Banner from '@components/Banner/Banner.vue';
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
|
-
emits: [
|
|
8
|
+
emits: [
|
|
9
|
+
'remove',
|
|
10
|
+
'input',
|
|
11
|
+
'validationChanged',
|
|
12
|
+
],
|
|
9
13
|
|
|
10
14
|
components: {
|
|
11
15
|
ArrayList,
|
|
@@ -60,6 +64,34 @@ export default {
|
|
|
60
64
|
const { index, type } = event;
|
|
61
65
|
|
|
62
66
|
this.typeValues[index] = type;
|
|
67
|
+
|
|
68
|
+
this.validateTypes();
|
|
69
|
+
},
|
|
70
|
+
updateResourceIdentifier({ type, customType, index }) {
|
|
71
|
+
if (type.startsWith(TYPES.EXTENDED)) {
|
|
72
|
+
this.typeValues[index] = `extended.${ customType }`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.validateTypes();
|
|
76
|
+
},
|
|
77
|
+
validateTypes(isValid = true) {
|
|
78
|
+
if (!isValid) {
|
|
79
|
+
this.$emit('validationChanged', false);
|
|
80
|
+
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const hasMissingExtendedValue = this.typeValues.some((typeValue) => {
|
|
85
|
+
if (!typeValue.startsWith(TYPES.EXTENDED)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const [, resourceIdentifier] = typeValue.split('.');
|
|
90
|
+
|
|
91
|
+
return !resourceIdentifier;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.$emit('validationChanged', !hasMissingExtendedValue);
|
|
63
95
|
},
|
|
64
96
|
remainingTypes(currentType) {
|
|
65
97
|
return this.mappedTypes
|
|
@@ -72,7 +104,13 @@ export default {
|
|
|
72
104
|
});
|
|
73
105
|
},
|
|
74
106
|
emitRemove(data) {
|
|
107
|
+
this.typeValues = this.typeValues.filter((_typeValue, index) => {
|
|
108
|
+
return index !== data.index;
|
|
109
|
+
});
|
|
110
|
+
|
|
75
111
|
this.$emit('remove', data.row?.value);
|
|
112
|
+
|
|
113
|
+
this.validateTypes();
|
|
76
114
|
}
|
|
77
115
|
},
|
|
78
116
|
};
|
|
@@ -122,6 +160,7 @@ export default {
|
|
|
122
160
|
:add-allowed="remainingTypes().length > 0"
|
|
123
161
|
:mode="mode"
|
|
124
162
|
@remove="emitRemove"
|
|
163
|
+
@add="validateTypes(false)"
|
|
125
164
|
>
|
|
126
165
|
<template #columns="props">
|
|
127
166
|
<Row
|
|
@@ -129,9 +168,11 @@ export default {
|
|
|
129
168
|
:mode="mode"
|
|
130
169
|
:types="remainingTypes(typeValues[props.i])"
|
|
131
170
|
:type="typeValues[props.i]"
|
|
171
|
+
:type-values="typeValues"
|
|
132
172
|
:index="props.i"
|
|
133
173
|
@input="$emit('input', $event)"
|
|
134
174
|
@type-change="updateType($event)"
|
|
175
|
+
@update:resource-identifier="updateResourceIdentifier"
|
|
135
176
|
/>
|
|
136
177
|
</template>
|
|
137
178
|
</ArrayList>
|
|
@@ -5,7 +5,10 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|
|
5
5
|
import { ROW_COMPUTED, TYPES } from './shared';
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
|
-
emits: [
|
|
8
|
+
emits: [
|
|
9
|
+
'type-change',
|
|
10
|
+
'update:resource-identifier',
|
|
11
|
+
],
|
|
9
12
|
|
|
10
13
|
components: {
|
|
11
14
|
Select,
|
|
@@ -26,6 +29,10 @@ export default {
|
|
|
26
29
|
type: String,
|
|
27
30
|
default: ''
|
|
28
31
|
},
|
|
32
|
+
typeValues: {
|
|
33
|
+
type: Array,
|
|
34
|
+
default: () => [],
|
|
35
|
+
},
|
|
29
36
|
value: {
|
|
30
37
|
type: Object,
|
|
31
38
|
default: () => {
|
|
@@ -39,7 +46,10 @@ export default {
|
|
|
39
46
|
},
|
|
40
47
|
|
|
41
48
|
data() {
|
|
42
|
-
return {
|
|
49
|
+
return {
|
|
50
|
+
customType: '',
|
|
51
|
+
previousTypeValues: [],
|
|
52
|
+
};
|
|
43
53
|
},
|
|
44
54
|
|
|
45
55
|
created() {
|
|
@@ -48,6 +58,7 @@ export default {
|
|
|
48
58
|
} else {
|
|
49
59
|
this.customType = this.type;
|
|
50
60
|
}
|
|
61
|
+
this.previousTypeValues = [...this.typeValues];
|
|
51
62
|
},
|
|
52
63
|
|
|
53
64
|
computed: {
|
|
@@ -99,6 +110,22 @@ export default {
|
|
|
99
110
|
}
|
|
100
111
|
},
|
|
101
112
|
|
|
113
|
+
watch: {
|
|
114
|
+
typeValues: {
|
|
115
|
+
/**
|
|
116
|
+
* Intentionally uses `oldValues` (not `_newValues`) so that
|
|
117
|
+
* `previousTypeValues` always reflects the committed state before the
|
|
118
|
+
* current change. `deleteResourceLimits` relies on this snapshot to
|
|
119
|
+
* detect duplicate keys: if a key still appears twice in the old list
|
|
120
|
+
* we know another row owns it and must not delete the shared limit.
|
|
121
|
+
*/
|
|
122
|
+
handler(_newValues, oldValues) {
|
|
123
|
+
this.previousTypeValues = [...oldValues];
|
|
124
|
+
},
|
|
125
|
+
deep: true
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
102
129
|
methods: {
|
|
103
130
|
updateType(type) {
|
|
104
131
|
const oldResourceKey = this.isCustom ? this.customType : this.localType;
|
|
@@ -117,9 +144,17 @@ export default {
|
|
|
117
144
|
updateCustomType(type) {
|
|
118
145
|
const oldType = this.customType;
|
|
119
146
|
|
|
120
|
-
|
|
147
|
+
if (oldType) {
|
|
148
|
+
this.deleteResourceLimits(oldType, true);
|
|
149
|
+
}
|
|
121
150
|
|
|
122
151
|
this.customType = type;
|
|
152
|
+
|
|
153
|
+
this.$emit('update:resource-identifier', {
|
|
154
|
+
type: this.type,
|
|
155
|
+
customType: this.customType,
|
|
156
|
+
index: this.index,
|
|
157
|
+
});
|
|
123
158
|
},
|
|
124
159
|
|
|
125
160
|
updateQuotaLimit(prop, type, val) {
|
|
@@ -140,7 +175,38 @@ export default {
|
|
|
140
175
|
this.value.spec[prop].limit[type] = val;
|
|
141
176
|
},
|
|
142
177
|
|
|
143
|
-
deleteResourceLimits(resourceKey) {
|
|
178
|
+
deleteResourceLimits(resourceKey, isExtended = false) {
|
|
179
|
+
const limit = this.value?.spec.resourceQuota?.limit;
|
|
180
|
+
const usedLimit = this.value?.spec.namespaceDefaultResourceQuota?.limit;
|
|
181
|
+
|
|
182
|
+
if (isExtended) {
|
|
183
|
+
// `previousTypeValues` holds the previous snapshot of typeValues.
|
|
184
|
+
// Counting matches against the old list lets us detect whether another
|
|
185
|
+
// row already owns this key before we delete the shared limit entry.
|
|
186
|
+
const matchesForKey = this.previousTypeValues.filter((typeValue) => {
|
|
187
|
+
const [, typeKey] = typeValue.split('.');
|
|
188
|
+
|
|
189
|
+
return resourceKey === typeKey;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Prevent inadvertently deleting values for an existing custom resource
|
|
194
|
+
* if a duplicate key is entered.
|
|
195
|
+
*/
|
|
196
|
+
if (matchesForKey.length >= 2) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (limit?.extended && typeof this.value.spec.resourceQuota?.limit?.extended[resourceKey] !== 'undefined') {
|
|
201
|
+
delete this.value.spec.resourceQuota.limit.extended[resourceKey];
|
|
202
|
+
}
|
|
203
|
+
if (usedLimit?.extended && typeof this.value.spec.namespaceDefaultResourceQuota?.limit?.extended[resourceKey] !== 'undefined') {
|
|
204
|
+
delete this.value.spec.namespaceDefaultResourceQuota.limit.extended[resourceKey];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
144
210
|
if (typeof this.value.spec.resourceQuota?.limit[resourceKey] !== 'undefined') {
|
|
145
211
|
delete this.value.spec.resourceQuota.limit[resourceKey];
|
|
146
212
|
}
|
|
@@ -171,6 +237,7 @@ export default {
|
|
|
171
237
|
:mode="mode"
|
|
172
238
|
:placeholder="t('resourceQuota.resourceIdentifier.placeholder')"
|
|
173
239
|
:rules="customTypeRules"
|
|
240
|
+
:require-dirty="false"
|
|
174
241
|
class="mr-10"
|
|
175
242
|
data-testid="projectrow-custom-type-input"
|
|
176
243
|
@update:value="updateCustomType($event)"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import Project from '@shell/components/form/ResourceQuota/Project.vue';
|
|
3
|
+
import { TYPES } from '@shell/components/form/ResourceQuota/shared';
|
|
4
|
+
|
|
5
|
+
describe('project', () => {
|
|
6
|
+
const defaultProps = {
|
|
7
|
+
mode: 'create',
|
|
8
|
+
value: {
|
|
9
|
+
spec: {
|
|
10
|
+
resourceQuota: { limit: {} },
|
|
11
|
+
namespaceDefaultResourceQuota: { limit: {} }
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
types: []
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
it('should emit validationChanged with false when validateTypes(false) is called (e.g., when adding a row)', () => {
|
|
18
|
+
const wrapper: any = shallowMount(Project, { props: defaultProps });
|
|
19
|
+
|
|
20
|
+
wrapper.vm.validateTypes(false);
|
|
21
|
+
|
|
22
|
+
expect(wrapper.emitted('validationChanged')).toBeTruthy();
|
|
23
|
+
expect(wrapper.emitted('validationChanged')[0]).toStrictEqual([false]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should emit validationChanged with false if an extended type has no resource identifier', () => {
|
|
27
|
+
const wrapper: any = shallowMount(Project, { props: defaultProps });
|
|
28
|
+
|
|
29
|
+
wrapper.setData({ typeValues: [TYPES.EXTENDED] });
|
|
30
|
+
|
|
31
|
+
wrapper.vm.validateTypes(true);
|
|
32
|
+
|
|
33
|
+
expect(wrapper.emitted('validationChanged')).toBeTruthy();
|
|
34
|
+
expect(wrapper.emitted('validationChanged')[0]).toStrictEqual([false]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should emit validationChanged with true if an extended type has resource identifier', () => {
|
|
38
|
+
const wrapper: any = shallowMount(Project, { props: defaultProps });
|
|
39
|
+
|
|
40
|
+
wrapper.setData({ typeValues: ['extended.my-resource'] });
|
|
41
|
+
|
|
42
|
+
wrapper.vm.validateTypes(true);
|
|
43
|
+
|
|
44
|
+
expect(wrapper.emitted('validationChanged')).toBeTruthy();
|
|
45
|
+
expect(wrapper.emitted('validationChanged')[0]).toStrictEqual([true]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should update typeValues and validate when updateResourceIdentifier is called', () => {
|
|
49
|
+
const wrapper: any = shallowMount(Project, { props: defaultProps });
|
|
50
|
+
|
|
51
|
+
wrapper.setData({ typeValues: [TYPES.EXTENDED] });
|
|
52
|
+
|
|
53
|
+
wrapper.vm.updateResourceIdentifier({
|
|
54
|
+
type: TYPES.EXTENDED,
|
|
55
|
+
customType: 'my-resource',
|
|
56
|
+
index: 0
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(wrapper.vm.typeValues[0]).toStrictEqual('extended.my-resource');
|
|
60
|
+
expect(wrapper.emitted('validationChanged')).toBeTruthy();
|
|
61
|
+
expect(wrapper.emitted('validationChanged')[0]).toStrictEqual([true]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -149,6 +149,11 @@ export default {
|
|
|
149
149
|
type: Boolean,
|
|
150
150
|
default: false,
|
|
151
151
|
},
|
|
152
|
+
|
|
153
|
+
isGithubDotComRepository: {
|
|
154
|
+
type: Boolean,
|
|
155
|
+
default: false,
|
|
156
|
+
},
|
|
152
157
|
},
|
|
153
158
|
|
|
154
159
|
async fetch() {
|
|
@@ -662,7 +667,7 @@ export default {
|
|
|
662
667
|
data-testid="auth-secret-basic-password"
|
|
663
668
|
:mode="mode"
|
|
664
669
|
type="password"
|
|
665
|
-
label-key="selectOrCreateAuthSecret.basic.password"
|
|
670
|
+
:label-key="isGithubDotComRepository ? 'selectOrCreateAuthSecret.basic.passwordPersonalAccessToken' : 'selectOrCreateAuthSecret.basic.password'"
|
|
666
671
|
/>
|
|
667
672
|
</div>
|
|
668
673
|
</template>
|
|
@@ -31,4 +31,39 @@ describe('component: SelectOrCreateAuthSecret', () => {
|
|
|
31
31
|
|
|
32
32
|
expect(knownSshHosts.exists()).toBe(showSshKnownHosts || false);
|
|
33
33
|
});
|
|
34
|
+
|
|
35
|
+
it.each([
|
|
36
|
+
['selectOrCreateAuthSecret.basic.passwordPersonalAccessToken', true],
|
|
37
|
+
['selectOrCreateAuthSecret.basic.password', false],
|
|
38
|
+
['selectOrCreateAuthSecret.basic.password', undefined],
|
|
39
|
+
])('should render "%s" label when isGithubDotComRepository is %p', async(expectedLabel, isGithubDotComRepository) => {
|
|
40
|
+
const wrapper = mount(SelectOrCreateAuthSecret, {
|
|
41
|
+
...requiredSetup(),
|
|
42
|
+
props: {
|
|
43
|
+
mode: _EDIT,
|
|
44
|
+
namespace: 'default',
|
|
45
|
+
value: {},
|
|
46
|
+
isGithubDotComRepository,
|
|
47
|
+
registerBeforeHook: () => {},
|
|
48
|
+
},
|
|
49
|
+
data() {
|
|
50
|
+
return { selected: AUTH_TYPE._BASIC } as any;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await wrapper.vm.$nextTick();
|
|
55
|
+
|
|
56
|
+
// Find the LabeledInput component with the password data-testid
|
|
57
|
+
const labeledInputComponents = wrapper.findAllComponents({ name: 'LabeledInput' });
|
|
58
|
+
|
|
59
|
+
// The second LabeledInput component should be the password field
|
|
60
|
+
const passwordLabeledInput = labeledInputComponents.length > 1 ? labeledInputComponents[1] : null;
|
|
61
|
+
|
|
62
|
+
expect(passwordLabeledInput).not.toBeNull();
|
|
63
|
+
|
|
64
|
+
// Check the label-key prop to verify correct i18n key is used
|
|
65
|
+
const expectedLabelKey = isGithubDotComRepository ? 'selectOrCreateAuthSecret.basic.passwordPersonalAccessToken' : 'selectOrCreateAuthSecret.basic.password';
|
|
66
|
+
|
|
67
|
+
expect(passwordLabeledInput!.props('labelKey')).toBe(expectedLabelKey);
|
|
68
|
+
});
|
|
34
69
|
});
|
|
@@ -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>
|
|
@@ -37,7 +37,10 @@ export default {
|
|
|
37
37
|
offset="1"
|
|
38
38
|
>
|
|
39
39
|
<template #popper>
|
|
40
|
-
<table
|
|
40
|
+
<table
|
|
41
|
+
data-testid="machine-progress-popper"
|
|
42
|
+
class="fixed"
|
|
43
|
+
>
|
|
41
44
|
<tbody>
|
|
42
45
|
<tr
|
|
43
46
|
v-for="(obj, i) in row.stateParts"
|
|
@@ -49,7 +52,10 @@ export default {
|
|
|
49
52
|
>
|
|
50
53
|
{{ obj.label }}
|
|
51
54
|
</td>
|
|
52
|
-
<td
|
|
55
|
+
<td
|
|
56
|
+
:data-testid="`machine-progress-popper-${obj.label.toLowerCase()}`"
|
|
57
|
+
class="text-right"
|
|
58
|
+
>
|
|
53
59
|
{{ obj.value }}
|
|
54
60
|
</td>
|
|
55
61
|
</tr>
|
|
@@ -60,11 +66,13 @@ export default {
|
|
|
60
66
|
<div
|
|
61
67
|
class="content"
|
|
62
68
|
:class="{ horizontal }"
|
|
69
|
+
data-testid="machine-progress-count"
|
|
63
70
|
>
|
|
64
71
|
<ProgressBarMulti
|
|
65
72
|
v-if="row.stateParts"
|
|
66
73
|
:values="row.stateParts"
|
|
67
74
|
class="progress-bar"
|
|
75
|
+
data-testid="machine-progress-bar"
|
|
68
76
|
/>
|
|
69
77
|
<span
|
|
70
78
|
v-if="row.desired === ready"
|