@rancher/shell 0.3.7 → 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 (48) hide show
  1. package/assets/translations/en-us.yaml +55 -14
  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/Inactivity.vue +229 -0
  9. package/components/YamlEditor.vue +2 -2
  10. package/components/form/ArrayList.vue +1 -1
  11. package/components/form/KeyValue.vue +34 -1
  12. package/components/form/MatchExpressions.vue +120 -21
  13. package/components/form/NodeAffinity.vue +54 -4
  14. package/components/form/PodAffinity.vue +160 -47
  15. package/components/form/Tolerations.vue +40 -4
  16. package/components/form/__tests__/ArrayList.test.ts +3 -3
  17. package/components/form/__tests__/MatchExpressions.test.ts +1 -1
  18. package/components/nav/Header.vue +2 -0
  19. package/config/settings.ts +10 -1
  20. package/core/plugins-loader.js +0 -2
  21. package/creators/app/files/.gitignore +73 -0
  22. package/creators/app/init +1 -0
  23. package/edit/configmap.vue +33 -6
  24. package/edit/provisioning.cattle.io.cluster/AgentConfiguration.vue +326 -0
  25. package/edit/provisioning.cattle.io.cluster/index.vue +1 -0
  26. package/edit/provisioning.cattle.io.cluster/rke2.vue +63 -15
  27. package/edit/workload/mixins/workload.js +12 -4
  28. package/layouts/blank.vue +4 -0
  29. package/layouts/default.vue +3 -0
  30. package/layouts/home.vue +4 -1
  31. package/layouts/plain.vue +4 -1
  32. package/mixins/chart.js +1 -1
  33. package/models/batch.cronjob.js +18 -3
  34. package/models/provisioning.cattle.io.cluster.js +24 -0
  35. package/models/workload.js +1 -1
  36. package/package.json +2 -3
  37. package/pages/auth/login.vue +1 -0
  38. package/pages/c/_cluster/explorer/index.vue +1 -4
  39. package/pages/c/_cluster/settings/performance.vue +61 -7
  40. package/pages/prefs.vue +18 -2
  41. package/pkg/vue.config.js +0 -1
  42. package/plugins/codemirror.js +158 -0
  43. package/public/index.html +1 -1
  44. package/store/index.js +36 -21
  45. package/types/shell/index.d.ts +20 -1
  46. package/utils/create-yaml.js +105 -8
  47. package/utils/settings.ts +12 -0
  48. package/vue.config.js +2 -2
@@ -0,0 +1,229 @@
1
+ <script>
2
+ import ModalWithCard from '@shell/components/ModalWithCard';
3
+ import { Banner } from '@components/Banner';
4
+ import PercentageBar from '@shell/components/PercentageBar.vue';
5
+ import throttle from 'lodash/throttle';
6
+ import { MANAGEMENT } from '@shell/config/types';
7
+ import { DEFAULT_PERF_SETTING, SETTING } from '@shell/config/settings';
8
+
9
+ export default {
10
+ name: 'Inactivity',
11
+ components: {
12
+ ModalWithCard, Banner, PercentageBar
13
+ },
14
+ data() {
15
+ return {
16
+ enabled: null,
17
+ isOpen: false,
18
+ isInactive: false,
19
+ showModalAfter: null,
20
+ inactivityTimeoutId: null,
21
+ courtesyTimer: null,
22
+ courtesyTimerId: null,
23
+ courtesyCountdown: null,
24
+ trackInactivity: throttle(this._trackInactivity, 1000),
25
+ };
26
+ },
27
+ async mounted() {
28
+ // Info: normally, this is done in the fetch hook but for some reasons while awaiting for things that will take a while, it won't be ready by the time mounted() is called, pending for investigation.
29
+ let settings;
30
+
31
+ try {
32
+ const settingsString = await this.$store.dispatch('management/find', { type: MANAGEMENT.SETTING, id: SETTING.UI_PERFORMANCE });
33
+
34
+ settings = settingsString?.value ? JSON.parse(settingsString.value) : DEFAULT_PERF_SETTING;
35
+ } catch { }
36
+
37
+ if (!settings || !settings?.inactivity || !settings?.inactivity.enabled) {
38
+ return;
39
+ }
40
+
41
+ this.enabled = settings?.inactivity?.enabled || false;
42
+
43
+ // Total amount of time before the user's session is lost
44
+ const thresholdToSeconds = settings?.inactivity?.threshold * 60;
45
+
46
+ // Amount of time the user sees the inactivity warning
47
+ this.courtesyTimer = Math.floor(thresholdToSeconds * 0.1);
48
+ this.courtesyTimer = Math.min(this.courtesyTimer, 60 * 5); // Never show the modal more than 5 minutes
49
+ // Amount of time before the user sees the inactivity warning
50
+ // Note - time before warning is shown + time warning is shown = settings threshold (total amount of time)
51
+ this.showModalAfter = thresholdToSeconds - this.courtesyTimer;
52
+
53
+ console.debug(`Inactivity modal will show after ${ this.showModalAfter / 60 }(m) and be shown for ${ this.courtesyTimer / 60 }(m)`); // eslint-disable-line no-console
54
+
55
+ this.courtesyCountdown = this.courtesyTimer;
56
+
57
+ if (settings?.inactivity.enabled) {
58
+ this.trackInactivity();
59
+ this.addIdleListeners();
60
+ }
61
+ },
62
+ beforeDestroy() {
63
+ this.removeEventListener();
64
+ this.clearAllTimeouts();
65
+ },
66
+ methods: {
67
+ _trackInactivity() {
68
+ if (this.isInactive || this.isOpen || !this.showModalAfter) {
69
+ return;
70
+ }
71
+
72
+ this.clearAllTimeouts();
73
+ const endTime = Date.now() + this.showModalAfter * 1000;
74
+
75
+ const checkInactivityTimer = () => {
76
+ const now = Date.now();
77
+
78
+ if (now >= endTime) {
79
+ this.isOpen = true;
80
+ this.startCountdown();
81
+
82
+ this.$modal.show('inactivityModal');
83
+ } else {
84
+ this.inactivityTimeoutId = setTimeout(checkInactivityTimer, 1000);
85
+ }
86
+ };
87
+
88
+ checkInactivityTimer();
89
+ },
90
+ startCountdown() {
91
+ const endTime = Date.now() + (this.courtesyCountdown * 1000);
92
+
93
+ const checkCountdown = () => {
94
+ const now = Date.now();
95
+
96
+ if (now >= endTime) {
97
+ this.isInactive = true;
98
+ this.unsubscribe();
99
+ this.clearAllTimeouts();
100
+ } else {
101
+ this.courtesyCountdown = Math.floor((endTime - now) / 1000);
102
+ this.courtesyTimerId = setTimeout(checkCountdown, 1000);
103
+ }
104
+ };
105
+
106
+ checkCountdown();
107
+ },
108
+ addIdleListeners() {
109
+ document.addEventListener('mousemove', this.trackInactivity);
110
+ document.addEventListener('mousedown', this.trackInactivity);
111
+ document.addEventListener('keypress', this.trackInactivity);
112
+ document.addEventListener('touchmove', this.trackInactivity);
113
+ document.addEventListener('visibilitychange', this.trackInactivity);
114
+ },
115
+ removeEventListener() {
116
+ document.removeEventListener('mousemove', this.trackInactivity);
117
+ document.removeEventListener('mousedown', this.trackInactivity);
118
+ document.removeEventListener('keypress', this.trackInactivity);
119
+ document.removeEventListener('touchmove', this.trackInactivity);
120
+ document.removeEventListener('visibilitychange', this.trackInactivity);
121
+ },
122
+
123
+ resume() {
124
+ this.isInactive = false;
125
+ this.isOpen = false;
126
+ this.courtesyCountdown = this.courtesyTimer;
127
+ this.clearAllTimeouts();
128
+
129
+ this.$modal.hide('inactivityModal');
130
+ },
131
+
132
+ refresh() {
133
+ window.location.reload();
134
+ },
135
+
136
+ unsubscribe() {
137
+ this.$store.dispatch('unsubscribe');
138
+ },
139
+ clearAllTimeouts() {
140
+ clearTimeout(this.inactivityTimeoutId);
141
+ clearTimeout(this.courtesyTimerId);
142
+ }
143
+
144
+ },
145
+ computed: {
146
+ isInactiveTexts() {
147
+ return this.isInactive ? {
148
+ title: this.t('inactivity.titleExpired'),
149
+ banner: this.t('inactivity.bannerExpired'),
150
+ content: this.t('inactivity.contentExpired'),
151
+ } : {
152
+ title: this.t('inactivity.title'),
153
+ banner: this.t('inactivity.banner'),
154
+ content: this.t('inactivity.content'),
155
+ };
156
+ },
157
+ timerPercentageLeft() {
158
+ return Math.floor((this.courtesyCountdown / this.courtesyTimer ) * 100);
159
+ },
160
+ colorStops() {
161
+ return {
162
+ 0: '--info', 30: '--info', 70: '--info'
163
+ };
164
+ },
165
+ }
166
+ };
167
+ </script>
168
+
169
+ <template>
170
+ <ModalWithCard
171
+ ref="inactivityModal"
172
+ name="inactivityModal"
173
+ save-text="Continue"
174
+ :v-if="isOpen"
175
+ @finish="resume"
176
+ >
177
+ <template #title>
178
+ {{ isInactiveTexts.title }}
179
+ </template>
180
+ <span>{{ courtesyCountdown }}</span>
181
+
182
+ <template #content>
183
+ <Banner color="info">
184
+ {{ isInactiveTexts.banner }}
185
+ </Banner>
186
+
187
+ <p>
188
+ {{ isInactiveTexts.content }}
189
+ </p>
190
+
191
+ <PercentageBar
192
+ v-if="!isInactive"
193
+ class="mt-20"
194
+ :value="timerPercentageLeft"
195
+ :color-stops="colorStops"
196
+ />
197
+ </template>
198
+
199
+ <template
200
+ #footer
201
+ >
202
+ <div class="card-actions">
203
+ <button
204
+ v-if="!isInactive"
205
+ class="btn role-tertiary bg-primary"
206
+ @click.prevent="resume"
207
+ >
208
+ <t k="inactivity.cta" />
209
+ </button>
210
+
211
+ <button
212
+ v-if="isInactive"
213
+ class="btn role-tertiary bg-primary"
214
+ @click.prevent="refresh"
215
+ >
216
+ <t k="inactivity.ctaExpired" />
217
+ </button>
218
+ </div>
219
+ </template>
220
+ </ModalWithCard>
221
+ </template>
222
+
223
+ <style lang="scss" scoped>
224
+ .card-actions {
225
+ display: flex;
226
+ width: 100%;
227
+ justify-content: flex-end;
228
+ }
229
+ </style>
@@ -86,7 +86,7 @@ export default {
86
86
  },
87
87
 
88
88
  computed: {
89
- cmOptions() {
89
+ codeMirrorOptions() {
90
90
  const readOnly = this.editorMode === EDITOR_MODES.VIEW_CODE;
91
91
 
92
92
  const gutters = [];
@@ -228,7 +228,7 @@ export default {
228
228
  ref="cm"
229
229
  :class="{fill: true, scrolling: scrolling}"
230
230
  :value="curValue"
231
- :options="cmOptions"
231
+ :options="codeMirrorOptions"
232
232
  :data-testid="componentTestid + '-code-mirror'"
233
233
  @onInput="onInput"
234
234
  @onReady="onReady"
@@ -224,7 +224,7 @@ export default {
224
224
  <div
225
225
  v-for="(row, idx) in rows"
226
226
  :key="idx"
227
- data-testid="array-list-box"
227
+ :data-testid="`array-list-box${ idx }`"
228
228
  class="box"
229
229
  >
230
230
  <slot
@@ -10,11 +10,13 @@ import Select from '@shell/components/form/Select';
10
10
  import FileSelector from '@shell/components/form/FileSelector';
11
11
  import { _EDIT, _VIEW } from '@shell/config/query-params';
12
12
  import { asciiLike } from '@shell/utils/string';
13
+ import CodeMirror from '@shell/components/CodeMirror';
13
14
 
14
15
  export default {
15
16
  name: 'KeyValue',
16
17
 
17
18
  components: {
19
+ CodeMirror,
18
20
  Select,
19
21
  TextAreaAutoGrow,
20
22
  FileSelector
@@ -139,6 +141,10 @@ export default {
139
141
  type: Boolean,
140
142
  default: false,
141
143
  },
144
+ valueMarkdownMultiline: {
145
+ type: Boolean,
146
+ default: false,
147
+ },
142
148
  valueMultiline: {
143
149
  type: Boolean,
144
150
  default: true,
@@ -245,7 +251,10 @@ export default {
245
251
  data() {
246
252
  const rows = this.getRows(this.value);
247
253
 
248
- return { rows };
254
+ return {
255
+ rows,
256
+ codeMirrorFocus: {},
257
+ };
249
258
  },
250
259
 
251
260
  computed: {
@@ -519,6 +528,19 @@ export default {
519
528
  return this.t('detailText.binary', { n }, true);
520
529
  },
521
530
  get,
531
+ /**
532
+ * Update 'rows' variable with the user's input and prevents to update queue before the row model is updated
533
+ */
534
+ onInputMarkdownMultiline(idx, value) {
535
+ this.rows = this.rows.map((row, i) => i === idx ? { ...row, value } : row);
536
+ this.queueUpdate();
537
+ },
538
+ /**
539
+ * Set focus on CodeMirror fields
540
+ */
541
+ onFocusMarkdownMultiline(idx, value) {
542
+ this.$set(this.codeMirrorFocus, idx, value);
543
+ }
522
544
  }
523
545
  };
524
546
  </script>
@@ -635,6 +657,16 @@ export default {
635
657
  <div v-else-if="row.binary">
636
658
  {{ binaryTextSize(row.value) }}
637
659
  </div>
660
+ <CodeMirror
661
+ v-else-if="valueMarkdownMultiline"
662
+ ref="cm"
663
+ :class="{['focus']: codeMirrorFocus[i]}"
664
+ :value="row[valueName]"
665
+ :as-text-area="true"
666
+ :mode="mode"
667
+ @onInput="onInputMarkdownMultiline(i, $event)"
668
+ @onFocus="onFocusMarkdownMultiline(i, $event)"
669
+ />
638
670
  <TextAreaAutoGrow
639
671
  v-else-if="valueMultiline"
640
672
  v-model="row[valueName]"
@@ -750,6 +782,7 @@ export default {
750
782
  &.value textarea{
751
783
  padding: 10px 10px 10px 10px;
752
784
  }
785
+
753
786
  .text-monospace:not(.conceal) {
754
787
  font-family: monospace, monospace;
755
788
  }
@@ -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>