@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.
Files changed (128) hide show
  1. package/assets/brand/suse/metadata.json +2 -1
  2. package/assets/translations/en-us.yaml +105 -5
  3. package/components/ActionMenuShell.vue +1 -1
  4. package/components/Inactivity.vue +2 -2
  5. package/components/Resource/Detail/Card/ExtrasCard.vue +49 -15
  6. package/components/Resource/Detail/Card/__tests__/ExtrasCard.test.ts +111 -0
  7. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +0 -17
  8. package/components/Resource/Detail/Masthead/index.vue +11 -4
  9. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +3 -1
  10. package/components/Resource/Detail/Metadata/index.vue +1 -1
  11. package/components/Resource/Detail/ResourceRow.vue +1 -1
  12. package/components/ResourceDetail/Masthead/latest.vue +12 -2
  13. package/components/ResourceList/index.vue +9 -0
  14. package/components/ResourceTable.vue +38 -4
  15. package/components/Tabbed/Tab.vue +4 -0
  16. package/components/Tabbed/index.vue +4 -1
  17. package/components/__tests__/ProjectRow.test.ts +60 -0
  18. package/components/form/ChangePassword.vue +41 -35
  19. package/components/form/ResourceQuota/Project.vue +42 -1
  20. package/components/form/ResourceQuota/ProjectRow.vue +71 -4
  21. package/components/form/ResourceQuota/__tests__/Project.test.ts +63 -0
  22. package/components/form/SelectOrCreateAuthSecret.vue +6 -1
  23. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +35 -0
  24. package/components/formatter/KubeconfigClusters.vue +74 -0
  25. package/components/formatter/MachineSummaryGraph.vue +10 -2
  26. package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
  27. package/components/nav/TopLevelMenu.helper.ts +50 -2
  28. package/components/nav/TopLevelMenu.vue +14 -0
  29. package/components/nav/Type.vue +5 -0
  30. package/components/nav/__tests__/TopLevelMenu.test.ts +3 -3
  31. package/components/nav/__tests__/Type.test.ts +6 -4
  32. package/config/product/explorer.js +4 -3
  33. package/config/product/manager.js +47 -3
  34. package/config/router/navigation-guards/authentication.js +8 -9
  35. package/config/router/routes.js +4 -1
  36. package/config/types.js +10 -2
  37. package/detail/auditlog.cattle.io.auditpolicy.vue +19 -0
  38. package/detail/management.cattle.io.user.vue +1 -2
  39. package/detail/node.vue +0 -1
  40. package/detail/provisioning.cattle.io.cluster.vue +2 -1
  41. package/dialog/ChangePasswordDialog.vue +8 -0
  42. package/dialog/GenericPrompt.vue +20 -3
  43. package/dialog/ScaleMachineDownDialog.vue +65 -15
  44. package/dialog/SearchDialog.vue +10 -2
  45. package/dialog/__tests__/ScaleMachineDownDialog.test.ts +184 -0
  46. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +89 -0
  47. package/edit/__tests__/management.cattle.io.project.test.js +56 -1
  48. package/edit/auditlog.cattle.io.auditpolicy/AdditionalRedactions.vue +114 -0
  49. package/edit/auditlog.cattle.io.auditpolicy/Filters.vue +119 -0
  50. package/edit/auditlog.cattle.io.auditpolicy/General.vue +180 -0
  51. package/edit/auditlog.cattle.io.auditpolicy/__tests__/AdditionalRedactions.test.ts +327 -0
  52. package/edit/auditlog.cattle.io.auditpolicy/__tests__/Filters.test.ts +449 -0
  53. package/edit/auditlog.cattle.io.auditpolicy/__tests__/General.test.ts +472 -0
  54. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/AdditionalRedactions.test.ts.snap +27 -0
  55. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/Filters.test.ts.snap +39 -0
  56. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +174 -0
  57. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +29 -0
  58. package/edit/auditlog.cattle.io.auditpolicy/__tests__/index.test.ts +215 -0
  59. package/edit/auditlog.cattle.io.auditpolicy/index.vue +104 -0
  60. package/edit/auditlog.cattle.io.auditpolicy/types.ts +28 -0
  61. package/edit/fleet.cattle.io.gitrepo.vue +16 -1
  62. package/edit/management.cattle.io.project.vue +8 -2
  63. package/edit/management.cattle.io.user.vue +29 -34
  64. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +178 -0
  65. package/edit/provisioning.cattle.io.cluster/rke2.vue +22 -2
  66. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -0
  67. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
  68. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +57 -2
  69. package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +109 -0
  70. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +1 -0
  71. package/list/auditlog.cattle.io.auditpolicy.vue +63 -0
  72. package/list/ext.cattle.io.kubeconfig.vue +118 -0
  73. package/list/group.principal.vue +11 -15
  74. package/list/management.cattle.io.user.vue +11 -21
  75. package/machine-config/azure.vue +14 -0
  76. package/mixins/__tests__/chart.test.ts +147 -0
  77. package/mixins/browser-tab-visibility.js +5 -4
  78. package/mixins/chart.js +10 -8
  79. package/mixins/fetch.client.js +6 -0
  80. package/models/__tests__/auditlog.cattle.io.auditpolicy.test.ts +117 -0
  81. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
  82. package/models/__tests__/secret.test.ts +55 -0
  83. package/models/__tests__/workload.test.ts +49 -6
  84. package/models/auditlog.cattle.io.auditpolicy.js +46 -0
  85. package/models/cluster.x-k8s.io.machine.js +1 -1
  86. package/models/cluster.x-k8s.io.machinedeployment.js +5 -5
  87. package/models/event.js +5 -0
  88. package/models/ext.cattle.io.groupmembershiprefreshrequest.js +15 -0
  89. package/models/ext.cattle.io.kubeconfig.ts +97 -0
  90. package/models/ext.cattle.io.passwordchangerequest.js +15 -0
  91. package/models/ext.cattle.io.selfuser.js +15 -0
  92. package/models/fleet-application.js +17 -7
  93. package/models/management.cattle.io.user.js +28 -31
  94. package/models/schema.js +18 -0
  95. package/models/secret.js +28 -25
  96. package/models/steve-schema.ts +39 -2
  97. package/models/workload.js +3 -2
  98. package/package.json +2 -2
  99. package/pages/about.vue +3 -2
  100. package/pages/account/index.vue +23 -16
  101. package/pages/auth/login.vue +15 -8
  102. package/pages/auth/setup.vue +52 -15
  103. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
  104. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  105. package/pages/home.vue +9 -3
  106. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -3
  107. package/plugins/dashboard-store/actions.js +7 -0
  108. package/plugins/dashboard-store/getters.js +23 -1
  109. package/plugins/dashboard-store/index.js +3 -2
  110. package/plugins/dashboard-store/mutations.js +4 -0
  111. package/plugins/dashboard-store/resource-class.js +12 -5
  112. package/plugins/steve/__tests__/steve-class.test.ts +167 -0
  113. package/plugins/steve/schema.d.ts +5 -0
  114. package/plugins/steve/steve-class.js +19 -0
  115. package/plugins/steve/steve-pagination-utils.ts +2 -1
  116. package/rancher-components/RcItemCard/RcItemCard.test.ts +4 -2
  117. package/rancher-components/RcItemCard/RcItemCard.vue +27 -10
  118. package/store/auth.js +57 -19
  119. package/store/notifications.ts +1 -1
  120. package/store/type-map.js +12 -1
  121. package/types/shell/index.d.ts +24 -15
  122. package/types/store/dashboard-store.types.ts +7 -0
  123. package/utils/__tests__/chart.test.ts +96 -0
  124. package/utils/__tests__/version.test.ts +1 -19
  125. package/utils/chart.js +64 -0
  126. package/utils/pagination-wrapper.ts +11 -3
  127. package/utils/version.js +5 -17
  128. package/vue.config.js +26 -13
@@ -44,6 +44,10 @@ export default {
44
44
  type: Boolean,
45
45
  default: false
46
46
  },
47
+ errorIconTooltip: {
48
+ type: String,
49
+ default: ''
50
+ },
47
51
  badge: {
48
52
  default: 0,
49
53
  required: false,
@@ -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
- if (this.isChange) {
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(ctx) {
37
+ data() {
43
38
  return {
44
- username: '',
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(user) {
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.setPassword(user);
244
+ return this.changePassword('set');
238
245
  }
239
246
  },
240
247
 
241
- async setPassword(user) {
242
- // Error handling is catered for by caller
243
- await this.$store.dispatch('rancher/resourceAction', {
244
- type: NORMAN.USER,
245
- actionName: 'setpassword',
246
- resource: user,
247
- body: { newPassword: this.isRandomGenerated ? this.form.genP : this.form.newP },
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
- await this.$store.dispatch('rancher/collectionAction', {
254
- type: NORMAN.USER,
255
- actionName: 'changepassword',
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: ['remove', 'input'],
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: ['type-change'],
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 { customType: '' };
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
- this.deleteResourceLimits(oldType);
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">,&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>
@@ -37,7 +37,10 @@ export default {
37
37
  offset="1"
38
38
  >
39
39
  <template #popper>
40
- <table class="fixed">
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 class="text-right">
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"