@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
@@ -12,6 +12,12 @@ import debounce from 'lodash/debounce';
12
12
  import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
13
13
  import { getUniqueLabelKeys } from '@shell/utils/array';
14
14
 
15
+ const NAMESPACE_SELECTION_OPTION_VALUES = {
16
+ POD: 'pod',
17
+ ALL: 'all',
18
+ SELECTED: 'selected',
19
+ };
20
+
15
21
  export default {
16
22
  components: {
17
23
  ArrayListGrouped, MatchExpressions, LabeledSelect, RadioGroup, LabeledInput
@@ -26,6 +32,13 @@ export default {
26
32
  }
27
33
  },
28
34
 
35
+ // Field key on the value object to store the pod affinity - typically this is 'affinity'
36
+ // Cluster Agent Configuration uses a different field
37
+ field: {
38
+ type: String,
39
+ default: 'affinity'
40
+ },
41
+
29
42
  mode: {
30
43
  type: String,
31
44
  default: 'create'
@@ -40,6 +53,22 @@ export default {
40
53
  type: Array,
41
54
  default: null
42
55
  },
56
+
57
+ allNamespacesOptionAvailable: {
58
+ default: false,
59
+ type: Boolean
60
+ },
61
+
62
+ forceInputNamespaceSelection: {
63
+ default: false,
64
+ type: Boolean
65
+ },
66
+
67
+ removeLabeledInputNamespaceLabel: {
68
+ default: false,
69
+ type: Boolean
70
+ },
71
+
43
72
  loading: {
44
73
  default: false,
45
74
  type: Boolean
@@ -47,32 +76,38 @@ export default {
47
76
  },
48
77
 
49
78
  data() {
50
- if (!this.value.affinity) {
51
- this.$set(this.value, 'affinity', {});
79
+ if (!this.value[this.field]) {
80
+ this.$set(this.value, this.field, {});
52
81
  }
53
- const { podAffinity = {}, podAntiAffinity = {} } = this.value.affinity;
82
+ const { podAffinity = {}, podAntiAffinity = {} } = this.value[this.field];
54
83
  const allAffinityTerms = [...(podAffinity.preferredDuringSchedulingIgnoredDuringExecution || []), ...(podAffinity.requiredDuringSchedulingIgnoredDuringExecution || [])].map((term) => {
55
- const out = clone(term);
84
+ let out = clone(term);
56
85
 
57
86
  out._id = randomStr(4);
58
87
  out._anti = false;
59
88
  if (term.podAffinityTerm) {
60
89
  Object.assign(out, term.podAffinityTerm);
61
- out._namespaces = (term.podAffinityTerm.namespaces || []).toString();
90
+ out = this.parsePodAffinityTerm(out);
91
+
62
92
  delete out.podAffinityTerm;
93
+ } else {
94
+ out = this.parsePodAffinityTerm(out);
63
95
  }
64
96
 
65
97
  return out;
66
98
  });
67
99
  const allAntiTerms = [...(podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution || []), ...(podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution || [])].map((term) => {
68
- const out = clone(term);
100
+ let out = clone(term);
69
101
 
70
102
  out._id = randomStr(4);
71
103
  out._anti = true;
72
104
  if (term.podAffinityTerm) {
73
105
  Object.assign(out, term.podAffinityTerm);
74
- out._namespaces = (term.podAffinityTerm.namespaces || []).toString();
106
+ out = this.parsePodAffinityTerm(out);
107
+
75
108
  delete out.podAffinityTerm;
109
+ } else {
110
+ out = this.parsePodAffinityTerm(out);
76
111
  }
77
112
 
78
113
  return out;
@@ -82,10 +117,17 @@ export default {
82
117
 
83
118
  return {
84
119
  allSelectorTerms,
85
- defaultWeight: 1,
120
+ defaultWeight: 1,
86
121
  // rules in MatchExpressions.vue can not catch changes what happens on parent component
87
122
  // we need re-render it via key changing
88
- rerenderNums: randomStr(4)
123
+ rerenderNums: randomStr(4),
124
+ NAMESPACE_SELECTION_OPTION_VALUES,
125
+ defaultAddValue: {
126
+ _namespaceOption: NAMESPACE_SELECTION_OPTION_VALUES.POD,
127
+ matchExpressions: [],
128
+ namespaces: null,
129
+ _namespaces: null,
130
+ }
89
131
  };
90
132
  },
91
133
  computed: {
@@ -101,9 +143,14 @@ export default {
101
143
  return NODE;
102
144
  },
103
145
 
104
- allNamespaces() {
146
+ labeledInputNamespaceLabel() {
147
+ return this.removeLabeledInputNamespaceLabel ? '' : this.t('workload.scheduling.affinity.matchExpressions.inNamespaces');
148
+ },
149
+
150
+ allNamespacesOptions() {
105
151
  const inStore = this.$store.getters['currentStore'](NAMESPACE);
106
152
  const choices = this.namespaces || this.$store.getters[`${ inStore }/all`](NAMESPACE);
153
+
107
154
  const out = sortBy(choices.map((obj) => {
108
155
  return {
109
156
  label: obj.nameDisplay,
@@ -122,8 +169,38 @@ export default {
122
169
  return this.nodes.length;
123
170
  },
124
171
 
172
+ namespaceSelectionOptions() {
173
+ if (this.allNamespacesOptionAvailable) {
174
+ return [
175
+ NAMESPACE_SELECTION_OPTION_VALUES.POD,
176
+ NAMESPACE_SELECTION_OPTION_VALUES.ALL,
177
+ NAMESPACE_SELECTION_OPTION_VALUES.SELECTED
178
+ ];
179
+ }
180
+
181
+ return [
182
+ NAMESPACE_SELECTION_OPTION_VALUES.POD,
183
+ NAMESPACE_SELECTION_OPTION_VALUES.SELECTED
184
+ ];
185
+ },
186
+
187
+ namespaceSelectionLabels() {
188
+ if (this.allNamespacesOptionAvailable) {
189
+ return [
190
+ this.t('workload.scheduling.affinity.thisPodNamespace'),
191
+ this.t('workload.scheduling.affinity.allNamespaces'),
192
+ this.t('workload.scheduling.affinity.matchExpressions.inNamespaces')
193
+ ];
194
+ }
195
+
196
+ return [
197
+ this.t('workload.scheduling.affinity.thisPodNamespace'),
198
+ this.t('workload.scheduling.affinity.matchExpressions.inNamespaces')
199
+ ];
200
+ },
201
+
125
202
  hasNamespaces() {
126
- return this.allNamespaces.length;
203
+ return this.allNamespacesOptions.length;
127
204
  },
128
205
  },
129
206
 
@@ -132,6 +209,20 @@ export default {
132
209
  },
133
210
 
134
211
  methods: {
212
+ parsePodAffinityTerm(out) {
213
+ if (out.namespaceSelector && typeof out.namespaceSelector === 'object' && !Object.keys(out.namespaceSelector).length && this.allNamespacesOptionAvailable) {
214
+ out._namespaceOption = NAMESPACE_SELECTION_OPTION_VALUES.ALL;
215
+ } else if (out.namespaces?.length) {
216
+ out._namespaceOption = NAMESPACE_SELECTION_OPTION_VALUES.SELECTED;
217
+ } else {
218
+ out._namespaceOption = NAMESPACE_SELECTION_OPTION_VALUES.POD;
219
+ }
220
+
221
+ out._namespaces = (out.namespaces || []).toString();
222
+
223
+ return out;
224
+ },
225
+
135
226
  update() {
136
227
  const podAffinity = { requiredDuringSchedulingIgnoredDuringExecution: [], preferredDuringSchedulingIgnoredDuringExecution: [] };
137
228
  const podAntiAffinity = { requiredDuringSchedulingIgnoredDuringExecution: [], preferredDuringSchedulingIgnoredDuringExecution: [] };
@@ -154,7 +245,7 @@ export default {
154
245
  }
155
246
  });
156
247
 
157
- Object.assign(this.value.affinity, { podAffinity, podAntiAffinity });
248
+ Object.assign(this.value[this.field], { podAffinity, podAntiAffinity });
158
249
  this.$emit('update', this.value);
159
250
  },
160
251
 
@@ -163,17 +254,6 @@ export default {
163
254
  this.queueUpdate();
164
255
  },
165
256
 
166
- addSelector() {
167
- const neu = {
168
- namespaces: null,
169
- labelSelector: { matchExpressions: [] },
170
- topologyKey: '',
171
- _id: randomStr(4)
172
- };
173
-
174
- this.allSelectorTerms.push(neu);
175
- },
176
-
177
257
  changePriority(term, idx) {
178
258
  if (term.weight) {
179
259
  delete term.weight;
@@ -189,14 +269,41 @@ export default {
189
269
  return term.weight ? this.t('workload.scheduling.affinity.preferred') : this.t('workload.scheduling.affinity.required');
190
270
  },
191
271
 
192
- changeNamespaceMode(term, idx) {
193
- if (term.namespaces) {
272
+ changeNamespaceMode(val, term, idx) {
273
+ this.$set(term, '_namespaceOption', val);
274
+
275
+ switch (val) {
276
+ case NAMESPACE_SELECTION_OPTION_VALUES.POD:
194
277
  term.namespaces = null;
195
278
  term._namespaces = null;
196
- } else {
279
+
280
+ if (term.namespaceSelector || term.namespaceSelector === null) {
281
+ delete term.namespaceSelector;
282
+ }
283
+ break;
284
+ case NAMESPACE_SELECTION_OPTION_VALUES.ALL:
285
+ term.namespaceSelector = {};
286
+
287
+ if (term.namespaces || term.namespaces === null) {
288
+ delete term.namespaces;
289
+ }
290
+
291
+ if (term._namespaces || term._namespaces === null) {
292
+ delete term._namespaces;
293
+ }
294
+ break;
295
+
296
+ default:
197
297
  this.$set(term, 'namespaces', []);
198
298
  this.$set(term, '_namespaces', '');
299
+
300
+ if (term.namespaceSelector || term.namespaceSelector === null) {
301
+ delete term.namespaceSelector;
302
+ }
303
+
304
+ break;
199
305
  }
306
+
200
307
  this.$set(this.allSelectorTerms, idx, term);
201
308
  this.queueUpdate();
202
309
  },
@@ -205,7 +312,7 @@ export default {
205
312
  let nsArray = namespaces;
206
313
 
207
314
  // namespaces would be String if there is no namespace
208
- if (!this.hasNamespaces) {
315
+ if (typeof namespaces === 'string') {
209
316
  nsArray = namespaces.split(',').map(ns => ns.trim()).filter(ns => ns?.length);
210
317
  }
211
318
 
@@ -231,9 +338,9 @@ export default {
231
338
  <ArrayListGrouped
232
339
  v-model="allSelectorTerms"
233
340
  class="mt-20"
234
- :default-add-value="{ matchExpressions: [] }"
341
+ :default-add-value="defaultAddValue"
235
342
  :mode="mode"
236
- :add-label="t('workload.scheduling.affinity.addNodeSelector')"
343
+ :add-label="t('podAffinity.addLabel')"
237
344
  @remove="remove"
238
345
  >
239
346
  <template #default="props">
@@ -244,6 +351,7 @@ export default {
244
351
  :options="[t('workload.scheduling.affinity.affinityOption'),t('workload.scheduling.affinity.antiAffinityOption')]"
245
352
  :value="props.row.value._anti ?t('workload.scheduling.affinity.antiAffinityOption') :t('workload.scheduling.affinity.affinityOption') "
246
353
  :label="t('workload.scheduling.affinity.type')"
354
+ :data-testid="`pod-affinity-type-index${props.i}`"
247
355
  @input="$set(props.row.value, '_anti',!props.row.value._anti)"
248
356
  />
249
357
  </div>
@@ -254,41 +362,44 @@ export default {
254
362
  :options="[t('workload.scheduling.affinity.preferred'),t('workload.scheduling.affinity.required')]"
255
363
  :value="priorityDisplay(props.row.value)"
256
364
  :label="t('workload.scheduling.affinity.priority')"
365
+ :data-testid="`pod-affinity-priority-index${props.i}`"
257
366
  @input="changePriority(props.row.value, props.i)"
258
367
  />
259
368
  </div>
260
369
  </div>
261
370
  <div class="row">
262
371
  <RadioGroup
263
- :options="[false, true]"
264
- :labels="[t('workload.scheduling.affinity.thisPodNamespace'),t('workload.scheduling.affinity.matchExpressions.inNamespaces'),]"
372
+ :options="namespaceSelectionOptions"
373
+ :labels="namespaceSelectionLabels"
265
374
  :name="`namespaces-${props.row.value._id}`"
266
375
  :mode="mode"
267
- :value="!!props.row.value.namespaces"
268
- @input="changeNamespaceMode(props.row.value, props.i)"
376
+ :value="props.row.value._namespaceOption"
377
+ :data-testid="`pod-affinity-namespacetype-index${props.i}`"
378
+ @input="changeNamespaceMode($event, props.row.value, props.i)"
269
379
  />
270
380
  </div>
271
- <div class="spacer" />
272
381
  <div
273
- v-if="!!props.row.value.namespaces || !!get(props.row.value, 'podAffinityTerm.namespaces')"
274
- class="row mb-20"
382
+ v-if="props.row.value._namespaceOption === NAMESPACE_SELECTION_OPTION_VALUES.SELECTED"
383
+ class="row mt-10 mb-20"
275
384
  >
276
385
  <LabeledSelect
277
- v-if="hasNamespaces"
386
+ v-if="hasNamespaces && !forceInputNamespaceSelection"
278
387
  v-model="props.row.value.namespaces"
279
388
  :mode="mode"
280
389
  :multiple="true"
281
390
  :taggable="true"
282
- :options="allNamespaces"
391
+ :options="allNamespacesOptions"
283
392
  :label="t('workload.scheduling.affinity.matchExpressions.inNamespaces')"
393
+ :data-testid="`pod-affinity-namespace-select-index${props.i}`"
284
394
  @input="updateNamespaces(props.row.value, props.row.value.namespaces)"
285
395
  />
286
396
  <LabeledInput
287
397
  v-else
288
398
  v-model="props.row.value._namespaces"
289
399
  :mode="mode"
290
- :label="t('workload.scheduling.affinity.matchExpressions.inNamespaces')"
400
+ :label="labeledInputNamespaceLabel"
291
401
  :placeholder="t('cluster.credential.harvester.affinity.namespaces.placeholder')"
402
+ :data-testid="`pod-affinity-namespace-input-index${props.i}`"
292
403
  @input="updateNamespaces(props.row.value, props.row.value._namespaces)"
293
404
  />
294
405
  </div>
@@ -299,11 +410,11 @@ export default {
299
410
  :type="pod"
300
411
  :value="get(props.row.value, 'labelSelector.matchExpressions')"
301
412
  :show-remove="false"
413
+ :data-testid="`pod-affinity-expressions-index${props.i}`"
302
414
  @input="e=>set(props.row.value, 'labelSelector.matchExpressions', e)"
303
415
  />
304
- <div class="spacer" />
305
- <div class="row">
306
- <div class="col span-12">
416
+ <div class="row mt-20">
417
+ <div class="col span-9">
307
418
  <LabeledSelect
308
419
  v-if="hasNodes"
309
420
  v-model="props.row.value.topologyKey"
@@ -317,6 +428,7 @@ export default {
317
428
  :options="existingNodeLabels"
318
429
  :disabled="mode==='view'"
319
430
  :loading="loading"
431
+ :data-testid="`pod-affinity-topology-select-index${props.i}`"
320
432
  @input="update"
321
433
  />
322
434
  <LabeledInput
@@ -326,14 +438,14 @@ export default {
326
438
  :label="t('workload.scheduling.affinity.topologyKey.label')"
327
439
  :placeholder="t('workload.scheduling.affinity.topologyKey.placeholder')"
328
440
  required
441
+ :data-testid="`pod-affinity-topology-input-index${props.i}`"
329
442
  @input="update"
330
443
  />
331
444
  </div>
332
- </div>
333
-
334
- <div class="spacer" />
335
- <div class="row">
336
- <div class="col span-6">
445
+ <div
446
+ v-if="props.row.value.weight"
447
+ class="col span-3"
448
+ >
337
449
  <LabeledInput
338
450
  v-model.number="props.row.value.weight"
339
451
  :mode="mode"
@@ -342,6 +454,7 @@ export default {
342
454
  max="100"
343
455
  :label="t('workload.scheduling.affinity.weight.label')"
344
456
  :placeholder="t('workload.scheduling.affinity.weight.placeholder')"
457
+ :data-testid="`pod-affinity-weight-index${props.i}`"
345
458
  />
346
459
  </div>
347
460
  </div>
@@ -115,7 +115,24 @@ export default {
115
115
  },
116
116
 
117
117
  update() {
118
- this.$emit('input', this.rules);
118
+ // let's delete the vKey prop as it's only poluting the data
119
+ const rules = this.rules.map((rule) => {
120
+ const newRule = { ...rule };
121
+
122
+ // prevent vKey from being sent as data
123
+ if (newRule.vKey) {
124
+ delete newRule.vKey;
125
+ }
126
+
127
+ // let's clear the value field if operator is Exists
128
+ if (newRule.operator === 'Exists' && newRule.value) {
129
+ newRule.value = null;
130
+ }
131
+
132
+ return newRule;
133
+ });
134
+
135
+ this.$emit('input', rules);
119
136
  },
120
137
 
121
138
  addToleration() {
@@ -126,6 +143,8 @@ export default {
126
143
  if (neu !== 'NoExecute' && rule.tolerationSeconds) {
127
144
  delete rule.tolerationSeconds;
128
145
  }
146
+
147
+ this.update();
129
148
  }
130
149
  }
131
150
 
@@ -146,7 +165,7 @@ export default {
146
165
  <span />
147
166
  </div>
148
167
  <div
149
- v-for="rule in rules"
168
+ v-for="(rule, index) in rules"
150
169
  :key="rule.vKey"
151
170
  class="rule"
152
171
  >
@@ -154,6 +173,9 @@ export default {
154
173
  <LabeledInput
155
174
  v-model="rule.key"
156
175
  :mode="mode"
176
+ :data-testid="`toleration-key-index${ index }`"
177
+ class="height-adjust-input"
178
+ @input="update"
157
179
  />
158
180
  </div>
159
181
  <div class="col">
@@ -162,6 +184,7 @@ export default {
162
184
  v-model="rule.operator"
163
185
  :options="operatorOpts"
164
186
  :mode="mode"
187
+ :data-testid="`toleration-operator-index${ index }`"
165
188
  @input="update"
166
189
  />
167
190
  </div>
@@ -171,6 +194,7 @@ export default {
171
194
  value="n/a"
172
195
  :mode="mode"
173
196
  disabled
197
+ class="height-adjust-input"
174
198
  />
175
199
  </div>
176
200
  </template>
@@ -179,6 +203,9 @@ export default {
179
203
  <LabeledInput
180
204
  v-model="rule.value"
181
205
  :mode="mode"
206
+ :data-testid="`toleration-value-index${ index }`"
207
+ class="height-adjust-input"
208
+ @input="update"
182
209
  />
183
210
  </div>
184
211
  </template>
@@ -187,6 +214,7 @@ export default {
187
214
  v-model="rule.effect"
188
215
  :options="effectOpts"
189
216
  :mode="mode"
217
+ :data-testid="`toleration-effect-index${ index }`"
190
218
  @input="e=>updateEffect(e, rule)"
191
219
  />
192
220
  </div>
@@ -196,6 +224,9 @@ export default {
196
224
  :disabled="rule.effect !== 'NoExecute'"
197
225
  :mode="mode"
198
226
  suffix="Seconds"
227
+ :data-testid="`toleration-seconds-index${ index }`"
228
+ class="height-adjust-input"
229
+ @input="update"
199
230
  />
200
231
  </div>
201
232
  <div class="col remove">
@@ -204,6 +235,7 @@ export default {
204
235
  type="button"
205
236
  class="btn role-link"
206
237
  :disabled="mode==='view'"
238
+ :data-testid="`toleration-remove-index${ index }`"
207
239
  @click="remove(rule)"
208
240
  >
209
241
  <t k="generic.remove" />
@@ -214,6 +246,7 @@ export default {
214
246
  v-if="!isView"
215
247
  type="button"
216
248
  class="btn role-tertiary"
249
+ data-testid="add-toleration-btn"
217
250
  @click="addToleration"
218
251
  >
219
252
  <t k="workload.scheduling.tolerations.addToleration" />
@@ -228,8 +261,8 @@ export default {
228
261
 
229
262
  .rule, .toleration-headers{
230
263
  display: grid;
231
- grid-template-columns: 20% 10% 20% 15% 20% 10%;
232
- grid-gap: $column-gutter;
264
+ grid-template-columns: 20% 10% 20% 15% 20% 15%;
265
+ grid-gap: 10px;
233
266
  align-items: center;
234
267
  }
235
268
 
@@ -247,4 +280,7 @@ export default {
247
280
  .remove BUTTON {
248
281
  padding: 0px;
249
282
  }
283
+ .height-adjust-input {
284
+ min-height: 42px;
285
+ }
250
286
  </style>
@@ -23,7 +23,7 @@ describe('the ArrayList', () => {
23
23
  initialEmptyRow: true
24
24
  },
25
25
  });
26
- const arrayListBoxes = wrapper.findAll('[data-testid="array-list-box"]');
26
+ const arrayListBoxes = wrapper.findAll('[data-testid^="array-list-box"]');
27
27
 
28
28
  expect(arrayListBoxes).toHaveLength(1);
29
29
  });
@@ -40,7 +40,7 @@ describe('the ArrayList', () => {
40
40
 
41
41
  await arrayListButton.click();
42
42
  await arrayListButton.click();
43
- const arrayListBoxes = wrapper.findAll('[data-testid="array-list-box"]');
43
+ const arrayListBoxes = wrapper.findAll('[data-testid^="array-list-box"]');
44
44
 
45
45
  expect(arrayListBoxes).toHaveLength(2);
46
46
  });
@@ -55,7 +55,7 @@ describe('the ArrayList', () => {
55
55
  const deleteButton = wrapper.get('[data-testid^="remove-item"]').element as HTMLElement;
56
56
 
57
57
  await deleteButton.click();
58
- const arrayListBoxes = wrapper.findAll('[data-testid="array-list-box"]');
58
+ const arrayListBoxes = wrapper.findAll('[data-testid^="array-list-box"]');
59
59
 
60
60
  expect(arrayListBoxes).toHaveLength(1);
61
61
  });
@@ -21,7 +21,7 @@ describe('component: MatchExpressions', () => {
21
21
 
22
22
  const inputWraps = wrapper.findAll('[data-testid^=input-match-expression-]');
23
23
 
24
- expect(inputWraps).toHaveLength(3);
24
+ expect(inputWraps).toHaveLength(8);
25
25
  });
26
26
 
27
27
  it.each([
@@ -621,6 +621,7 @@ export default {
621
621
  <div
622
622
  v-if="showUserMenu"
623
623
  class="user user-menu"
624
+ data-testid="nav_header_showUserMenu"
624
625
  tabindex="0"
625
626
  @blur="showMenu(false)"
626
627
  @click="showMenu(true)"
@@ -654,6 +655,7 @@ export default {
654
655
  >
655
656
  <ul
656
657
  class="list-unstyled dropdown"
658
+ data-testid="user-menu-dropdown"
657
659
  @click.stop="showMenu(false)"
658
660
  >
659
661
  <li
@@ -83,7 +83,12 @@ export const SETTING = {
83
83
  * both pre and post log in. If not present defaults to the usual process
84
84
  */
85
85
  THEME: 'ui-theme',
86
- SYSTEM_NAMESPACES: 'system-namespaces'
86
+ SYSTEM_NAMESPACES: 'system-namespaces',
87
+ /**
88
+ * Cluster Agent configuration
89
+ */
90
+ CLUSTER_AGENT_DEFAULT_AFFINITY: 'cluster-agent-default-affinity',
91
+ FLEET_AGENT_DEFAULT_AFFINITY: 'fleet-agent-default-affinity',
87
92
  };
88
93
 
89
94
  // These are the settings that are allowed to be edited via the UI
@@ -1,5 +1,4 @@
1
1
  import Vue from 'vue';
2
- import $ from 'jquery';
3
2
  import JSZip from 'jszip';
4
3
  import jsyaml from 'js-yaml';
5
4
 
@@ -26,7 +25,6 @@ export default function({
26
25
  window.Vue = Vue;
27
26
 
28
27
  // Global libraries - allows us to externalise these to reduce package bundle size
29
- window.$ = $;
30
28
  window.__jszip = JSZip;
31
29
  window.__jsyaml = jsyaml;
32
30
  }
@@ -31,40 +31,65 @@ export default {
31
31
  computed: {
32
32
  hasBinaryData() {
33
33
  return Object.keys(this.binaryData).length > 0;
34
- }
34
+ },
35
+ /**
36
+ * Keep all newlines from end, see: https://yaml-multiline.info
37
+ * Apply to 'data' field
38
+ */
39
+ yamlModifiers() {
40
+ return {
41
+ data: Object.keys(this.data).reduce((acc, key) => ({
42
+ ...acc,
43
+ [key]: { chomping: '+' },
44
+ }), {}),
45
+ };
46
+ },
47
+
48
+ validationPassed() {
49
+ return !!this.value.name;
50
+ },
35
51
  },
36
52
 
37
53
  watch: {
38
- data(neu, old) {
54
+ data(neu) {
39
55
  this.updateValue(neu, 'data');
40
56
  },
41
- binaryData(neu, old) {
57
+ binaryData(neu) {
42
58
  this.updateValue(neu, 'binaryData');
43
59
  },
44
60
  },
45
61
 
46
62
  methods: {
63
+ async saveConfigMap() {
64
+ const yaml = this.$refs.cru.createResourceYaml(this.yamlModifiers);
65
+
66
+ await this.value.saveYaml(yaml);
67
+ this.done();
68
+ },
69
+
47
70
  updateValue(val, type) {
48
71
  this.$set(this.value, type, {});
49
72
 
50
73
  Object.keys(val).forEach((key) => {
51
74
  this.$set(this.value[type], key, val[key]);
52
75
  });
53
- }
76
+ },
54
77
  }
55
78
  };
56
79
  </script>
57
80
 
58
81
  <template>
59
82
  <CruResource
83
+ ref="cru"
60
84
  :done-route="doneRoute"
61
85
  :mode="mode"
62
86
  :resource="value"
63
87
  :subtypes="[]"
64
- :validation-passed="true"
88
+ :validation-passed="validationPassed"
89
+ :yaml-modifiers="yamlModifiers"
65
90
  :errors="errors"
66
91
  @error="e=>errors = e"
67
- @finish="save"
92
+ @finish="saveConfigMap"
68
93
  @cancel="done"
69
94
  >
70
95
  <NameNsDescription
@@ -86,6 +111,8 @@ export default {
86
111
  :protip="t('configmap.tabs.data.protip')"
87
112
  :initial-empty-row="true"
88
113
  :value-can-be-empty="true"
114
+ :value-trim="false"
115
+ :value-markdown-multiline="true"
89
116
  :read-multiple="true"
90
117
  :read-accept="'*'"
91
118
  />