@rancher/shell 0.5.2 → 0.5.3

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 (66) hide show
  1. package/assets/translations/en-us.yaml +8 -4
  2. package/components/ClusterIconMenu.vue +24 -9
  3. package/components/CodeMirror.vue +79 -18
  4. package/components/FixedBanner.vue +1 -0
  5. package/components/ResourceDetail/index.vue +1 -4
  6. package/components/ResourceYaml.vue +29 -5
  7. package/components/SideNav.vue +42 -64
  8. package/components/SortableTable/index.vue +1 -1
  9. package/components/YamlEditor.vue +1 -0
  10. package/components/__tests__/CodeMirror.spec.ts +99 -0
  11. package/components/form/BannerSettings.vue +3 -0
  12. package/components/form/FileSelector.vue +1 -0
  13. package/components/form/KeyValue.vue +1 -0
  14. package/components/formatter/WorkloadDetailEndpoints.vue +12 -22
  15. package/components/formatter/__tests__/WorkloadDetailEndpoints.test.ts +81 -0
  16. package/components/nav/Header.vue +1 -0
  17. package/components/nav/Jump.vue +19 -9
  18. package/components/nav/TopLevelMenu.vue +37 -15
  19. package/components/nav/Type.vue +15 -4
  20. package/components/nav/__tests__/TopLevelMenu.test.ts +1 -1
  21. package/components/nav/__tests__/Type.test.ts +30 -0
  22. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +77 -0
  23. package/detail/fleet.cattle.io.bundle.vue +1 -1
  24. package/detail/provisioning.cattle.io.cluster.vue +19 -4
  25. package/edit/management.cattle.io.setting.vue +1 -0
  26. package/edit/monitoring.coreos.com.alertmanagerconfig/types/opsgenie.vue +1 -1
  27. package/edit/monitoring.coreos.com.alertmanagerconfig/types/pagerduty.vue +1 -2
  28. package/edit/monitoring.coreos.com.alertmanagerconfig/types/slack.vue +1 -1
  29. package/edit/provisioning.cattle.io.cluster/index.vue +14 -7
  30. package/edit/provisioning.cattle.io.cluster/rke2.vue +22 -50
  31. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +9 -11
  32. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +3 -1
  33. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +3 -0
  34. package/edit/token.vue +1 -0
  35. package/list/catalog.cattle.io.app.vue +1 -0
  36. package/list/management.cattle.io.setting.vue +1 -0
  37. package/machine-config/amazonec2.vue +1 -0
  38. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +151 -0
  39. package/models/__tests__/secret.test.ts +37 -0
  40. package/models/__tests__/storage.k8s.io.storageclass.test.ts +22 -0
  41. package/models/provisioning.cattle.io.cluster.js +36 -1
  42. package/models/secret.js +9 -0
  43. package/models/storage.k8s.io.storageclass.js +1 -1
  44. package/package.json +1 -1
  45. package/pages/c/_cluster/settings/DefaultLinksEditor.vue +1 -0
  46. package/pages/c/_cluster/settings/brand.vue +3 -0
  47. package/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +4 -4
  48. package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +5 -2
  49. package/pages/c/_cluster/uiplugins/__tests__/AddExtensionRepos.test.ts +96 -0
  50. package/pages/c/_cluster/uiplugins/__tests__/SetupUIPlugins.test.ts +128 -0
  51. package/plugins/dashboard-store/__tests__/actions.test.ts +196 -111
  52. package/plugins/dashboard-store/actions.js +4 -6
  53. package/plugins/dashboard-store/getters.js +60 -2
  54. package/plugins/dashboard-store/resource-class.js +6 -2
  55. package/plugins/steve/__tests__/getters.spec.ts +10 -0
  56. package/plugins/steve/__tests__/resource-utils.test.ts +159 -0
  57. package/plugins/steve/actions.js +3 -37
  58. package/plugins/steve/getters.js +6 -0
  59. package/plugins/steve/resource-utils.ts +38 -0
  60. package/store/__tests__/type-map.test.ts +1122 -0
  61. package/store/index.js +3 -2
  62. package/store/type-map.js +145 -75
  63. package/types/shell/index.d.ts +2 -0
  64. package/utils/__tests__/create-yaml.test.ts +10 -0
  65. package/utils/create-yaml.js +5 -1
  66. package/utils/object.js +10 -0
@@ -185,7 +185,7 @@ nav:
185
185
  configuration: Configuration
186
186
  hci: HCI
187
187
  search:
188
- placeholder: Type to search clusters
188
+ placeholder: Filter clusters by...
189
189
  noResults: No matching clusters
190
190
  clusters: clusters
191
191
  resourceSearch:
@@ -1862,6 +1862,7 @@ cluster:
1862
1862
  header: System Services
1863
1863
  cni:
1864
1864
  label: Container Network
1865
+ cniNoneBanner: When CNI is set to 'none' an <a href="https://ranchermanager.docs.rancher.com/reference-guides/cluster-configuration/rancher-server-configuration/rke2-cluster-configuration#additionalmanifest" target="_blank" rel="noopener noreferrer nofollow">Additional Manifest</a> deploying an alternate CNI must be specified for successful cluster provisioning.
1865
1866
  cilium:
1866
1867
  BandwidthManager:
1867
1868
  enable: Enable Bandwidth Manager
@@ -1870,7 +1871,6 @@ cluster:
1870
1871
  header: Cloud Provider Config
1871
1872
  defaultValue:
1872
1873
  label: Default - RKE2 Embedded
1873
- unsupported: The current Cloud Provider is not supported by this version of Kubernetes. The Cloud Provider has been changed to External. Please use the Cloud Provider Config to supply an out-of-tree configuration as needed.
1874
1874
  security:
1875
1875
  header: Security
1876
1876
  cis:
@@ -1903,8 +1903,8 @@ cluster:
1903
1903
  toolTip: "This can be either a fixed number of nodes (e.g. 1) at a time or a percentage (e.g. 10%)"
1904
1904
  drain:
1905
1905
  label: Drain Nodes
1906
- toolTip: Draining preemptively removes the pods on each node so there are no running workloads on the nodes being upgraded. Upgrading without draining is faster and causes less shuffling around, but pods may still be restarted depending on the upgrade being performed.
1907
- deleteEmptyDir:
1906
+ toolTip: Draining preemptively removes the pods on each node so there are no running workloads on the nodes being upgraded. Upgrading without draining is faster and causes less shuffling around, but pods may still be restarted depending on the upgrade being performed.
1907
+ deleteEmptyDir:
1908
1908
  warning: "By default, pods using emptyDir volumes will be deleted on upgrade. Operations reliant on emptyDir volumes persisting through the pod's lifecycle may be impacted."
1909
1909
  label: Delete pods using emptyDir volumes
1910
1910
  tooltip: emptyDir volumes are often used for ephemeral data, but the data will be permanently deleted. Draining will fail if this is not set and there are pods using emptyDir.
@@ -2078,6 +2078,10 @@ resource:
2078
2078
  errors:
2079
2079
  update: "Error updating {name}"
2080
2080
 
2081
+ codeMirror:
2082
+ keymap:
2083
+ tooltip: Key mapping preference.
2084
+
2081
2085
  cruResource:
2082
2086
  backToForm: Back to Form
2083
2087
  backBody: You will lose any changes made to the YAML.
@@ -15,10 +15,11 @@ export default {
15
15
  showLocalIcon() {
16
16
  return this.cluster.isLocal && !this.cluster.isHarvester && !this.cluster.badge?.iconText;
17
17
  },
18
- badgeLogoBorderBottom() {
19
- const color = this.cluster.badge?.color;
20
-
21
- return color ? `4px solid ${ color }` : '';
18
+ hasCustomColor() {
19
+ return this.cluster.badge?.color;
20
+ },
21
+ customColor() {
22
+ return this.cluster.badge?.color || '';
22
23
  }
23
24
  },
24
25
  methods: {
@@ -44,14 +45,17 @@ export default {
44
45
  >
45
46
  <div
46
47
  class="cluster-badge-logo"
47
- :class="{ 'disabled': !isEnabled }"
48
- :style="{ borderBottom: badgeLogoBorderBottom }"
48
+ :class="{ 'disabled': !isEnabled, 'custom-color': hasCustomColor }"
49
49
  >
50
50
  <span
51
51
  class="cluster-badge-logo-text"
52
52
  >
53
53
  {{ smallIdentifier(cluster.label) }}
54
54
  </span>
55
+ <span
56
+ class="custom-color-decoration"
57
+ :style="{'background': customColor}"
58
+ />
55
59
  <svg
56
60
  v-if="showLocalIcon"
57
61
  class="cluster-local-logo"
@@ -99,7 +103,6 @@ export default {
99
103
  </template>
100
104
 
101
105
  <style lang="scss" scoped>
102
-
103
106
  .cluster-icon-menu {
104
107
  position: relative;
105
108
  align-items: center;
@@ -111,8 +114,8 @@ export default {
111
114
  .cluster-pin-icon {
112
115
  position: absolute;
113
116
  top: -6px;
114
- right: -4px;
115
- font-size: 12px;
117
+ right: -7px;
118
+ font-size: 14px;
116
119
  transform: scaleX(-1);
117
120
  color: var(--body-text);
118
121
  }
@@ -135,6 +138,18 @@ export default {
135
138
  font-size: 12px;
136
139
  text-transform: uppercase;
137
140
 
141
+ &.custom-color {
142
+ border-bottom: 4px solid transparent;
143
+ }
144
+
145
+ .custom-color-decoration {
146
+ height: 4px;
147
+ width: 100%;
148
+ position: absolute;
149
+ bottom: 0;
150
+ border-radius: 0px 0px 5px 5px;
151
+ }
152
+
138
153
  &.disabled {
139
154
  filter: grayscale(1);
140
155
  color: var(--muted);
@@ -24,18 +24,23 @@ export default {
24
24
  asTextArea: {
25
25
  type: Boolean,
26
26
  default: false
27
- }
27
+ },
28
+ showKeyMapBox: {
29
+ type: Boolean,
30
+ default: false
31
+ },
28
32
  },
29
33
 
30
34
  data() {
31
35
  return {
32
- codeMirrorRef: null,
33
- loaded: false
36
+ codeMirrorRef: null,
37
+ loaded: false,
38
+ showKeyMapCloseIcon: false,
39
+ removeKeyMapBox: false,
34
40
  };
35
41
  },
36
42
 
37
43
  computed: {
38
-
39
44
  isDisabled() {
40
45
  return this.mode === _VIEW;
41
46
  },
@@ -70,6 +75,10 @@ export default {
70
75
 
71
76
  return out;
72
77
  },
78
+
79
+ keyMapText() {
80
+ return this.combinedOptions?.keyMap ? this.t(`prefs.keymap.${ this.combinedOptions.keyMap }`) : null;
81
+ },
73
82
  },
74
83
 
75
84
  created() {
@@ -124,6 +133,14 @@ export default {
124
133
  if ( this.$refs.codeMirrorRef ) {
125
134
  this.$refs.codeMirrorRef.codemirror.doc.setValue(value);
126
135
  }
136
+ },
137
+
138
+ closeKeyMapInfo() {
139
+ this.removeKeyMapBox = true;
140
+ },
141
+
142
+ onKeyMapMouseOver(v) {
143
+ this.showKeyMapCloseIcon = v;
127
144
  }
128
145
  }
129
146
  };
@@ -134,18 +151,40 @@ export default {
134
151
  class="code-mirror"
135
152
  :class="{['as-text-area']: asTextArea}"
136
153
  >
137
- <codemirror
138
- v-if="loaded"
139
- ref="codeMirrorRef"
140
- :value="value"
141
- :options="combinedOptions"
142
- :disabled="isDisabled"
143
- @ready="onReady"
144
- @input="onInput"
145
- @changes="onChanges"
146
- @focus="onFocus"
147
- @blur="onBlur"
148
- />
154
+ <div v-if="loaded">
155
+ <div
156
+ v-if="showKeyMapBox && !removeKeyMapBox && keyMapText"
157
+ class="keymap overlay"
158
+ >
159
+ <div
160
+ v-clean-tooltip="t('codeMirror.keymap.tooltip')"
161
+ class="label"
162
+ data-testid="code-mirror-keymap"
163
+ @mouseover="onKeyMapMouseOver(true)"
164
+ @mouseleave="onKeyMapMouseOver(false)"
165
+ >
166
+ <span>
167
+ {{ keyMapText }}
168
+ </span>
169
+ <i
170
+ v-if="showKeyMapCloseIcon"
171
+ class="icon icon-close icon-sm"
172
+ @click="closeKeyMapInfo"
173
+ />
174
+ </div>
175
+ </div>
176
+ <codemirror
177
+ ref="codeMirrorRef"
178
+ :value="value"
179
+ :options="combinedOptions"
180
+ :disabled="isDisabled"
181
+ @ready="onReady"
182
+ @input="onInput"
183
+ @changes="onChanges"
184
+ @focus="onFocus"
185
+ @blur="onBlur"
186
+ />
187
+ </div>
149
188
  <div v-else>
150
189
  Loading...
151
190
  </div>
@@ -156,6 +195,30 @@ export default {
156
195
  .code-mirror {
157
196
  z-index: 0;
158
197
 
198
+ .overlay {
199
+ position: sticky;
200
+ display: grid;
201
+ top: 0;
202
+ float: right;
203
+ height: 0;
204
+ z-index: 1;
205
+
206
+ .label {
207
+ border-radius: 2px;
208
+ border-style: dashed;
209
+ border-width: 0.1px;
210
+ margin: 7px 7px 0 0;
211
+ padding: 7px;
212
+ color: var(--darker);
213
+ background-color: var(--overlay-bg);
214
+ font-size: 12px;
215
+
216
+ .icon {
217
+ cursor: pointer;
218
+ }
219
+ }
220
+ }
221
+
159
222
  .vue-codemirror .CodeMirror {
160
223
  height: initial;
161
224
  background: none
@@ -248,7 +311,5 @@ export default {
248
311
  background-color: var(--primary);
249
312
  }
250
313
  }
251
-
252
314
  }
253
-
254
315
  </style>
@@ -150,6 +150,7 @@ export default {
150
150
  <div
151
151
  v-if="!showAsDialog"
152
152
  class="banner"
153
+ data-testid="fixed__banner"
153
154
  :style="bannerStyle"
154
155
  :class="{'banner-consent': consent}"
155
156
  >
@@ -28,7 +28,6 @@ function modeFor(route) {
28
28
  }
29
29
 
30
30
  async function getYaml(store, model) {
31
- const inStore = store.getters['currentStore'](model.type);
32
31
  let yaml;
33
32
  const opt = { headers: { accept: 'application/yaml' } };
34
33
 
@@ -36,9 +35,7 @@ async function getYaml(store, model) {
36
35
  yaml = (await model.followLink('view', opt)).data;
37
36
  }
38
37
 
39
- const cleanedYaml = await store.dispatch(`${ inStore }/cleanForDownload`, yaml);
40
-
41
- return cleanedYaml;
38
+ return model.cleanForDownload(yaml);
42
39
  }
43
40
 
44
41
  export default {
@@ -365,6 +365,8 @@ export default {
365
365
  >
366
366
  <Footer
367
367
  v-if="showFooter"
368
+ class="footer"
369
+ :class="{ 'edit': !isView }"
368
370
  :mode="mode"
369
371
  :errors="errors"
370
372
  @save="save"
@@ -408,11 +410,29 @@ export default {
408
410
  </template>
409
411
 
410
412
  <style lang='scss' scoped>
411
- .flex-content {
412
- display: flex;
413
- flex-direction: column;
414
- flex-grow: 1;
415
- }
413
+ .flex-content {
414
+ display: flex;
415
+ flex-direction: column;
416
+ flex-grow: 1;
417
+ }
418
+
419
+ .footer {
420
+ margin-top: 20px;
421
+ right: 0;
422
+ position: sticky;
423
+ bottom: 0;
424
+ background-color: var(--header-bg);
425
+
426
+ // Overrides outlet padding
427
+ margin-left: -$space-m;
428
+ margin-right: -$space-m;
429
+ margin-bottom: -$space-m;
430
+ padding: $space-s $space-m;
431
+
432
+ &.edit {
433
+ border-top: var(--header-border-size) solid var(--header-border);
434
+ }
435
+ }
416
436
  </style>
417
437
 
418
438
  <style lang="scss">
@@ -424,6 +444,10 @@ export default {
424
444
  footer .actions {
425
445
  text-align: right;
426
446
  }
447
+
448
+ .spacer-small {
449
+ padding: 0;
450
+ }
427
451
  }
428
452
 
429
453
  </style>
@@ -7,16 +7,16 @@ import {
7
7
  FAVORITE_TYPES
8
8
  } from '@shell/store/prefs';
9
9
  import { getVersionInfo } from '@shell/utils/version';
10
- import { addObjects, replaceWith, clear, addObject } from '@shell/utils/array';
10
+ import {
11
+ addObjects, replaceWith, clear, addObject, sameContents
12
+ } from '@shell/utils/array';
11
13
  import { sortBy } from '@shell/utils/sort';
12
14
  import { ucFirst } from '@shell/utils/string';
13
15
 
14
- import {
15
- HCI, CATALOG, UI, SCHEMA, COUNT
16
- } from '@shell/config/types';
16
+ import { HCI, CATALOG, UI, SCHEMA } from '@shell/config/types';
17
17
  import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
18
18
  import { NAME as EXPLORER } from '@shell/config/product/explorer';
19
- import { BASIC, FAVORITE, USED } from '@shell/store/type-map';
19
+ import { TYPE_MODES } from '@shell/store/type-map';
20
20
  import { NAME as NAVLINKS } from '@shell/config/product/navlinks';
21
21
  import Group from '@shell/components/nav/Group';
22
22
 
@@ -31,6 +31,7 @@ export default {
31
31
  },
32
32
 
33
33
  created() {
34
+ // Ensure that changes to resource that change often don't resort to spamming redraw of the side nav
34
35
  this.queueUpdate = debounce(this.getGroups, 500);
35
36
 
36
37
  this.getGroups();
@@ -42,24 +43,25 @@ export default {
42
43
  },
43
44
 
44
45
  watch: {
45
- counts(a, b) {
46
- if ( a !== b ) {
47
- this.queueUpdate();
48
- }
49
- },
50
46
 
51
- allSchemas(a, b) {
52
- if ( a !== b ) {
47
+ /**
48
+ * Keep this simple, we're only interested in new / removed schemas
49
+ */
50
+ allSchemasIds(a, b) {
51
+ if ( !sameContents(a, b) ) {
53
52
  this.queueUpdate();
54
53
  }
55
54
  },
56
55
 
57
- allNavLinks(a, b) {
58
- if ( a !== b ) {
56
+ allNavLinksIds(a, b) {
57
+ if ( !sameContents(a, b) ) {
59
58
  this.queueUpdate();
60
59
  }
61
60
  },
62
61
 
62
+ /**
63
+ * Note - There's no watch on prefs, so this only catches in session changes
64
+ */
63
65
  favoriteTypes(a, b) {
64
66
  if ( !isEqual(a, b) ) {
65
67
  this.queueUpdate();
@@ -73,23 +75,24 @@ export default {
73
75
  },
74
76
 
75
77
  productId(a, b) {
76
- if ( !isEqual(a, b) ) {
78
+ if ( a !== b) {
77
79
  // Immediately update because you'll see it come in later
78
80
  this.getGroups();
79
81
  }
80
82
  },
81
83
 
84
+ // Queue namespaceMode and namespaces
85
+ // Changes to namespaceMode can also change namespaces, so keep this simple and execute both in a shortened queue
86
+
82
87
  namespaceMode(a, b) {
83
- if ( !isEqual(a, b) ) {
84
- // Immediately update because you'll see it come in later
85
- this.getGroups();
88
+ if ( a !== b ) {
89
+ this.queueUpdate();
86
90
  }
87
91
  },
88
92
 
89
93
  namespaces(a, b) {
90
94
  if ( !isEqual(a, b) ) {
91
- // Immediately update because you'll see it come in later
92
- this.getGroups();
95
+ this.queueUpdate();
93
96
  }
94
97
  },
95
98
 
@@ -100,13 +103,6 @@ export default {
100
103
  }
101
104
  },
102
105
 
103
- product(a, b) {
104
- if ( !isEqual(a, b) ) {
105
- // Immediately update because you'll see it come in later
106
- this.getGroups();
107
- }
108
- },
109
-
110
106
  $route(a, b) {
111
107
  this.$nextTick(() => this.syncNav());
112
108
  },
@@ -174,7 +170,7 @@ export default {
174
170
  return this.$store.getters['cluster/all'](UI.NAV_LINK);
175
171
  },
176
172
 
177
- allSchemas() {
173
+ allSchemasIds() {
178
174
  const managementReady = this.managementReady;
179
175
  const product = this.currentProduct;
180
176
 
@@ -182,33 +178,19 @@ export default {
182
178
  return [];
183
179
  }
184
180
 
185
- return this.$store.getters[`${ product.inStore }/all`](SCHEMA);
186
- },
187
-
188
- counts() {
189
- const managementReady = this.managementReady;
190
- const product = this.currentProduct;
191
-
192
- if ( !managementReady || !product ) {
193
- return {};
194
- }
195
-
196
- const inStore = product.inStore;
197
-
198
- // So that there's something to watch for updates
199
- if ( this.$store.getters[`${ inStore }/haveAll`](COUNT) ) {
200
- const counts = this.$store.getters[`${ inStore }/all`](COUNT)[0].counts;
201
-
202
- return counts;
203
- }
204
-
205
- return {};
181
+ // This does take some up-front time, however avoids an even more costly getGroups call
182
+ return this.$store.getters[`${ product.inStore }/all`](SCHEMA).map((s) => s.id).sort();
206
183
  },
207
184
 
208
185
  namespaces() {
209
186
  return this.$store.getters['activeNamespaceCache'];
210
187
  },
188
+
189
+ allNavLinksIds() {
190
+ return this.allNavLinks.map((a) => a.id);
191
+ },
211
192
  },
193
+
212
194
  methods: {
213
195
  /**
214
196
  * Fetch navigation by creating groups from product schemas
@@ -227,13 +209,6 @@ export default {
227
209
  }
228
210
 
229
211
  const currentProduct = this.$store.getters['productId'];
230
- let namespaces = null;
231
-
232
- if ( !this.$store.getters['isAllNamespaces'] ) {
233
- const namespacesObject = this.$store.getters['namespaces']();
234
-
235
- namespaces = Object.keys(namespacesObject);
236
- }
237
212
 
238
213
  // Always show cluster-level types, regardless of the namespace filter
239
214
  const namespaceMode = 'both';
@@ -255,7 +230,8 @@ export default {
255
230
  // This should already have come into the list from above, but in case it hasn't...
256
231
  addObject(loadProducts, currentProduct);
257
232
 
258
- this.getProductsGroups(out, loadProducts, namespaceMode, namespaces, productMap);
233
+ this.getProductsGroups(out, loadProducts, namespaceMode, productMap);
234
+
259
235
  this.getExplorerGroups(out);
260
236
 
261
237
  replaceWith(this.groups, ...sortBy(out, ['weight:desc', 'label']));
@@ -263,12 +239,12 @@ export default {
263
239
  this.gettingGroups = false;
264
240
  },
265
241
 
266
- getProductsGroups(out, loadProducts, namespaceMode, namespaces, productMap) {
242
+ getProductsGroups(out, loadProducts, namespaceMode, productMap) {
267
243
  const clusterId = this.$store.getters['clusterId'];
268
244
  const currentType = this.$route.params.resource || '';
269
245
 
270
246
  for ( const productId of loadProducts ) {
271
- const modes = [BASIC];
247
+ const modes = [TYPE_MODES.BASIC];
272
248
 
273
249
  if ( productId === NAVLINKS ) {
274
250
  // Navlinks produce their own top-level nav items so don't need to show it as a product.
@@ -276,14 +252,16 @@ export default {
276
252
  }
277
253
 
278
254
  if ( productId === EXPLORER ) {
279
- modes.push(FAVORITE);
280
- modes.push(USED);
255
+ modes.push(TYPE_MODES.FAVORITE);
256
+ modes.push(TYPE_MODES.USED);
281
257
  }
282
258
 
283
- for ( const mode of modes ) {
284
- const types = this.$store.getters['type-map/allTypes'](productId, mode) || {};
259
+ // Get all types for all modes
260
+ const typesByMode = this.$store.getters['type-map/allTypes'](productId, modes);
285
261
 
286
- const more = this.$store.getters['type-map/getTree'](productId, mode, types, clusterId, namespaceMode, namespaces, currentType);
262
+ for ( const mode of modes ) {
263
+ const types = typesByMode[mode] || {};
264
+ const more = this.$store.getters['type-map/getTree'](productId, mode, types, clusterId, namespaceMode, currentType);
287
265
 
288
266
  if ( productId === EXPLORER || !this.isExplorer ) {
289
267
  addObjects(out, more);
@@ -906,7 +906,7 @@ export default {
906
906
  <template>
907
907
  <div
908
908
  ref="container"
909
- data-testid="cluster-list-container"
909
+ :data-testid="componentTestid + '-list-container'"
910
910
  >
911
911
  <div
912
912
  :class="{'titled': $slots.title && $slots.title.length}"
@@ -229,6 +229,7 @@ export default {
229
229
  :class="{fill: true, scrolling: scrolling}"
230
230
  :value="curValue"
231
231
  :options="codeMirrorOptions"
232
+ :showKeyMapBox="true"
232
233
  :data-testid="componentTestid + '-code-mirror'"
233
234
  @onInput="onInput"
234
235
  @onReady="onReady"
@@ -0,0 +1,99 @@
1
+ import { shallowMount, Wrapper } from '@vue/test-utils';
2
+ import CodeMirror from '@shell/components/CodeMirror.vue';
3
+ import { _EDIT, _YAML } from '@shell/config/query-params';
4
+
5
+ describe('component: CodeMirror.vue', () => {
6
+ let wrapper: Wrapper<InstanceType<typeof CodeMirror>>;
7
+
8
+ const options = {
9
+ readOnly: false,
10
+ gutters: [
11
+ 'CodeMirror-lint-markers',
12
+ 'CodeMirror-foldgutter'
13
+ ],
14
+ mode: 'yaml',
15
+ lint: true,
16
+ lineNumbers: true,
17
+ styleActiveLine: true,
18
+ tabSize: 2,
19
+ indentWithTabs: false,
20
+ cursorBlinkRate: 530,
21
+ extraKeys: { 'Ctrl-Space': 'autocomplete' }
22
+ };
23
+
24
+ const mountOptions = {
25
+ propsData: {
26
+ value: '',
27
+ mode: _EDIT,
28
+ options,
29
+ asTextArea: false,
30
+ showKeyMapBox: true,
31
+ },
32
+ mocks: {
33
+ $store: {
34
+ getters: {
35
+ currentStore: () => 'current_store',
36
+ 'current_store/schemaFor': jest.fn(),
37
+ 'current_store/all': jest.fn(),
38
+ 'i18n/t': () => 'Vim',
39
+ 'prefs/get': () => 'Vim',
40
+ 'prefs/theme': jest.fn(),
41
+ }
42
+ },
43
+ $route: { query: { AS: _YAML } },
44
+ $router: { applyQuery: jest.fn() },
45
+ },
46
+ };
47
+
48
+ describe('keyMap info', () => {
49
+ (window as any).__codeMirrorLoader = () => new Promise((resolve) => {
50
+ resolve(true);
51
+ });
52
+
53
+ wrapper = shallowMount(
54
+ CodeMirror,
55
+ mountOptions,
56
+ );
57
+
58
+ it(`should show keyMap preference`, async() => {
59
+ await wrapper.vm.$nextTick();
60
+
61
+ const keyMapBox = wrapper.find('[data-testid="code-mirror-keymap"]');
62
+ const closeIcon = keyMapBox.find('.icon');
63
+
64
+ expect(keyMapBox.element.textContent).toContain('Vim');
65
+ expect(closeIcon.element).toBeUndefined();
66
+ });
67
+
68
+ it(`should show keyMap close icon on mouse over`, async() => {
69
+ await wrapper.vm.$nextTick();
70
+
71
+ const keyMapBox = wrapper.find('[data-testid="code-mirror-keymap"]');
72
+
73
+ keyMapBox.trigger('mouseover');
74
+ await wrapper.vm.$nextTick();
75
+
76
+ const closeIcon = keyMapBox.find('.icon');
77
+
78
+ expect(closeIcon.element).toBeDefined();
79
+ });
80
+
81
+ it(`should remove keyMap box`, async() => {
82
+ await wrapper.vm.$nextTick();
83
+
84
+ let keyMapBox = wrapper.find('[data-testid="code-mirror-keymap"]');
85
+
86
+ keyMapBox.trigger('mouseover');
87
+ await wrapper.vm.$nextTick();
88
+
89
+ const closeIcon = keyMapBox.find('.icon');
90
+
91
+ closeIcon.element.click();
92
+ await wrapper.vm.$nextTick();
93
+
94
+ keyMapBox = wrapper.find('[data-testid="code-mirror-keymap"]');
95
+
96
+ expect(keyMapBox.element).toBeUndefined();
97
+ });
98
+ });
99
+ });