@rancher/shell 3.0.1 → 3.0.2-rc.2

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 (98) hide show
  1. package/assets/styles/base/_basic.scss +17 -5
  2. package/assets/styles/base/_mixins.scss +2 -1
  3. package/assets/styles/global/_button.scss +10 -0
  4. package/assets/styles/global/_form.scss +2 -2
  5. package/assets/translations/en-us.yaml +33 -5
  6. package/assets/translations/zh-hans.yaml +1 -1
  7. package/components/ActionMenu.vue +8 -0
  8. package/components/AsyncButton.vue +9 -3
  9. package/components/BannerGraphic.vue +10 -0
  10. package/components/ButtonGroup.vue +2 -0
  11. package/components/ButtonMultiAction.vue +6 -0
  12. package/components/ClusterIconMenu.vue +1 -1
  13. package/components/CodeMirror.vue +28 -1
  14. package/components/CommunityLinks.vue +13 -0
  15. package/components/CruResource.vue +6 -0
  16. package/components/GrowlManager.vue +14 -4
  17. package/components/LocaleSelector.vue +49 -5
  18. package/components/PaginatedResourceTable.vue +4 -3
  19. package/components/ResourceDetail/Masthead.vue +11 -4
  20. package/components/ResourceList/index.vue +5 -3
  21. package/components/ResourceTable.vue +1 -1
  22. package/components/SortableTable/THead.vue +19 -4
  23. package/components/SortableTable/index.vue +13 -9
  24. package/components/SortableTable/selection.js +19 -5
  25. package/components/YamlEditor.vue +2 -1
  26. package/components/auth/SelectPrincipal.vue +1 -1
  27. package/components/fleet/FleetBundles.vue +2 -1
  28. package/components/form/LabeledSelect.vue +20 -7
  29. package/components/form/NodeScheduling.vue +5 -1
  30. package/components/form/Password.vue +23 -13
  31. package/components/form/ResourceLabeledSelect.vue +1 -1
  32. package/components/form/Select.vue +28 -6
  33. package/components/form/SelectOrCreateAuthSecret.vue +39 -11
  34. package/components/form/__tests__/NodeScheduling.test.ts +44 -0
  35. package/components/formatter/Endpoints.vue +1 -1
  36. package/components/formatter/LiveExpiryDate.vue +5 -1
  37. package/components/formatter/ServiceTargets.vue +1 -1
  38. package/components/formatter/ServiceType.vue +19 -17
  39. package/components/nav/Pinned.vue +6 -1
  40. package/components/nav/TopLevelMenu.helper.ts +17 -1
  41. package/components/nav/TopLevelMenu.vue +154 -19
  42. package/config/pagination-table-headers.js +9 -1
  43. package/config/product/apps.js +63 -30
  44. package/config/product/explorer.js +182 -17
  45. package/config/product/settings.js +9 -1
  46. package/config/router/routes.js +0 -1
  47. package/config/settings.ts +20 -2
  48. package/config/table-headers.js +23 -15
  49. package/config/types.js +2 -1
  50. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +12 -3
  51. package/edit/__tests__/management.cattle.io.setting.test.ts +37 -18
  52. package/edit/fleet.cattle.io.gitrepo.vue +40 -33
  53. package/edit/management.cattle.io.setting.vue +2 -0
  54. package/edit/provisioning.cattle.io.cluster/rke2.vue +13 -2
  55. package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +10 -2
  56. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +8 -2
  57. package/edit/provisioning.cattle.io.cluster/tabs/registries/__tests__/RegistryConfigs.test.ts +6 -3
  58. package/edit/workload/mixins/workload.js +15 -7
  59. package/list/catalog.cattle.io.app.vue +4 -11
  60. package/list/catalog.cattle.io.clusterrepo.vue +59 -25
  61. package/list/fleet.cattle.io.bundle.vue +2 -2
  62. package/list/management.cattle.io.feature.vue +12 -5
  63. package/list/management.cattle.io.setting.vue +30 -19
  64. package/list/namespace.vue +4 -1
  65. package/list/networking.k8s.io.ingress.vue +14 -11
  66. package/list/node.vue +65 -63
  67. package/list/persistentvolume.vue +55 -20
  68. package/list/persistentvolumeclaim.vue +3 -15
  69. package/list/service.vue +16 -21
  70. package/list/workload.vue +35 -49
  71. package/mixins/resource-fetch.js +8 -1
  72. package/mixins/vue-select-overrides.js +10 -16
  73. package/models/management.cattle.io.cluster.js +6 -1
  74. package/models/persistentvolume.js +1 -3
  75. package/models/storage.k8s.io.storageclass.js +4 -0
  76. package/package.json +29 -29
  77. package/pages/c/_cluster/explorer/EventsTable.vue +58 -16
  78. package/pages/c/_cluster/explorer/index.vue +3 -16
  79. package/pages/c/_cluster/settings/performance.vue +49 -23
  80. package/pages/home.vue +24 -3
  81. package/pages/support/index.vue +1 -1
  82. package/plugins/floating-vue.js +1 -1
  83. package/plugins/steve/steve-pagination-utils.ts +85 -15
  84. package/rancher-components/Banner/Banner.vue +12 -0
  85. package/rancher-components/Form/Checkbox/Checkbox.vue +27 -5
  86. package/rancher-components/Form/Radio/RadioButton.vue +0 -6
  87. package/rancher-components/Form/Radio/RadioGroup.vue +5 -1
  88. package/scripts/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml +2 -2
  89. package/scripts/test-plugins-build.sh +21 -6
  90. package/scripts/typegen.sh +1 -0
  91. package/store/index.js +16 -0
  92. package/store/type-map.utils.ts +14 -1
  93. package/types/shell/index.d.ts +467 -418
  94. package/types/store/vuex.d.ts +1 -1
  95. package/types/vue-shim.d.ts +2 -8
  96. package/utils/cluster.js +2 -2
  97. package/utils/string.js +6 -0
  98. package/vue.config.js +3 -4
@@ -54,8 +54,10 @@ export default {
54
54
  this.customTypeDisplay = component.typeDisplay.apply(this);
55
55
  }
56
56
 
57
- // If your list page has a fetch then it's responsible for populating rows itself
58
- if ( component?.fetch ) {
57
+ // Is the custom component responsible fetching the resources?
58
+ // - Component has a fetch method - legacy method. fetch will handle the requests
59
+ // - Component contains the PaginatedResourceTable component - go forward method. PaginatedResourceTable owns fetching the resources
60
+ if ( component?.fetch || component?.components?.['PaginatedResourceTable']) {
59
61
  this.componentWillFetch = true;
60
62
  }
61
63
 
@@ -269,7 +271,7 @@ export default {
269
271
  v-else
270
272
  :schema="schema"
271
273
  :rows="rows"
272
- :alt-loading="canPaginate"
274
+ :alt-loading="canPaginate && !isFirstLoad"
273
275
  :loading="loading"
274
276
  :headers="headers"
275
277
  :group-by="groupBy"
@@ -167,7 +167,7 @@ export default {
167
167
  default: false
168
168
  },
169
169
  /**
170
- * Manaul force the update of live and delayed cells. Change this number to kick off the update
170
+ * Manual force the update of live and delayed cells. Change this number to kick off the update
171
171
  */
172
172
  forceUpdateLiveAndDelayed: {
173
173
  type: Number,
@@ -162,6 +162,14 @@ export default {
162
162
  return col.name === this.sortBy;
163
163
  },
164
164
 
165
+ ariaSort(col) {
166
+ if (this.isCurrent(col)) {
167
+ return this.descending ? this.t('generic.descending') : this.t('generic.ascending');
168
+ }
169
+
170
+ return this.t('generic.none');
171
+ },
172
+
165
173
  tableColsOptionsClick(ev) {
166
174
  // set menu position
167
175
  const menu = document.querySelector('.table-options-container');
@@ -235,7 +243,12 @@ export default {
235
243
  :align="col.align || 'left'"
236
244
  :width="col.width"
237
245
  :class="{ sortable: col.sort, [col.breakpoint]: !!col.breakpoint}"
246
+ :tabindex="col.sort ? 0 : -1"
247
+ class="sortable-table-head-element"
248
+ :aria-sort="ariaSort(col)"
238
249
  @click.prevent="changeSort($event, col)"
250
+ @keyup.enter="changeSort($event, col)"
251
+ @keyup.space="changeSort($event, col)"
239
252
  >
240
253
  <div
241
254
  class="table-header-container"
@@ -415,10 +428,7 @@ export default {
415
428
  background-color: var(--sortable-table-header-bg);
416
429
  color: var(--body-text);
417
430
  text-align: left;
418
-
419
- &:not(.loading) {
420
- border-bottom: 1px solid var(--sortable-table-top-divider);
421
- }
431
+ border-bottom: 1px solid var(--sortable-table-top-divider);
422
432
  }
423
433
  }
424
434
 
@@ -428,6 +438,11 @@ export default {
428
438
  border: 0;
429
439
  color: var(--body-text);
430
440
 
441
+ &.sortable-table-head-element:focus-visible {
442
+ @include focus-outline;
443
+ outline-offset: -4px;
444
+ }
445
+
431
446
  .table-header-container {
432
447
  display: inline-flex;
433
448
 
@@ -380,8 +380,10 @@ export default {
380
380
  eventualSearchQuery = this.$route.query?.q;
381
381
  }
382
382
 
383
+ const isLoading = this.loading || false;
384
+
383
385
  return {
384
- refreshButtonPhase: ASYNC_BUTTON_STATES.WAITING,
386
+ refreshButtonPhase: isLoading ? ASYNC_BUTTON_STATES.WAITING : ASYNC_BUTTON_STATES.ACTION,
385
387
  expanded: {},
386
388
  searchQuery,
387
389
  eventualSearchQuery,
@@ -392,7 +394,7 @@ export default {
392
394
  /**
393
395
  * The is the bool the DOM uses to show loading state. it's proxied from `loading` to avoid blipping the indicator (see usages)
394
396
  */
395
- isLoading: false,
397
+ isLoading
396
398
  };
397
399
  },
398
400
 
@@ -504,7 +506,7 @@ export default {
504
506
  if (neu) {
505
507
  this._altLoadingDelayTimer = setTimeout(() => {
506
508
  this.isLoading = true;
507
- }, 200); // this should be higher than the targetted quick response
509
+ }, 200); // this should be higher than the targeted quick response
508
510
  } else {
509
511
  clearTimeout(this._altLoadingDelayTimer);
510
512
  this.isLoading = false;
@@ -536,9 +538,6 @@ export default {
536
538
  manualRefreshLoadingFinished() {
537
539
  const res = !!(!this.isLoading && this._didinit && this.rows?.length && !this.isManualRefreshLoading);
538
540
 
539
- // Always ensure the Refresh button phase aligns with loading state (regardless of if manualRefreshLoadingFinished has changed or not)
540
- this.refreshButtonPhase = !res || this.loading ? ASYNC_BUTTON_STATES.WAITING : ASYNC_BUTTON_STATES.ACTION;
541
-
542
541
  return res;
543
542
  },
544
543
 
@@ -575,11 +574,13 @@ export default {
575
574
  },
576
575
 
577
576
  showHeaderRow() {
577
+ // All of these are used to show content in the header
578
578
  return this.search ||
579
579
  this.tableActions ||
580
- this.$slots['header-left']?.() ||
581
- this.$slots['header-middle']?.() ||
582
- this.$slots['header-right']?.();
580
+ this.$slots['header-left'] ||
581
+ this.$slots['header-middle'] ||
582
+ this.$slots['header-right'] ||
583
+ this.isTooManyItemsToAutoUpdate;
583
584
  },
584
585
 
585
586
  columns() {
@@ -1222,6 +1223,7 @@ export default {
1222
1223
  class="sortable-table"
1223
1224
  :class="classObject"
1224
1225
  width="100%"
1226
+ role="table"
1225
1227
  >
1226
1228
  <THead
1227
1229
  v-if="showHeaders"
@@ -1451,6 +1453,8 @@ export default {
1451
1453
  :data-testid="componentTestid + '-' + i + '-action-button'"
1452
1454
  :borderless="true"
1453
1455
  @click="handleActionButtonClick(i, $event)"
1456
+ @keyup.enter="handleActionButtonClick(i, $event)"
1457
+ @keyup.space="handleActionButtonClick(i, $event)"
1454
1458
  />
1455
1459
  </slot>
1456
1460
  </td>
@@ -1,3 +1,4 @@
1
+ import { mapGetters } from 'vuex';
1
2
  import { isMore, isRange, suppressContextMenu, isAlternate } from '@shell/utils/platform';
2
3
  import { get } from '@shell/utils/object';
3
4
  import { filterBy } from '@shell/utils/array';
@@ -29,6 +30,13 @@ export default {
29
30
  },
30
31
 
31
32
  computed: {
33
+ ...mapGetters({
34
+ // Use either these Vuex getters
35
+ // OR the props to set the action menu state,
36
+ // but don't use both.
37
+ targetElem: 'action-menu/elem',
38
+ shouldShow: 'action-menu/showing',
39
+ }),
32
40
  // Used for the table-level selection check-box to show checked (all selected)/intermediate (some selected)/unchecked (none selected)
33
41
  howMuchSelected() {
34
42
  const total = this.pagedRows.length;
@@ -270,11 +278,17 @@ export default {
270
278
  }
271
279
  }
272
280
 
273
- this.$store.commit(`action-menu/show`, {
274
- resources,
275
- event: e,
276
- elem: actionElement
277
- });
281
+ if (!this.targetElem && !this.shouldShow) {
282
+ this.$store.commit(`action-menu/show`, {
283
+ resources,
284
+ event: e,
285
+ elem: actionElement
286
+ });
287
+ } else if (this.targetElem === actionElement && this.shouldShow) {
288
+ // this condition is needed so that we can "toggle" the action menu with
289
+ // the keyboard for accessibility (row action menu)
290
+ this.$store.commit('action-menu/hide');
291
+ }
278
292
 
279
293
  return;
280
294
  }
@@ -13,7 +13,7 @@ export const EDITOR_MODES = {
13
13
  };
14
14
 
15
15
  export default {
16
- emits: ['update:value', 'newObject', 'onInput', 'onReady', 'onChanges'],
16
+ emits: ['update:value', 'newObject', 'onInput', 'onReady', 'onChanges', 'validationChanged'],
17
17
 
18
18
  components: {
19
19
  CodeMirror,
@@ -236,6 +236,7 @@ export default {
236
236
  @onInput="onInput"
237
237
  @onReady="onReady"
238
238
  @onChanges="onChanges"
239
+ @validationChanged="$emit('validationChanged', $event)"
239
240
  />
240
241
  <FileDiff
241
242
  v-else
@@ -127,7 +127,7 @@ export default {
127
127
  }
128
128
  },
129
129
 
130
- onSearch(str, loading, vm) {
130
+ onSearch(str, loading) {
131
131
  str = (str || '').trim();
132
132
 
133
133
  this.searchStr = str;
@@ -127,7 +127,8 @@ export default {
127
127
  class="text-warning"
128
128
  >
129
129
  {{ row.status.summary.ready }}/{{ row.status.summary.desiredReady }}</span>
130
- <span v-else-if="row.status">{{ row.status.summary.desiredReady }}</span>
130
+ <span v-else-if="row.status && row.status.summary">{{ row.status.summary.desiredReady }}</span>
131
+ <span v-else>-</span>
131
132
  </template>
132
133
  </ResourceTable>
133
134
  </div>
@@ -22,7 +22,7 @@ export default {
22
22
  LabeledSelectPagination
23
23
  ],
24
24
 
25
- emits: ['on-open', 'on-close', 'selecting', 'deselecting', 'update:validation', 'update:value'],
25
+ emits: ['on-open', 'on-close', 'selecting', 'deselecting', 'search', 'update:validation', 'update:value'],
26
26
 
27
27
  props: {
28
28
  appendToBody: {
@@ -147,18 +147,29 @@ export default {
147
147
 
148
148
  // update placeholder text to inform user they can add their own opts when none are found
149
149
  showTagPrompts() {
150
- return !this.options.length && this.$attrs.taggable;
150
+ return !this.options.length && this.$attrs.taggable && this.isSearchable;
151
151
  }
152
152
  },
153
153
 
154
154
  methods: {
155
155
  // resizeHandler = in mixin
156
156
  focusSearch() {
157
- const blurredAgo = Date.now() - this.blurred;
157
+ // we need this override as in a "closeOnSelect" type of component
158
+ // if we don't have this override, it would open again
159
+ if (this.overridesMixinPreventDoubleTriggerKeysOpen) {
160
+ this.$nextTick(() => {
161
+ const el = this.$refs['select'];
162
+
163
+ if ( el ) {
164
+ el.focus();
165
+ }
166
+
167
+ this.overridesMixinPreventDoubleTriggerKeysOpen = false;
168
+ });
158
169
 
159
- if (!this.focused && blurredAgo < 250) {
160
170
  return;
161
171
  }
172
+ this.$refs['select-input'].open = true;
162
173
 
163
174
  this.$nextTick(() => {
164
175
  const el = this.$refs['select-input']?.searchEl;
@@ -238,7 +249,7 @@ export default {
238
249
  return noDrop ? false : open && shouldOpen && !mutableLoading;
239
250
  },
240
251
 
241
- onSearch(newSearchString) {
252
+ onSearch(newSearchString, loading) {
242
253
  if (this.canPaginate) {
243
254
  this.setPaginationFilter(newSearchString);
244
255
  } else {
@@ -246,6 +257,7 @@ export default {
246
257
  this.dropdownShouldOpen(this.$refs['select-input'], true);
247
258
  }
248
259
  }
260
+ this.$emit('search', newSearchString, loading);
249
261
  },
250
262
 
251
263
  getOptionKey(opt) {
@@ -277,8 +289,9 @@ export default {
277
289
  'no-label': !hasLabel
278
290
  }
279
291
  ]"
292
+ :tabindex="isView || disabled ? -1 : 0"
280
293
  @click="focusSearch"
281
- @focus="focusSearch"
294
+ @keyup.enter.space.down="focusSearch"
282
295
  >
283
296
  <div
284
297
  :class="{ 'labeled-container': true, raised, empty, [mode]: true }"
@@ -318,7 +331,7 @@ export default {
318
331
  :selectable="selectable"
319
332
  :modelValue="value != null && !loading ? value : ''"
320
333
  :dropdown-should-open="dropdownShouldOpen"
321
-
334
+ :tabindex="-1"
322
335
  @update:modelValue="$emit('selecting', $event); $emit('update:value', $event)"
323
336
  @search:blur="onBlur"
324
337
  @search:focus="onFocus"
@@ -32,6 +32,7 @@ export default {
32
32
  type: String,
33
33
  default: 'create'
34
34
  },
35
+
35
36
  loading: {
36
37
  default: false,
37
38
  type: Boolean
@@ -169,6 +170,7 @@ export default {
169
170
  name="selectNode"
170
171
  :options="selectNodeOptions"
171
172
  :mode="mode"
173
+ :data-testid="'node-scheduling-selectNode'"
172
174
  @input="update"
173
175
  />
174
176
  </div>
@@ -182,7 +184,8 @@ export default {
182
184
  :mode="mode"
183
185
  :multiple="false"
184
186
  :loading="loading"
185
- @input="update"
187
+ :data-testid="'node-scheduling-nodeSelector'"
188
+ @update:value="update"
186
189
  />
187
190
  </div>
188
191
  </div>
@@ -191,6 +194,7 @@ export default {
191
194
  <NodeAffinity
192
195
  v-model:value="nodeAffinity"
193
196
  :mode="mode"
197
+ :data-testid="'node-scheduling-nodeAffinity'"
194
198
  @input="update"
195
199
  />
196
200
  </template>
@@ -41,7 +41,7 @@ export default {
41
41
  mode: {
42
42
  type: String,
43
43
  default: _CREATE,
44
- },
44
+ }
45
45
  },
46
46
  data() {
47
47
  return { reveal: false };
@@ -68,6 +68,9 @@ export default {
68
68
  }
69
69
 
70
70
  return attributes;
71
+ },
72
+ hideShowLabel() {
73
+ return this.reveal ? this.t('action.hide') : this.t('action.show');
71
74
  }
72
75
  },
73
76
  watch: {
@@ -92,6 +95,9 @@ export default {
92
95
  },
93
96
  focus() {
94
97
  this.$refs.input.$refs.value.focus();
98
+ },
99
+ hideShowFn() {
100
+ this.reveal ? this.reveal = false : this.reveal = true;
95
101
  }
96
102
  }
97
103
  };
@@ -127,17 +133,15 @@ export default {
127
133
  class="addon"
128
134
  >
129
135
  <a
130
- v-if="reveal"
131
- tabindex="-1"
132
136
  href="#"
133
- @click.prevent.stop="reveal = false"
134
- >{{ t('action.hide') }}</a>
135
- <a
136
- v-else
137
- tabindex="-1"
138
- href="#"
139
- @click.prevent.stop="reveal=true"
140
- >{{ t('action.show') }}</a>
137
+ tabindex="0"
138
+ class="hide-show"
139
+ role="button"
140
+ @click.prevent.stop="hideShowFn"
141
+ @keyup.space.prevent.stop="hideShowFn"
142
+ >
143
+ {{ hideShowLabel }}
144
+ </a>
141
145
  </div>
142
146
  </template>
143
147
  </LabeledInput>
@@ -157,10 +161,16 @@ export default {
157
161
  .password {
158
162
  display: flex;
159
163
  flex-direction: column;
164
+
160
165
  .labeled-input {
161
166
  .addon {
162
- padding-left: 12px;
163
- min-width: 65px;
167
+ padding-left: 12px;
168
+ min-width: 65px;
169
+
170
+ .hide-show:focus-visible {
171
+ @include focus-outline;
172
+ outline-offset: 4px;
173
+ }
164
174
  }
165
175
  }
166
176
  .genPassword {
@@ -27,7 +27,7 @@ export interface ResourceLabeledSelectPaginateSettings extends SharedSettings {
27
27
  */
28
28
  overrideRequest?: LabelSelectPaginateFn,
29
29
  /**
30
- * Override the default settings used in the convience function to fetch a page of results
30
+ * Override the default settings used in the convenience function to fetch a page of results
31
31
  */
32
32
  requestSettings?: PaginateTypeOverridesFn,
33
33
  }
@@ -114,11 +114,24 @@ export default {
114
114
  calculatePosition(dropdownList, component, width, this.placement);
115
115
  },
116
116
 
117
- focus() {
118
- this.focusSearch();
119
- },
120
-
121
117
  focusSearch() {
118
+ // we need this override as in a "closeOnSelect" type of component
119
+ // if we don't have this override, it would open again
120
+ if (this.overridesMixinPreventDoubleTriggerKeysOpen) {
121
+ this.$nextTick(() => {
122
+ const el = this.$refs['select'];
123
+
124
+ if ( el ) {
125
+ el.focus();
126
+ }
127
+
128
+ this.overridesMixinPreventDoubleTriggerKeysOpen = false;
129
+ });
130
+
131
+ return;
132
+ }
133
+ this.$refs['select-input'].open = true;
134
+
122
135
  this.$nextTick(() => {
123
136
  const el = this.$refs['select-input']?.searchEl;
124
137
 
@@ -176,6 +189,11 @@ export default {
176
189
  },
177
190
  report(e) {
178
191
  alert(e);
192
+ },
193
+ handleDropdownOpen(args) {
194
+ // function that prevents the "opening dropdown on focus"
195
+ // default behaviour of v-select
196
+ return args.noDrop || args.disabled ? false : args.open;
179
197
  }
180
198
  },
181
199
  computed: {
@@ -227,7 +245,7 @@ export default {
227
245
  ref="select"
228
246
  class="unlabeled-select"
229
247
  :class="{
230
- disabled: disabled && !isView,
248
+ disabled: disabled || isView,
231
249
  focused,
232
250
  [mode]: true,
233
251
  [status]: status,
@@ -236,7 +254,9 @@ export default {
236
254
  'compact-input': compact,
237
255
  [$attrs.class]: $attrs.class
238
256
  }"
239
- @focus="focusSearch"
257
+ :tabindex="disabled || isView ? -1 : 0"
258
+ @click="focusSearch"
259
+ @keyup.enter.space.down="focusSearch"
240
260
  >
241
261
  <v-select
242
262
  ref="select-input"
@@ -258,6 +278,8 @@ export default {
258
278
  :searchable="isSearchable"
259
279
  :selectable="selectable"
260
280
  :modelValue="value != null ? value : ''"
281
+ :dropdownShouldOpen="handleDropdownOpen"
282
+ :tabindex="-1"
261
283
 
262
284
  @update:modelValue="$emit('update:value', $event)"
263
285
  @search:blur="onBlur"
@@ -1,5 +1,6 @@
1
1
  <script>
2
2
  import { _EDIT } from '@shell/config/query-params';
3
+ import { Banner } from '@components/Banner';
3
4
  import { LabeledInput } from '@components/Form/LabeledInput';
4
5
  import LabeledSelect from '@shell/components/form/LabeledSelect';
5
6
  import { AUTH_TYPE, NORMAN, SECRET } from '@shell/config/types';
@@ -19,6 +20,7 @@ export default {
19
20
  emits: ['inputauthval', 'update:value'],
20
21
 
21
22
  components: {
23
+ Banner,
22
24
  LabeledInput,
23
25
  LabeledSelect,
24
26
  },
@@ -118,7 +120,7 @@ export default {
118
120
  },
119
121
 
120
122
  /**
121
- * This component is used in MultiStep Priocess
123
+ * This component is used in MultiStep Process
122
124
  * So when user click through to final step and submit the form
123
125
  * This component get recreated therefore register `doCreate` as a hook each time
124
126
  * Also, the parent step component is not aware that credential is created
@@ -194,6 +196,7 @@ export default {
194
196
  SSH: AUTH_TYPE._SSH,
195
197
  BASIC: AUTH_TYPE._BASIC,
196
198
  S3: AUTH_TYPE._S3,
199
+ RKE: AUTH_TYPE._RKE,
197
200
  };
198
201
  },
199
202
 
@@ -308,7 +311,7 @@ export default {
308
311
  });
309
312
  }
310
313
 
311
- if (this.allowSsh || this.allowS3 || this.allowBasic) {
314
+ if (this.allowSsh || this.allowS3 || this.allowBasic || this.allowRke) {
312
315
  out.unshift({
313
316
  label: 'divider',
314
317
  disabled: true,
@@ -340,6 +343,15 @@ export default {
340
343
  });
341
344
  }
342
345
 
346
+ // Note here about order
347
+ if ( this.allowRke ) {
348
+ out.unshift({
349
+ label: this.t('selectOrCreateAuthSecret.createRKE'),
350
+ value: AUTH_TYPE._RKE,
351
+ kind: 'highlighted'
352
+ });
353
+ }
354
+
343
355
  return out;
344
356
  },
345
357
 
@@ -348,7 +360,7 @@ export default {
348
360
  return '';
349
361
  }
350
362
 
351
- if ( this.selected === AUTH_TYPE._SSH || this.selected === AUTH_TYPE._BASIC || this.selected === AUTH_TYPE._S3 ) {
363
+ if ( this.selected === AUTH_TYPE._SSH || this.selected === AUTH_TYPE._BASIC || this.selected === AUTH_TYPE._RKE || this.selected === AUTH_TYPE._S3 ) {
352
364
  return 'col span-4';
353
365
  }
354
366
 
@@ -448,7 +460,7 @@ export default {
448
460
  },
449
461
 
450
462
  updateKeyVal() {
451
- if ( ![AUTH_TYPE._SSH, AUTH_TYPE._BASIC, AUTH_TYPE._S3].includes(this.selected) ) {
463
+ if ( ![AUTH_TYPE._SSH, AUTH_TYPE._BASIC, AUTH_TYPE._S3, AUTH_TYPE._RKE].includes(this.selected) ) {
452
464
  this.privateKey = '';
453
465
  this.publicKey = '';
454
466
  }
@@ -461,7 +473,7 @@ export default {
461
473
  },
462
474
 
463
475
  update() {
464
- if ( (!this.selected || [AUTH_TYPE._SSH, AUTH_TYPE._BASIC, AUTH_TYPE._S3, AUTH_TYPE._NONE].includes(this.selected))) {
476
+ if ( (!this.selected || [AUTH_TYPE._SSH, AUTH_TYPE._BASIC, AUTH_TYPE._S3, AUTH_TYPE._RKE, AUTH_TYPE._NONE].includes(this.selected))) {
465
477
  this.$emit('update:value', null);
466
478
  } else if ( this.selected.includes(':') ) {
467
479
  // Cloud creds
@@ -485,7 +497,7 @@ export default {
485
497
  },
486
498
 
487
499
  async doCreate() {
488
- if ( ![AUTH_TYPE._SSH, AUTH_TYPE._BASIC, AUTH_TYPE._S3].includes(this.selected) || this.delegateCreateToParent ) {
500
+ if ( ![AUTH_TYPE._SSH, AUTH_TYPE._BASIC, AUTH_TYPE._S3, AUTH_TYPE._RKE].includes(this.selected) || this.delegateCreateToParent ) {
489
501
  return;
490
502
  }
491
503
 
@@ -521,15 +533,24 @@ export default {
521
533
  publicField = 'username';
522
534
  privateField = 'password';
523
535
  break;
536
+ case AUTH_TYPE._RKE:
537
+ type = SECRET_TYPES.RKE_AUTH_CONFIG;
538
+ // Set the 'auth' key to be the base64 of the username and password concatenated with a ':' character
539
+ secret.data = { auth: base64Encode(`${ this.publicKey }:${ this.privateKey }`) };
540
+ break;
524
541
  default:
525
542
  throw new Error('Unknown type');
526
543
  }
527
544
 
528
545
  secret._type = type;
529
- secret.data = {
530
- [publicField]: base64Encode(this.publicKey),
531
- [privateField]: base64Encode(this.privateKey),
532
- };
546
+
547
+ // Set the data if not set by one of the specific cases above
548
+ if (!secret.data) {
549
+ secret.data = {
550
+ [publicField]: base64Encode(this.publicKey),
551
+ [privateField]: base64Encode(this.privateKey),
552
+ };
553
+ }
533
554
  }
534
555
 
535
556
  await secret.save();
@@ -583,7 +604,14 @@ export default {
583
604
  />
584
605
  </div>
585
606
  </template>
586
- <template v-else-if="selected === BASIC">
607
+ <template v-else-if="selected === BASIC || selected === RKE">
608
+ <Banner
609
+ v-if="selected === RKE"
610
+ color="info"
611
+ :class="moreCols"
612
+ >
613
+ {{ t('selectOrCreateAuthSecret.rke.info', {}, true) }}
614
+ </Banner>
587
615
  <div :class="moreCols">
588
616
  <LabeledInput
589
617
  v-model:value="publicKey"
@@ -0,0 +1,44 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import NodeScheduling from '@shell/components/form/NodeScheduling.vue';
3
+ import { _CREATE, _EDIT, _VIEW } from '@shell/config/query-params';
4
+
5
+ const requiredSetup = () => {
6
+ return {
7
+ global: {
8
+ mocks: {
9
+ $store: {
10
+ getters: {
11
+ currentProduct: { inStore: 'cluster' },
12
+ 'i18n/t': (text: string) => text,
13
+ t: (text: string) => text,
14
+ }
15
+ }
16
+ },
17
+ }
18
+ };
19
+ };
20
+
21
+ describe('component: NodeScheduling', () => {
22
+ const value = { nodeName: 'node-1' };
23
+
24
+ const nodes = ['node-0', 'node-1'];
25
+
26
+ it.each([
27
+ _VIEW,
28
+ _CREATE,
29
+ _EDIT
30
+ ])('should show NodeName option', (mode) => {
31
+ const wrapper = mount(
32
+ NodeScheduling,
33
+ {
34
+ props: {
35
+ mode, loading: false, value, nodes
36
+ },
37
+ ...requiredSetup(),
38
+ }
39
+ );
40
+
41
+ expect(wrapper.find('[data-testid="node-scheduling-selectNode"]').exists()).toBeTruthy();
42
+ expect(wrapper.find('[data-testid="node-scheduling-nodeSelector"]').element.textContent).toContain(value.nodeName);
43
+ });
44
+ });