@rancher/shell 3.0.1-rc.3 → 3.0.1

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 (94) hide show
  1. package/assets/data/aws-regions.json +1 -0
  2. package/assets/styles/base/_basic.scss +5 -0
  3. package/assets/styles/base/_mixins.scss +8 -0
  4. package/assets/styles/global/_button.scss +5 -0
  5. package/assets/styles/themes/_dark.scss +2 -0
  6. package/assets/styles/themes/_light.scss +2 -0
  7. package/assets/translations/en-us.yaml +40 -22
  8. package/assets/translations/zh-hans.yaml +1 -7
  9. package/chart/monitoring/StorageClassSelector.vue +1 -1
  10. package/components/AssignTo.vue +1 -0
  11. package/components/AsyncButton.vue +1 -0
  12. package/components/BackLink.vue +8 -2
  13. package/components/PaginatedResourceTable.vue +135 -0
  14. package/components/ResourceDetail/Masthead.vue +1 -1
  15. package/components/ResourceDetail/index.vue +66 -11
  16. package/components/ResourceList/index.vue +0 -1
  17. package/components/ResourceTable.vue +6 -1
  18. package/components/ResourceYaml.vue +0 -53
  19. package/components/SortableTable/index.vue +8 -6
  20. package/components/Tabbed/index.vue +35 -2
  21. package/components/form/ResourceLabeledSelect.vue +2 -2
  22. package/components/form/ResourceTabs/index.vue +0 -23
  23. package/components/form/Taints.vue +1 -1
  24. package/components/form/UnitInput.vue +1 -1
  25. package/components/form/__tests__/UnitInput.test.ts +1 -1
  26. package/components/nav/TopLevelMenu.helper.ts +546 -0
  27. package/components/nav/TopLevelMenu.vue +125 -160
  28. package/components/nav/WindowManager/ContainerShell.vue +13 -4
  29. package/components/nav/WindowManager/__tests__/ContainerShell.test.ts +20 -18
  30. package/components/nav/__tests__/TopLevelMenu.test.ts +338 -326
  31. package/composables/useLabeledFormElement.ts +6 -2
  32. package/config/pagination-table-headers.js +4 -4
  33. package/config/product/explorer.js +2 -0
  34. package/config/router/navigation-guards/index.js +1 -2
  35. package/config/router/routes.js +1 -1
  36. package/config/settings.ts +15 -8
  37. package/core/plugin.ts +8 -1
  38. package/core/types-provisioning.ts +5 -0
  39. package/core/types.ts +26 -1
  40. package/dialog/DrainNode.vue +6 -6
  41. package/edit/catalog.cattle.io.clusterrepo.vue +95 -52
  42. package/edit/provisioning.cattle.io.cluster/index.vue +8 -3
  43. package/edit/workload/index.vue +1 -1
  44. package/edit/workload/storage/csi/index.vue +29 -1
  45. package/edit/workload/storage/index.vue +1 -0
  46. package/initialize/App.vue +3 -10
  47. package/initialize/install-plugins.js +1 -2
  48. package/list/management.cattle.io.podsecurityadmissionconfigurationtemplate.vue +6 -2
  49. package/list/node.vue +8 -5
  50. package/mixins/resource-fetch-api-pagination.js +40 -5
  51. package/mixins/resource-fetch.js +48 -5
  52. package/models/management.cattle.io.nodepool.js +5 -4
  53. package/models/nodedriver.js +2 -2
  54. package/models/provisioning.cattle.io.cluster.js +3 -11
  55. package/package.json +7 -8
  56. package/pages/about.vue +22 -0
  57. package/pages/auth/setup.vue +7 -28
  58. package/pages/c/_cluster/explorer/__tests__/index.test.ts +36 -24
  59. package/pages/c/_cluster/explorer/index.vue +100 -59
  60. package/pages/home.vue +308 -123
  61. package/plugins/dashboard-store/__tests__/mutations.test.ts +2 -0
  62. package/plugins/dashboard-store/actions.js +29 -19
  63. package/plugins/dashboard-store/getters.js +5 -2
  64. package/plugins/dashboard-store/mutations.js +4 -2
  65. package/plugins/steve/__tests__/mutations.test.ts +2 -1
  66. package/plugins/steve/steve-pagination-utils.ts +25 -2
  67. package/plugins/steve/subscribe.js +22 -8
  68. package/rancher-components/Banner/Banner.vue +1 -0
  69. package/rancher-components/Form/Checkbox/Checkbox.vue +2 -0
  70. package/rancher-components/Form/LabeledInput/LabeledInput.vue +2 -0
  71. package/rancher-components/Form/Radio/RadioButton.vue +2 -0
  72. package/rancher-components/Form/Radio/RadioGroup.vue +2 -0
  73. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +2 -0
  74. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +3 -0
  75. package/rancher-components/StringList/StringList.test.ts +15 -15
  76. package/rancher-components/StringList/StringList.vue +3 -0
  77. package/scripts/extension/helm/charts/ui-plugin-server/Chart.yaml +0 -2
  78. package/scripts/extension/parse-tag-name +2 -0
  79. package/scripts/test-plugins-build.sh +4 -2
  80. package/store/index.js +31 -9
  81. package/tsconfig.json +7 -1
  82. package/types/resources/settings.d.ts +1 -1
  83. package/types/shell/index.d.ts +1107 -1276
  84. package/types/store/dashboard-store.types.ts +4 -0
  85. package/types/store/pagination.types.ts +13 -0
  86. package/types/store/vuex.d.ts +8 -0
  87. package/types/vue-shim.d.ts +6 -31
  88. package/utils/cluster.js +92 -1
  89. package/utils/pagination-utils.ts +17 -8
  90. package/utils/pagination-wrapper.ts +70 -0
  91. package/utils/uiplugins.ts +307 -0
  92. package/components/templates/error.vue +0 -131
  93. package/config/router/navigation-guards/history.js +0 -13
  94. package/plugins/back-button.js +0 -3
@@ -4,18 +4,20 @@ import ClusterIconMenu from '@shell/components/ClusterIconMenu';
4
4
  import IconOrSvg from '../IconOrSvg';
5
5
  import { BLANK_CLUSTER } from '@shell/store/store-types.js';
6
6
  import { mapGetters } from 'vuex';
7
- import { CAPI, MANAGEMENT } from '@shell/config/types';
8
- import { MENU_MAX_CLUSTERS } from '@shell/store/prefs';
7
+ import { CAPI, COUNT, MANAGEMENT } from '@shell/config/types';
8
+ import { MENU_MAX_CLUSTERS, PINNED_CLUSTERS } from '@shell/store/prefs';
9
9
  import { sortBy } from '@shell/utils/sort';
10
10
  import { ucFirst } from '@shell/utils/string';
11
11
  import { KEY } from '@shell/utils/platform';
12
12
  import { getVersionInfo } from '@shell/utils/version';
13
13
  import { SETTING } from '@shell/config/settings';
14
- import { filterOnlyKubernetesClusters, filterHiddenLocalCluster } from '@shell/utils/cluster';
15
14
  import { getProductFromRoute } from '@shell/utils/router';
16
15
  import { isRancherPrime } from '@shell/config/version';
17
16
  import Pinned from '@shell/components/nav/Pinned';
18
17
  import { getGlobalBannerFontSizes } from '@shell/utils/banners';
18
+ import { TopLevelMenuHelperPagination, TopLevelMenuHelperLegacy } from '@shell/components/nav/TopLevelMenu.helper';
19
+ import { debounce } from 'lodash';
20
+ import { sameContents } from '@shell/utils/array';
19
21
 
20
22
  export default {
21
23
  components: {
@@ -29,6 +31,17 @@ export default {
29
31
  const { displayVersion, fullVersion } = getVersionInfo(this.$store);
30
32
  const hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
31
33
 
34
+ const canPagination = this.$store.getters[`management/paginationEnabled`]({
35
+ id: MANAGEMENT.CLUSTER,
36
+ context: 'side-bar',
37
+ }) && this.$store.getters[`management/paginationEnabled`]({
38
+ id: CAPI.RANCHER_CLUSTER,
39
+ context: 'side-bar',
40
+ });
41
+ const helper = canPagination ? new TopLevelMenuHelperPagination({ $store: this.$store }) : new TopLevelMenuHelperLegacy({ $store: this.$store });
42
+ const provClusters = !canPagination && hasProvCluster ? this.$store.getters[`management/all`](CAPI.RANCHER_CLUSTER) : [];
43
+ const mgmtClusters = !canPagination ? this.$store.getters[`management/all`](MANAGEMENT.CLUSTER) : [];
44
+
32
45
  return {
33
46
  shown: false,
34
47
  displayVersion,
@@ -37,27 +50,26 @@ export default {
37
50
  hasProvCluster,
38
51
  maxClustersToShow: MENU_MAX_CLUSTERS,
39
52
  emptyCluster: BLANK_CLUSTER,
40
- showPinClusters: false,
41
- searchActive: false,
42
53
  routeCombo: false,
43
- };
44
- },
45
54
 
46
- fetch() {
47
- if (this.hasProvCluster) {
48
- this.$store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER });
49
- }
55
+ canPagination,
56
+ helper,
57
+ debouncedHelperUpdateSlow: debounce((...args) => this.helper.update(...args), 750),
58
+ debouncedHelperUpdateQuick: debounce((...args) => this.helper.update(...args), 200),
59
+ provClusters,
60
+ mgmtClusters,
61
+ };
50
62
  },
51
63
 
52
64
  computed: {
53
65
  ...mapGetters(['clusterId']),
54
66
  ...mapGetters(['clusterReady', 'isRancher', 'currentCluster', 'currentProduct', 'isRancherInHarvester']),
55
67
  ...mapGetters({ features: 'features/get' }),
56
- value: {
57
- get() {
58
- return this.$store.getters['productId'];
59
- },
68
+
69
+ pinnedIds() {
70
+ return this.$store.getters['prefs/get'](PINNED_CLUSTERS);
60
71
  },
72
+
61
73
  sideMenuStyle() {
62
74
  const globalBannerSettings = getGlobalBannerFontSizes(this.$store);
63
75
 
@@ -68,161 +80,47 @@ export default {
68
80
  },
69
81
 
70
82
  showClusterSearch() {
71
- return this.clusters.length > this.maxClustersToShow;
83
+ return this.allClustersCount > this.maxClustersToShow;
72
84
  },
73
85
 
74
- /**
75
- * Filter mgmt clusters by
76
- * 1. Harvester type 1 (filterOnlyKubernetesClusters)
77
- * 2. Harvester type 2 (filterHiddenLocalCluster)
78
- * 3. There's a matching prov cluster
79
- *
80
- * Convert remaining clusters to special format
81
- */
82
- clusters() {
83
- if (!this.hasProvCluster) {
84
- // We're filtering out mgmt clusters without prov clusters, so if the user can't see any prov clusters at all
85
- // exit early
86
- return [];
87
- }
88
-
89
- const all = this.$store.getters['management/all'](MANAGEMENT.CLUSTER);
90
- const mgmtClusters = filterHiddenLocalCluster(filterOnlyKubernetesClusters(all, this.$store), this.$store);
91
- const provClusters = this.$store.getters['management/all'](CAPI.RANCHER_CLUSTER);
92
- const provClustersByMgmtId = provClusters.reduce((res, provCluster) => {
93
- if (provCluster.mgmt?.id) {
94
- res[provCluster.mgmt.id] = provCluster;
95
- }
86
+ allClustersCount() {
87
+ const counts = this.$store.getters[`management/all`](COUNT)?.[0]?.counts || {};
88
+ const count = counts[MANAGEMENT.CLUSTER] || {};
96
89
 
97
- return res;
98
- }, {});
90
+ return count?.summary.count;
91
+ },
99
92
 
100
- return (mgmtClusters || []).reduce((res, mgmtCluster) => {
101
- // Filter to only show mgmt clusters that exist for the available provisioning clusters
102
- // Addresses issue where a mgmt cluster can take some time to get cleaned up after the corresponding
103
- // provisioning cluster has been deleted
104
- if (!provClustersByMgmtId[mgmtCluster.id]) {
105
- return res;
106
- }
93
+ // New
94
+ search() {
95
+ return (this.clusterFilter || '').toLowerCase();
96
+ },
107
97
 
108
- const pCluster = provClustersByMgmtId[mgmtCluster.id];
109
-
110
- res.push({
111
- id: mgmtCluster.id,
112
- label: mgmtCluster.nameDisplay,
113
- ready: mgmtCluster.isReady && !pCluster?.hasError,
114
- osLogo: mgmtCluster.providerOsLogo,
115
- providerNavLogo: mgmtCluster.providerMenuLogo,
116
- badge: mgmtCluster.badge,
117
- isLocal: mgmtCluster.isLocal,
118
- isHarvester: mgmtCluster.isHarvester,
119
- pinned: mgmtCluster.pinned,
120
- description: pCluster?.description || mgmtCluster.description,
121
- pin: () => mgmtCluster.pin(),
122
- unpin: () => mgmtCluster.unpin(),
123
- clusterRoute: { name: 'c-cluster-explorer', params: { cluster: mgmtCluster.id } }
124
- });
98
+ // New
99
+ showPinClusters() {
100
+ return !this.clusterFilter;
101
+ },
125
102
 
126
- return res;
127
- }, []);
103
+ // New
104
+ searchActive() {
105
+ return !!this.search;
128
106
  },
129
107
 
130
108
  /**
131
- * Filter clusters by
132
- * 1. Not pinned
133
- * 2. Includes search term
134
- *
135
- * Sort remaining clusters
109
+ * Only Clusters that are pinned
136
110
  *
137
- * Reduce number of clusters if too many too show
138
- *
139
- * Important! This is used to show unpinned clusters OR results of search
111
+ * (see description of helper.clustersPinned for more details)
140
112
  */
141
- clustersFiltered() {
142
- const search = (this.clusterFilter || '').toLowerCase();
143
- let localCluster = null;
144
-
145
- const filtered = this.clusters.filter((c) => {
146
- // If we're searching we don't care if pinned or not
147
- if (search) {
148
- if (!c.label?.toLowerCase().includes(search)) {
149
- return false;
150
- }
151
- } else if (c.pinned) {
152
- // Not searching, not pinned, don't care
153
- return false;
154
- }
155
-
156
- if (!localCluster && c.id === 'local') {
157
- // Local cluster is a special case, we're inserting it at top so don't include in the middle
158
- localCluster = c;
159
-
160
- return false;
161
- }
162
-
163
- return true;
164
- });
165
-
166
- const sorted = sortBy(filtered, ['ready:desc', 'label']);
167
-
168
- // put local cluster on top of list always - https://github.com/rancher/dashboard/issues/10975
169
- if (localCluster) {
170
- sorted.unshift(localCluster);
171
- }
172
-
173
- if (search) {
174
- this.showPinClusters = false;
175
- this.searchActive = !sorted.length > 0;
176
-
177
- return sorted;
178
- }
179
- this.showPinClusters = true;
180
- this.searchActive = false;
181
-
182
- if (sorted.length >= this.maxClustersToShow) {
183
- return sorted.slice(0, this.maxClustersToShow);
184
- }
185
-
186
- return sorted;
113
+ pinFiltered() {
114
+ return this.hasProvCluster ? this.helper.clustersPinned : [];
187
115
  },
188
116
 
189
117
  /**
190
- * Filter clusters by
191
- * 1. Not pinned
192
- * 2. Includes search term
193
- *
194
- * Sort remaining clusters
118
+ * Used to shown unpinned clusters OR results of text search
195
119
  *
196
- * Reduce number of clusters if too many too show
197
- *
198
- * Important! This is hidden if there's a filter (user searching)
120
+ * (see description of helper.clustersOthers for more details)
199
121
  */
200
- pinFiltered() {
201
- let localCluster = null;
202
- const filtered = this.clusters.filter((c) => {
203
- if (!c.pinned) {
204
- // We only care about pinned clusters
205
- return false;
206
- }
207
-
208
- if (c.id === 'local') {
209
- // Special case, we're going to add this at the start so filter out
210
- localCluster = c;
211
-
212
- return false;
213
- }
214
-
215
- return true;
216
- });
217
-
218
- const sorted = sortBy(filtered, ['ready:desc', 'label']);
219
-
220
- // put local cluster on top of list always - https://github.com/rancher/dashboard/issues/10975
221
- if (localCluster) {
222
- sorted.unshift(localCluster);
223
- }
224
-
225
- return sorted;
122
+ clustersFiltered() {
123
+ return this.hasProvCluster ? this.helper.clustersOthers : [];
226
124
  },
227
125
 
228
126
  pinnedClustersHeight() {
@@ -232,7 +130,7 @@ export default {
232
130
  return `min-height: ${ height }px`;
233
131
  },
234
132
  clusterFilterCount() {
235
- return this.clusterFilter ? this.clustersFiltered.length : this.clusters.length;
133
+ return this.clusterFilter ? this.clustersFiltered.length : this.allClustersCount;
236
134
  },
237
135
 
238
136
  multiClusterApps() {
@@ -360,10 +258,45 @@ export default {
360
258
  }
361
259
  },
362
260
 
261
+ // See https://github.com/rancher/dashboard/issues/12831 for outstanding performance related work
363
262
  watch: {
364
263
  $route() {
365
264
  this.shown = false;
366
- }
265
+ },
266
+
267
+ pinnedIds: {
268
+ immediate: true,
269
+ handler(neu, old) {
270
+ if (sameContents(neu, old)) {
271
+ return;
272
+ }
273
+
274
+ this.updateClusters(neu, 'quick');
275
+ }
276
+ },
277
+
278
+ search() {
279
+ this.updateClusters(this.pinnedIds, 'slow');
280
+ },
281
+
282
+ provClusters: {
283
+ handler() {
284
+ // Shouldn't get here if SSP
285
+ this.updateClusters(this.pinnedIds, 'slow');
286
+ },
287
+ deep: true,
288
+ immediate: true,
289
+ },
290
+
291
+ mgmtClusters: {
292
+ handler() {
293
+ // Shouldn't get here if SSP
294
+ this.updateClusters(this.pinnedIds, 'slow');
295
+ },
296
+ deep: true,
297
+ immediate: true,
298
+ },
299
+
367
300
  },
368
301
 
369
302
  mounted() {
@@ -490,9 +423,27 @@ export default {
490
423
  popperClass
491
424
  };
492
425
  },
426
+
427
+ updateClusters(pinnedIds, speed = 'slow' | 'quick') {
428
+ const args = {
429
+ pinnedIds,
430
+ searchTerm: this.search,
431
+ unPinnedMax: this.maxClustersToShow
432
+ };
433
+
434
+ switch (speed) {
435
+ case 'slow':
436
+ this.debouncedHelperUpdateSlow(args);
437
+ break;
438
+ case 'quick':
439
+ this.debouncedHelperUpdateQuick(args);
440
+ break;
441
+ }
442
+ }
493
443
  }
494
444
  };
495
445
  </script>
446
+
496
447
  <template>
497
448
  <div>
498
449
  <!-- Overlay -->
@@ -514,7 +465,12 @@ export default {
514
465
  <div class="title">
515
466
  <div
516
467
  data-testid="top-level-menu"
468
+ :aria-label="t('nav.expandCollapseAppBar')"
469
+ role="button"
470
+ tabindex="0"
517
471
  class="menu"
472
+ @keyup.enter="toggle()"
473
+ @keyup.space="toggle()"
518
474
  @click="toggle()"
519
475
  >
520
476
  <svg
@@ -632,7 +588,7 @@ export default {
632
588
  </template>
633
589
 
634
590
  <!-- Cluster menu -->
635
- <template v-if="clusters && !!clusters.length">
591
+ <template v-if="!!allClustersCount">
636
592
  <div
637
593
  ref="clusterList"
638
594
  class="clusters"
@@ -790,7 +746,7 @@ export default {
790
746
 
791
747
  <!-- No clusters message -->
792
748
  <div
793
- v-if="(clustersFiltered.length === 0 || pinFiltered.length === 0) && searchActive"
749
+ v-if="clustersFiltered.length === 0 && searchActive"
794
750
  data-testid="top-level-menu-no-results"
795
751
  class="none-matching"
796
752
  >
@@ -800,7 +756,7 @@ export default {
800
756
 
801
757
  <!-- See all clusters -->
802
758
  <router-link
803
- v-if="clusters.length > maxClustersToShow"
759
+ v-if="allClustersCount > maxClustersToShow"
804
760
  class="clusters-all"
805
761
  :to="{name: 'c-cluster-product-resource', params: {
806
762
  cluster: emptyCluster,
@@ -972,6 +928,15 @@ export default {
972
928
  align-items: center;
973
929
  justify-content: center;
974
930
 
931
+ &:focus-visible {
932
+ outline: none;
933
+
934
+ .menu-icon {
935
+ @include focus-outline;
936
+ outline-offset: 4px; // Ensure there is space around the menu icon for the focus indication
937
+ }
938
+ }
939
+
975
940
  .menu-icon {
976
941
  width: 25px;
977
942
  height: 25px;
@@ -1119,7 +1084,7 @@ export default {
1119
1084
  display: block;
1120
1085
  font-size: $icon-size;
1121
1086
  margin-right: 14px;
1122
- &.group-icon {
1087
+ &:not(.pin){
1123
1088
  width: 42px;
1124
1089
  }
1125
1090
  }
@@ -14,6 +14,7 @@ import Socket, {
14
14
  EVENT_CONNECT_ERROR,
15
15
  } from '@shell/utils/socket';
16
16
  import Window from './Window';
17
+ import dayjs from 'dayjs';
17
18
 
18
19
  const commands = {
19
20
  linux: [
@@ -287,7 +288,7 @@ export default {
287
288
 
288
289
  // If we had an error message, try connecting with the next command
289
290
  if (this.errorMsg) {
290
- this.terminal.write(this.errorMsg);
291
+ this.terminal.writeln(this.errorMsg);
291
292
  if (this.backupShells.length && this.retries < 2) {
292
293
  this.retries++;
293
294
  // we're not really counting on this being a reactive change so there's no need to fire the whole action
@@ -299,7 +300,9 @@ export default {
299
300
  this.connect();
300
301
  } else {
301
302
  // Output an message to let he user know none of the shell commands worked
302
- this.terminal.write(this.t('wm.containerShell.failed'));
303
+ const timestamp = dayjs().format('YYYY-MM-DD HH:mm:ss');
304
+
305
+ this.terminal.writeln(`[${ timestamp }] ${ this.t('wm.containerShell.logLevel.info') }: ${ this.t('wm.containerShell.failed') }`);
303
306
  }
304
307
  }
305
308
  });
@@ -317,10 +320,16 @@ export default {
317
320
  }
318
321
  this.terminal.write(msg);
319
322
  } else {
320
- console.error(msg); // eslint-disable-line no-console
323
+ const timestamp = dayjs().format('YYYY-MM-DD HH:mm:ss');
324
+ let customError = `[${ timestamp }] ${ this.t('wm.containerShell.logLevel.error') }: ${ this.container }: ${ msg }`;
325
+
326
+ if (msg.includes('stat /bin/sh: no such file or directory')) {
327
+ customError = `[${ timestamp }] ${ this.t('wm.containerShell.logMessage.containerError', { logLevel: this.t('wm.containerShell.logLevel.error') }) }: ${ msg }`;
328
+ }
329
+ console.error(customError); // eslint-disable-line no-console
321
330
 
322
331
  if (`${ type }` === '3') {
323
- this.errorMsg = msg;
332
+ this.errorMsg = customError;
324
333
  }
325
334
  }
326
335
  });
@@ -29,6 +29,7 @@ describe('component: ContainerShell', () => {
29
29
  return { rows: 1 };
30
30
  });
31
31
  const write = jest.fn();
32
+ const writeln = jest.fn();
32
33
  const reset = jest.fn();
33
34
 
34
35
  jest.mock(/* webpackChunkName: "xterm" */ 'xterm', () => {
@@ -39,6 +40,7 @@ describe('component: ContainerShell', () => {
39
40
  open = open;
40
41
  focus = focus;
41
42
  write = write;
43
+ writeln = writeln;
42
44
  reset = reset
43
45
  }
44
46
  };
@@ -277,10 +279,10 @@ describe('component: ContainerShell', () => {
277
279
  eventConnected();
278
280
  eventMessage({ detail: { data: `3${ errorMessage }` } });
279
281
 
280
- expect(consoleError.mock.calls[0][0]).toBe(errorMessage);
282
+ expect(consoleError.mock.calls[0][0]).toContain(errorMessage);
281
283
  expect(wrapper.vm.isOpen).toBe(true);
282
284
  expect(wrapper.vm.isOpening).toBe(false);
283
- expect(wrapper.vm.errorMsg).toBe(errorMessage);
285
+ expect(wrapper.vm.errorMsg).toContain(errorMessage);
284
286
  expect(wrapper.vm.os).toBe('linux');
285
287
  });
286
288
 
@@ -325,10 +327,10 @@ describe('component: ContainerShell', () => {
325
327
 
326
328
  eventDisconnected();
327
329
 
328
- expect(consoleError.mock.calls[0][0]).toBe(errorMessage);
330
+ expect(consoleError.mock.calls[0][0]).toContain(errorMessage);
329
331
  expect(wrapper.vm.isOpen).toBe(false);
330
332
  expect(wrapper.vm.isOpening).toBe(false);
331
- expect(wrapper.vm.errorMsg).toBe('eventMessageError');
333
+ expect(wrapper.vm.errorMsg).toContain('eventMessageError');
332
334
  // the backup shell that was leftover was windows so it became the new os in dataprops
333
335
  expect(wrapper.vm.os).toBeUndefined();
334
336
  // but we still didn't write it to the pod itself since we don't know if it worked
@@ -370,11 +372,11 @@ describe('component: ContainerShell', () => {
370
372
  eventMessage({ detail: { data: `3${ windowsErrorMessage }` } });
371
373
  eventDisconnected();
372
374
 
373
- expect(consoleError.mock.calls[0][0]).toBe(linuxErrorMessage);
374
- expect(consoleError.mock.calls[1][0]).toBe(windowsErrorMessage);
375
+ expect(consoleError.mock.calls[0][0]).toContain(linuxErrorMessage);
376
+ expect(consoleError.mock.calls[1][0]).toContain(windowsErrorMessage);
375
377
  expect(wrapper.vm.isOpen).toBe(false);
376
378
  expect(wrapper.vm.isOpening).toBe(false);
377
- expect(wrapper.vm.errorMsg).toBe(windowsErrorMessage);
379
+ expect(wrapper.vm.errorMsg).toContain(windowsErrorMessage);
378
380
  expect(wrapper.vm.os).toBeUndefined();
379
381
  // we never found a shell that worked so we're going to leave the pod os as undefined
380
382
  expect(defaultContainerShellParams.propsData.pod.os).toBeUndefined();
@@ -426,11 +428,11 @@ describe('component: ContainerShell', () => {
426
428
  eventMessage({ detail: { data: `3${ windowsErrorMessage }` } });
427
429
  eventDisconnected();
428
430
 
429
- expect(consoleError.mock.calls[0][0]).toBe(linuxErrorMessage);
430
- expect(consoleError.mock.calls[1][0]).toBe(windowsErrorMessage);
431
+ expect(consoleError.mock.calls[0][0]).toContain(linuxErrorMessage);
432
+ expect(consoleError.mock.calls[1][0]).toContain(windowsErrorMessage);
431
433
  expect(wrapper.vm.isOpen).toBe(false);
432
434
  expect(wrapper.vm.isOpening).toBe(false);
433
- expect(wrapper.vm.errorMsg).toBe(windowsErrorMessage);
435
+ expect(wrapper.vm.errorMsg).toContain(windowsErrorMessage);
434
436
  expect(wrapper.vm.os).toBeUndefined();
435
437
  expect(testUndefinedOsParams.propsData.pod.os).toBeUndefined();
436
438
  expect(connect.mock.calls).toHaveLength(3);
@@ -472,13 +474,13 @@ describe('component: ContainerShell', () => {
472
474
  expect(wrapper.vm.backupShells).toHaveLength(1);
473
475
  expect(wrapper.vm.os).toBeUndefined();
474
476
  expect(testUndefinedOsParams.propsData.pod.os).toBeUndefined();
475
- expect(wrapper.vm.errorMsg).toBe(linuxErrorMessage);
477
+ expect(wrapper.vm.errorMsg).toContain(linuxErrorMessage);
476
478
 
477
479
  eventConnecting();
478
480
  eventConnected();
479
481
  eventMessage({ detail: { data: `1${ windowsShellMessage }` } });
480
482
 
481
- expect(consoleError.mock.calls[0][0]).toBe(linuxErrorMessage);
483
+ expect(consoleError.mock.calls[0][0]).toContain(linuxErrorMessage);
482
484
  expect(consoleError.mock.calls[1]).toBeUndefined();
483
485
  expect(wrapper.vm.isOpen).toBe(true);
484
486
  expect(wrapper.vm.isOpening).toBe(false);
@@ -532,7 +534,7 @@ describe('component: ContainerShell', () => {
532
534
  expect(wrapper.vm.backupShells).toHaveLength(1);
533
535
  expect(wrapper.vm.os).toBe('linux');
534
536
  expect(testNodeDefinedOsParams.propsData.pod.os).toBe('linux');
535
- expect(wrapper.vm.errorMsg).toBe(linuxErrorMessage);
537
+ expect(wrapper.vm.errorMsg).toContain(linuxErrorMessage);
536
538
 
537
539
  eventConnecting();
538
540
  eventConnected();
@@ -542,7 +544,7 @@ describe('component: ContainerShell', () => {
542
544
  expect(wrapper.vm.backupShells).toHaveLength(1);
543
545
  expect(wrapper.vm.isOpen).toBe(false);
544
546
  expect(wrapper.vm.isOpening).toBe(false);
545
- expect(wrapper.vm.errorMsg).toBe(linuxErrorMessage);
547
+ expect(wrapper.vm.errorMsg).toContain(linuxErrorMessage);
546
548
  expect(wrapper.vm.os).toBe('linux');
547
549
  expect(testNodeDefinedOsParams.propsData.pod.os).toBe('linux');
548
550
  expect(connect.mock.calls).toHaveLength(3);
@@ -552,13 +554,13 @@ describe('component: ContainerShell', () => {
552
554
  eventMessage({ detail: { data: `3${ linuxErrorMessage }` } });
553
555
  eventDisconnected();
554
556
 
555
- expect(consoleError.mock.calls[0][0]).toBe(linuxErrorMessage);
556
- expect(consoleError.mock.calls[1][0]).toBe(linuxErrorMessage);
557
- expect(consoleError.mock.calls[2][0]).toBe(linuxErrorMessage);
557
+ expect(consoleError.mock.calls[0][0]).toContain(linuxErrorMessage);
558
+ expect(consoleError.mock.calls[1][0]).toContain(linuxErrorMessage);
559
+ expect(consoleError.mock.calls[2][0]).toContain(linuxErrorMessage);
558
560
  expect(wrapper.vm.backupShells).toHaveLength(1);
559
561
  expect(wrapper.vm.isOpen).toBe(false);
560
562
  expect(wrapper.vm.isOpening).toBe(false);
561
- expect(wrapper.vm.errorMsg).toBe(linuxErrorMessage);
563
+ expect(wrapper.vm.errorMsg).toContain(linuxErrorMessage);
562
564
  expect(wrapper.vm.os).toBe('linux');
563
565
  expect(testNodeDefinedOsParams.propsData.pod.os).toBe('linux');
564
566
  // at some point we have to stop retying and if we're not burning through backup shells, there's a retry limit of 2 for a total of 3 attempts