@rancher/shell 0.3.8 → 0.3.9

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 (36) hide show
  1. package/assets/translations/en-us.yaml +28 -2
  2. package/babel.config.js +17 -4
  3. package/components/CodeMirror.vue +146 -14
  4. package/components/ContainerResourceLimit.vue +14 -1
  5. package/components/CruResource.vue +21 -5
  6. package/components/ExplorerProjectsNamespaces.vue +5 -1
  7. package/components/GroupPanel.vue +57 -0
  8. package/components/YamlEditor.vue +2 -2
  9. package/components/form/ArrayList.vue +1 -1
  10. package/components/form/KeyValue.vue +34 -1
  11. package/components/form/MatchExpressions.vue +120 -21
  12. package/components/form/NodeAffinity.vue +54 -4
  13. package/components/form/PodAffinity.vue +160 -47
  14. package/components/form/Tolerations.vue +40 -4
  15. package/components/form/__tests__/ArrayList.test.ts +3 -3
  16. package/components/form/__tests__/MatchExpressions.test.ts +1 -1
  17. package/components/nav/Header.vue +2 -0
  18. package/config/settings.ts +6 -1
  19. package/core/plugins-loader.js +0 -2
  20. package/edit/configmap.vue +33 -6
  21. package/edit/provisioning.cattle.io.cluster/AgentConfiguration.vue +326 -0
  22. package/edit/provisioning.cattle.io.cluster/index.vue +1 -0
  23. package/edit/provisioning.cattle.io.cluster/rke2.vue +60 -0
  24. package/mixins/chart.js +1 -1
  25. package/models/batch.cronjob.js +18 -3
  26. package/models/workload.js +1 -1
  27. package/package.json +2 -3
  28. package/pages/auth/login.vue +1 -0
  29. package/pages/prefs.vue +18 -2
  30. package/pkg/vue.config.js +0 -1
  31. package/plugins/codemirror.js +158 -0
  32. package/public/index.html +1 -1
  33. package/types/shell/index.d.ts +20 -1
  34. package/utils/create-yaml.js +105 -8
  35. package/utils/settings.ts +12 -0
  36. package/vue.config.js +2 -2
@@ -29,6 +29,13 @@ export default {
29
29
  default: NODE
30
30
  },
31
31
 
32
+ // has select for matching fields or expressions (used for node affinity)
33
+ // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#nodeselectorterm-v1-core
34
+ matchingSelectorDisplay: {
35
+ type: Boolean,
36
+ default: false,
37
+ },
38
+
32
39
  // whether or not to show an initial empty row of inputs when value is empty in editing modes
33
40
  initialEmptyRow: {
34
41
  type: Boolean,
@@ -83,28 +90,39 @@ export default {
83
90
 
84
91
  let rules;
85
92
 
86
- if ( isArray(this.value) ) {
93
+ // special case for matchFields and matchExpressions
94
+ // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#nodeselectorterm-v1-core
95
+ if ( this.matchingSelectorDisplay) {
96
+ const rulesByType = {
97
+ matchFields: [],
98
+ matchExpressions: []
99
+ };
100
+
101
+ ['matchFields', 'matchExpressions'].forEach((type) => {
102
+ rulesByType[type] = this.parseRules(this.value[type], type);
103
+ });
104
+
105
+ rules = [...rulesByType.matchFields, ...rulesByType.matchExpressions];
106
+ } else if ( isArray(this.value) ) {
87
107
  rules = [...this.value];
108
+ rules = this.parseRules(rules);
88
109
  } else {
89
110
  rules = convert(this.value.matchLabels, this.value.matchExpressions);
111
+ rules = this.parseRules(rules);
90
112
  }
91
113
 
92
- rules = rules.map((rule) => {
93
- const newRule = clone(rule);
94
-
95
- if (newRule.values && typeof newRule.values !== 'string') {
96
- newRule.values = newRule.values.join(', ');
97
- }
98
-
99
- return newRule;
100
- });
101
-
102
114
  if (!rules.length && this.initialEmptyRow && !this.isView) {
103
- rules.push({
115
+ const newRule = {
104
116
  key: '',
105
117
  operator: 'In',
106
118
  values: ''
107
- });
119
+ };
120
+
121
+ if (this.matchingSelectorDisplay) {
122
+ newRule.matching = 'matchExpressions';
123
+ }
124
+
125
+ rules.push(newRule);
108
126
  }
109
127
 
110
128
  return {
@@ -131,27 +149,71 @@ export default {
131
149
  return !!this.keysSelectOptions?.length;
132
150
  },
133
151
 
152
+ matchingSelectOptions() {
153
+ return [
154
+ {
155
+ label: this.t('workload.scheduling.affinity.matchExpressions.label'),
156
+ value: 'matchExpressions',
157
+ },
158
+ {
159
+ label: this.t('workload.scheduling.affinity.matchFields.label'),
160
+ value: 'matchFields',
161
+ },
162
+ ];
163
+ },
164
+
134
165
  ...mapGetters({ t: 'i18n/t' })
135
166
  },
136
167
 
137
168
  methods: {
169
+ parseRules(rules, matching) {
170
+ if (rules?.length) {
171
+ return rules.map((rule) => {
172
+ const newRule = clone(rule);
173
+
174
+ if (newRule.values && typeof newRule.values !== 'string') {
175
+ newRule.values = newRule.values.join(', ');
176
+ }
177
+
178
+ if (matching) {
179
+ newRule.matching = matching;
180
+ }
181
+
182
+ return newRule;
183
+ });
184
+ }
185
+
186
+ return [];
187
+ },
188
+
138
189
  removeRule(row) {
139
190
  removeObject(this.rules, row);
140
191
  this.update();
141
192
  },
142
193
 
143
194
  addRule() {
144
- this.rules.push({
195
+ const newRule = {
145
196
  key: '',
146
197
  operator: 'In',
147
198
  values: ''
148
- });
199
+ };
200
+
201
+ if (this.matchingSelectorDisplay) {
202
+ newRule.matching = 'matchExpressions';
203
+ }
204
+
205
+ this.rules.push(newRule);
149
206
  },
150
207
 
151
208
  update() {
152
209
  this.$nextTick(() => {
153
210
  const out = this.rules.map((rule) => {
154
- const matchExpression = { key: rule.key, operator: rule.operator };
211
+ const expression = { key: rule.key, operator: rule.operator };
212
+
213
+ if (this.matchingSelectorDisplay) {
214
+ expression.matching = rule.matching;
215
+ }
216
+
155
217
  let val = (rule.values || '').trim();
156
218
 
157
219
  if ( rule.operator === 'Exists' || rule.operator === 'DoesNotExist') {
@@ -161,13 +223,13 @@ export default {
161
223
  }
162
224
 
163
225
  if ( val !== null ) {
164
- matchExpression.values = val.split(/\s*,\s*/).filter(x => !!x);
226
+ expression.values = val.split(/\s*,\s*/).filter(x => !!x);
165
227
  }
166
228
 
167
- return matchExpression;
229
+ return expression;
168
230
  }).filter(x => !!x);
169
231
 
170
- if ( isArray(this.value) ) {
232
+ if ( isArray(this.value) || this.matchingSelectorDisplay ) {
171
233
  this.$emit('input', out);
172
234
  } else {
173
235
  this.$emit('input', simplify(out));
@@ -192,8 +254,11 @@ export default {
192
254
  <div
193
255
  v-if="rules.length"
194
256
  class="match-expression-header"
195
- :class="{'view':isView}"
257
+ :class="{ 'view':isView, 'match-expression-header-matching': matchingSelectorDisplay }"
196
258
  >
259
+ <label v-if="matchingSelectorDisplay">
260
+ {{ t('workload.scheduling.affinity.matchExpressions.matchType') }}
261
+ </label>
197
262
  <label>
198
263
  {{ t('workload.scheduling.affinity.matchExpressions.key') }}
199
264
  </label>
@@ -209,8 +274,25 @@ export default {
209
274
  v-for="(row, index) in rules"
210
275
  :key="row.id"
211
276
  class="match-expression-row"
212
- :class="{'view':isView, 'mb-10': index !== rules.length - 1}"
277
+ :class="{'view':isView, 'mb-10': index !== rules.length - 1, 'match-expression-row-matching': matchingSelectorDisplay}"
213
278
  >
279
+ <!-- Select for matchFields and matchExpressions -->
280
+ <div
281
+ v-if="matchingSelectorDisplay"
282
+ :data-testid="`input-match-type-field-${index}`"
283
+ >
284
+ <div v-if="isView">
285
+ {{ row.matching }}
286
+ </div>
287
+ <LabeledSelect
288
+ v-else
289
+ v-model="row.matching"
290
+ :mode="mode"
291
+ :options="matchingSelectOptions"
292
+ :data-testid="`input-match-type-field-control-${index}`"
293
+ @selecting="update"
294
+ />
295
+ </div>
214
296
  <div
215
297
  :data-testid="`input-match-expression-key-${index}`"
216
298
  >
@@ -221,6 +303,7 @@ export default {
221
303
  v-else-if="!hasKeySelectOptions"
222
304
  v-model="row.key"
223
305
  :mode="mode"
306
+ :data-testid="`input-match-expression-key-control-${index}`"
224
307
  @input="update"
225
308
  >
226
309
  <LabeledSelect
@@ -228,6 +311,7 @@ export default {
228
311
  v-model="row.key"
229
312
  :mode="mode"
230
313
  :options="keysSelectOptions"
314
+ :data-testid="`input-match-expression-key-control-select-${index}`"
231
315
  />
232
316
  </div>
233
317
  <div
@@ -244,6 +328,7 @@ export default {
244
328
  :clearable="false"
245
329
  :reduce="opt=>opt.value"
246
330
  :mode="mode"
331
+ :data-testid="`input-match-expression-operator-control-${index}`"
247
332
  @input="update"
248
333
  />
249
334
  </div>
@@ -266,6 +351,7 @@ export default {
266
351
  v-model="row.values"
267
352
  :mode="mode"
268
353
  :disabled="row.operator==='Exists' || row.operator==='DoesNotExist'"
354
+ :data-testid="`input-match-expression-values-control-${index}`"
269
355
  @input="update"
270
356
  >
271
357
  </div>
@@ -280,6 +366,7 @@ export default {
280
366
  :style="{padding:'0px'}"
281
367
 
282
368
  :disabled="mode==='view'"
369
+ :data-testid="`input-match-expression-remove-control-${index}`"
283
370
  @click="removeRule(row)"
284
371
  >
285
372
  <t k="generic.remove" />
@@ -293,6 +380,7 @@ export default {
293
380
  <button
294
381
  type="button"
295
382
  class="btn role-tertiary add"
383
+ :data-testid="`input-match-expression-add-rule`"
296
384
  @click="addRule"
297
385
  >
298
386
  <t k="workload.scheduling.affinity.matchExpressions.addRule" />
@@ -344,4 +432,15 @@ export default {
344
432
  grid-template-columns: repeat(3, 1fr) 50px;
345
433
  }
346
434
  }
435
+
436
+ .match-expression-row > div > input {
437
+ min-height: 40px !important;
438
+ }
439
+ .match-expression-row-matching, .match-expression-header-matching {
440
+ grid-template-columns: 1fr 1fr 1fr 1fr;
441
+
442
+ &:not(.view){
443
+ grid-template-columns: 1fr 1fr 1fr 1fr 100px;
444
+ }
445
+ }
347
446
  </style>
@@ -6,12 +6,13 @@ import { get, isEmpty, clone } from '@shell/utils/object';
6
6
  import { NODE } from '@shell/config/types';
7
7
  import MatchExpressions from '@shell/components/form/MatchExpressions';
8
8
  import LabeledSelect from '@shell/components/form/LabeledSelect';
9
+ import { LabeledInput } from '@components/Form/LabeledInput';
9
10
  import { randomStr } from '@shell/utils/string';
10
11
  import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
11
12
 
12
13
  export default {
13
14
  components: {
14
- ArrayListGrouped, MatchExpressions, LabeledSelect
15
+ ArrayListGrouped, MatchExpressions, LabeledSelect, LabeledInput
15
16
  },
16
17
 
17
18
  props: {
@@ -27,6 +28,13 @@ export default {
27
28
  type: String,
28
29
  default: 'create'
29
30
  },
31
+
32
+ // has select for matching fields or expressions (used for node affinity)
33
+ // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#nodeselectorterm-v1-core
34
+ matchingSelectorDisplay: {
35
+ type: Boolean,
36
+ default: false,
37
+ },
30
38
  },
31
39
 
32
40
  data() {
@@ -89,7 +97,7 @@ export default {
89
97
 
90
98
  this.allSelectorTerms.forEach((term) => {
91
99
  if (term.weight) {
92
- const neu = { weight: 1, preference: term };
100
+ const neu = { weight: term.weight, preference: term };
93
101
 
94
102
  preferredDuringSchedulingIgnoredDuringExecution.push(neu);
95
103
  } else {
@@ -103,6 +111,7 @@ export default {
103
111
  if (requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.length) {
104
112
  out.requiredDuringSchedulingIgnoredDuringExecution = requiredDuringSchedulingIgnoredDuringExecution;
105
113
  }
114
+
106
115
  this.$emit('input', out);
107
116
  },
108
117
 
@@ -124,6 +133,28 @@ export default {
124
133
  return term.weight ? this.t('workload.scheduling.affinity.preferred') : this.t('workload.scheduling.affinity.required');
125
134
  },
126
135
 
136
+ updateExpressions(row, expressions) {
137
+ const expressionsMatching = {
138
+ matchFields: [],
139
+ matchExpressions: []
140
+ };
141
+
142
+ if (expressions.length) {
143
+ expressions.forEach((expression) => {
144
+ expressionsMatching[expression.matching || 'matchExpressions'].push(expression);
145
+ });
146
+
147
+ if (expressionsMatching.matchFields.length) {
148
+ this.$set(row, 'matchFields', expressionsMatching.matchFields);
149
+ }
150
+ if (expressionsMatching.matchExpressions.length) {
151
+ this.$set(row, 'matchExpressions', expressionsMatching.matchExpressions);
152
+ }
153
+
154
+ this.update();
155
+ }
156
+ },
157
+
127
158
  get,
128
159
 
129
160
  isEmpty
@@ -148,23 +179,42 @@ export default {
148
179
  >
149
180
  <template #default="props">
150
181
  <div class="row">
151
- <div class="col span-6">
182
+ <div class="col span-9">
152
183
  <LabeledSelect
153
184
  :options="affinityOptions"
154
185
  :value="priorityDisplay(props.row.value)"
155
186
  :label="t('workload.scheduling.affinity.priority')"
156
187
  :mode="mode"
188
+ :data-testid="`node-affinity-priority-index${props.i}`"
157
189
  @input="(changePriority(props.row.value))"
158
190
  />
159
191
  </div>
192
+ <div
193
+ v-if="props.row.value.weight"
194
+ class="col span-3"
195
+ >
196
+ <LabeledInput
197
+ v-model.number="props.row.value.weight"
198
+ :mode="mode"
199
+ type="number"
200
+ min="1"
201
+ max="100"
202
+ :label="t('workload.scheduling.affinity.weight.label')"
203
+ :placeholder="t('workload.scheduling.affinity.weight.placeholder')"
204
+ :data-testid="`node-affinity-weight-index${props.i}`"
205
+ />
206
+ </div>
160
207
  </div>
161
208
  <MatchExpressions
162
209
  :key="rerenderNums"
163
- v-model="props.row.value.matchExpressions"
210
+ :value="matchingSelectorDisplay ? props.row.value : props.row.value.matchExpressions"
211
+ :matching-selector-display="matchingSelectorDisplay"
164
212
  :mode="mode"
165
213
  class="col span-12 mt-20"
166
214
  :type="node"
167
215
  :show-remove="false"
216
+ :data-testid="`node-affinity-expressions-index${props.i}`"
217
+ @input="(updateExpressions(props.row.value, $event))"
168
218
  />
169
219
  </template>
170
220
  </ArrayListGrouped>