@rancher/shell 3.0.1-rc.3 → 3.0.1-rc.4

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 (38) hide show
  1. package/assets/translations/en-us.yaml +13 -11
  2. package/assets/translations/zh-hans.yaml +0 -6
  3. package/components/ResourceDetail/Masthead.vue +1 -1
  4. package/components/ResourceDetail/index.vue +66 -11
  5. package/components/ResourceYaml.vue +0 -53
  6. package/components/form/UnitInput.vue +1 -1
  7. package/components/form/__tests__/UnitInput.test.ts +1 -1
  8. package/components/nav/TopLevelMenu.vue +1 -1
  9. package/components/nav/WindowManager/ContainerShell.vue +13 -4
  10. package/components/nav/WindowManager/__tests__/ContainerShell.test.ts +20 -18
  11. package/composables/useLabeledFormElement.ts +6 -2
  12. package/config/router/navigation-guards/index.js +1 -2
  13. package/config/settings.ts +2 -7
  14. package/edit/workload/index.vue +1 -1
  15. package/edit/workload/storage/csi/index.vue +29 -1
  16. package/edit/workload/storage/index.vue +1 -0
  17. package/initialize/App.vue +3 -10
  18. package/initialize/install-plugins.js +1 -2
  19. package/list/management.cattle.io.podsecurityadmissionconfigurationtemplate.vue +6 -2
  20. package/models/nodedriver.js +2 -2
  21. package/models/provisioning.cattle.io.cluster.js +1 -1
  22. package/package.json +2 -3
  23. package/pages/auth/setup.vue +7 -28
  24. package/rancher-components/Banner/Banner.vue +1 -0
  25. package/rancher-components/Form/Checkbox/Checkbox.vue +2 -0
  26. package/rancher-components/Form/LabeledInput/LabeledInput.vue +2 -0
  27. package/rancher-components/Form/Radio/RadioButton.vue +2 -0
  28. package/rancher-components/Form/Radio/RadioGroup.vue +2 -0
  29. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +2 -0
  30. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +3 -0
  31. package/rancher-components/StringList/StringList.test.ts +15 -15
  32. package/rancher-components/StringList/StringList.vue +3 -0
  33. package/scripts/extension/helm/charts/ui-plugin-server/Chart.yaml +0 -2
  34. package/scripts/test-plugins-build.sh +3 -2
  35. package/utils/uiplugins.ts +293 -0
  36. package/components/templates/error.vue +0 -131
  37. package/config/router/navigation-guards/history.js +0 -13
  38. package/plugins/back-button.js +0 -3
@@ -1480,16 +1480,16 @@ cluster:
1480
1480
  defaultLabel: Use default data directory configuration
1481
1481
  commonLabel: Use a common base directory for data directory configuration (sub-directories will be used for the system-agent, provisioning and distro paths)
1482
1482
  customLabel: Use custom data directories
1483
- common:
1483
+ common:
1484
1484
  label: Data directory base path
1485
1485
  tooltip: Data directory base path. We will append the sub-directories appropriate for each directory (/agent, /provisioning and either /rke2 or /k3s)
1486
- systemAgent:
1486
+ systemAgent:
1487
1487
  label: System-agent directory path
1488
1488
  tooltip: Data directory for the system-agent connection info and plans
1489
- provisioning:
1489
+ provisioning:
1490
1490
  label: Provisioning directory path
1491
1491
  tooltip: Data directory for provisioning related files
1492
- k8sDistro:
1492
+ k8sDistro:
1493
1493
  label: K8s Distro directory path
1494
1494
  tooltip: Data directory for the k8s distro
1495
1495
  machineConfig:
@@ -5300,7 +5300,6 @@ setup:
5300
5300
  skip: Skip
5301
5301
  tip: What URL should be used for this {vendor} installation? All the nodes in your clusters will need to be able to reach this.
5302
5302
  setPassword: The first order of business is to set a strong password for the default <code>{username}</code> user. We suggest using this random one generated just for you, but enter your own if you like.
5303
- telemetry: Allow collection of <a href="{docsBase}/faq/telemetry" target="_blank" rel="noopener noreferrer nofollow">anonymous statistics</a> to help us improve {name}
5304
5303
  useManual: Set a specific password to use
5305
5304
  useRandom: Use a randomly generated password
5306
5305
  welcome: Welcome to {vendor}!
@@ -6139,6 +6138,14 @@ wm:
6139
6138
  clear: Clear
6140
6139
  containerName: "Container: {label}"
6141
6140
  failed: "Unable to open a shell to the container (none of the shell commmands succeeded)\n\r"
6141
+ logLevel:
6142
+ info: INFO
6143
+ error: ERROR
6144
+ warning: WARN
6145
+ debug: DEBUG
6146
+ logMessage:
6147
+ containerError: "{ logLevel }: Container missing shell executable (/bin/sh)"
6148
+
6142
6149
  kubectlShell:
6143
6150
  title: "Kubectl: {name}"
6144
6151
 
@@ -6544,7 +6551,7 @@ workload:
6544
6551
  managed: Managed
6545
6552
  shared: Shared
6546
6553
  drivers:
6547
- driver.longhorn.io: Longhorn
6554
+ driver-longhorn-io: Longhorn
6548
6555
  fsType: Filesystem Type
6549
6556
  shareName: Share Name
6550
6557
  secretName: Secret Name
@@ -7415,7 +7422,6 @@ advancedSettings:
7415
7422
  'ui-dashboard-index': 'HTML index location for the {appName} UI.'
7416
7423
  'ui-offline-preferred': 'Controls whether UI assets are served locally by the server container or from the remote URL defined in the ui-index and ui-dashboard-index settings. The `Dynamic` option will use local assets in production builds of {appName}.'
7417
7424
  'ui-pl': 'Private-Label company name.'
7418
- 'telemetry-opt': 'Telemetry reporting opt-in.'
7419
7425
  'auth-user-info-max-age-seconds': 'The maximum age of a users auth tokens before an auth provider group membership sync will be performed.'
7420
7426
  'auth-user-info-resync-cron': 'Default cron schedule for resyncing auth provider group memberships.'
7421
7427
  'cluster-template-enforcement': 'Non-admins will be restricted to launching clusters via preapproved RKE Templates only.'
@@ -7440,10 +7446,6 @@ advancedSettings:
7440
7446
  'ui-default-landing':
7441
7447
  ember: Cluster Manager
7442
7448
  vue: Cluster Explorer
7443
- 'telemetry-opt':
7444
- prompt: Prompt
7445
- in: Opt-in to Telemetry
7446
- out: Opt-out of Telemetry
7447
7449
  'ui-offline-preferred':
7448
7450
  dynamic: Dynamic
7449
7451
  true: Local
@@ -4964,7 +4964,6 @@ setup:
4964
4964
  skip: 跳过
4965
4965
  tip: 此 {vendor} 安装应使用什么 URL?集群中的所有节点都需要能访问该 URL。
4966
4966
  setPassword: 请为默认用户 <code>{username}</code>设置强密码。建议使用生成的随机密码。你也可以自行设置。
4967
- telemetry: 允许收集<a href="{docsBase}/faq/telemetry" target="_blank" rel="noopener noreferrer nofollow">匿名统计数据</a>,以帮我们改进 {name}。
4968
4967
  useManual: 设置密码
4969
4968
  useRandom: 使用随机生成的密码
4970
4969
  welcome: 欢迎使用 {vendor}!
@@ -7011,7 +7010,6 @@ advancedSettings:
7011
7010
  'ui-dashboard-index': '{appName} UI 的 HTML 索引位置。'
7012
7011
  'ui-offline-preferred': '控制 UI 资产是由服务器容器在本地提供,还是从 ui-index 和 ui-dashboard-index 设置中定义的远程 URL 提供。`动态` 选项将在 {appName} 的生产版本中使用本地资产。'
7013
7012
  'ui-pl': '自有品牌公司名称。'
7014
- 'telemetry-opt': '遥测报告加入。'
7015
7013
  'auth-user-info-max-age-seconds': '在同步验证提供程序组成员之前,用户验证 Token 的最长存活时间。'
7016
7014
  'auth-user-info-resync-cron': '重新同步验证提供程序组成员的默认 cron 调度。'
7017
7015
  'cluster-template-enforcement': '非管理员只能通过预先批准的 RKE 模板启动集群。'
@@ -7032,10 +7030,6 @@ advancedSettings:
7032
7030
  'ui-default-landing':
7033
7031
  ember: Cluster Manager
7034
7032
  vue: 集群浏览器
7035
- 'telemetry-opt':
7036
- prompt: 提示
7037
- in: 加入遥测
7038
- out: 退出遥测
7039
7033
  'ui-offline-preferred':
7040
7034
  dynamic: 动态
7041
7035
  true: 本地
@@ -447,7 +447,7 @@ export default {
447
447
  {{ parent.displayName }}:
448
448
  </router-link>
449
449
  <span v-else>{{ parent.displayName }}:</span>
450
- <span v-if="value.detailPageHeaderActionOverride && value.detailPageHeaderActionOverride(realMode)">{{ value.detailPageHeaderActionOverride(realMode) }}</span>
450
+ <span v-if="value?.detailPageHeaderActionOverride && value?.detailPageHeaderActionOverride(realMode)">{{ value?.detailPageHeaderActionOverride(realMode) }}</span>
451
451
  <t
452
452
  v-else
453
453
  class="masthead-resource-title"
@@ -14,6 +14,8 @@ import { clone, diff } from '@shell/utils/object';
14
14
  import IconMessage from '@shell/components/IconMessage';
15
15
  import ForceDirectedTreeChart from '@shell/components/fleet/ForceDirectedTreeChart';
16
16
  import { checkSchemasForFindAllHash } from '@shell/utils/auth';
17
+ import { stringify } from '@shell/utils/error';
18
+ import { Banner } from '@components/Banner';
17
19
 
18
20
  function modeFor(route) {
19
21
  if ( route.query?.mode === _IMPORT ) {
@@ -48,6 +50,7 @@ export default {
48
50
  ResourceYaml,
49
51
  Masthead,
50
52
  IconMessage,
53
+ Banner
51
54
  },
52
55
 
53
56
  mixins: [CreateEditView],
@@ -75,7 +78,11 @@ export default {
75
78
  componentTestid: {
76
79
  type: String,
77
80
  default: 'resource-details'
78
- }
81
+ },
82
+ errorsMap: {
83
+ type: Object,
84
+ default: null
85
+ },
79
86
  },
80
87
 
81
88
  async fetch() {
@@ -202,16 +209,26 @@ export default {
202
209
  notFound = fqid;
203
210
  }
204
211
 
205
- if (realMode === _VIEW) {
206
- model = liveModel;
207
- } else {
208
- model = await store.dispatch(`${ inStore }/clone`, { resource: liveModel });
209
- }
210
-
211
- initialModel = await store.dispatch(`${ inStore }/clone`, { resource: liveModel });
212
+ try {
213
+ if (realMode === _VIEW) {
214
+ model = liveModel;
215
+ } else {
216
+ model = await store.dispatch(`${ inStore }/clone`, { resource: liveModel });
217
+ }
218
+ initialModel = await store.dispatch(`${ inStore }/clone`, { resource: liveModel });
212
219
 
220
+ if ( as === _YAML ) {
221
+ yaml = await getYaml(this.$store, liveModel);
222
+ }
223
+ } catch (e) {
224
+ this.errors.push(e);
225
+ }
213
226
  if ( as === _YAML ) {
214
- yaml = await getYaml(this.$store, liveModel);
227
+ try {
228
+ yaml = await getYaml(this.$store, liveModel);
229
+ } catch (e) {
230
+ this.errors.push(e);
231
+ }
215
232
  }
216
233
 
217
234
  if ( as === _GRAPH ) {
@@ -225,7 +242,11 @@ export default {
225
242
  }
226
243
 
227
244
  // Ensure common properties exists
228
- model = await store.dispatch(`${ inStore }/cleanForDetail`, model);
245
+ try {
246
+ model = await store.dispatch(`${ inStore }/cleanForDetail`, model);
247
+ } catch (e) {
248
+ this.errors.push(e);
249
+ }
229
250
 
230
251
  const out = {
231
252
  hasGraph,
@@ -272,6 +293,7 @@ export default {
272
293
  notFound: null,
273
294
  canViewChart: true,
274
295
  canViewYaml: null,
296
+ errors: []
275
297
  };
276
298
  },
277
299
 
@@ -311,6 +333,18 @@ export default {
311
333
 
312
334
  return null;
313
335
  },
336
+ hasErrors() {
337
+ return this.errors?.length && Array.isArray(this.errors);
338
+ },
339
+ mappedErrors() {
340
+ return !this.errors ? {} : this.errorsMap || this.errors.reduce((acc, error) => ({
341
+ ...acc,
342
+ [error]: {
343
+ message: error?.data?.message || error,
344
+ icon: null
345
+ }
346
+ }), {});
347
+ },
314
348
  },
315
349
 
316
350
  watch: {
@@ -360,6 +394,7 @@ export default {
360
394
  },
361
395
 
362
396
  methods: {
397
+ stringify,
363
398
  setSubtype(subtype) {
364
399
  this.resourceSubtype = subtype;
365
400
  },
@@ -371,6 +406,9 @@ export default {
371
406
  m[act]();
372
407
  }
373
408
  },
409
+ closeError(index) {
410
+ this.errors = this.errors.filter((_, i) => i !== index);
411
+ },
374
412
  }
375
413
  };
376
414
  </script>
@@ -398,6 +436,22 @@ export default {
398
436
  :value="liveModel"
399
437
  />
400
438
  </Masthead>
439
+ <div
440
+ v-if="hasErrors"
441
+ id="cru-errors"
442
+ class="cru__errors"
443
+ >
444
+ <Banner
445
+ v-for="(err, i) in errors"
446
+ :key="i"
447
+ color="error"
448
+ :data-testid="`error-banner${i}`"
449
+ :label="stringify(mappedErrors[err].message)"
450
+ :icon="mappedErrors[err].icon"
451
+ :closable="true"
452
+ @close="closeError(i)"
453
+ />
454
+ </div>
401
455
 
402
456
  <ForceDirectedTreeChart
403
457
  v-if="isGraph && canViewChart"
@@ -413,8 +467,9 @@ export default {
413
467
  :yaml="yaml"
414
468
  :offer-preview="offerPreview"
415
469
  :done-route="doneRoute"
416
- :done-override="value.doneOverride"
470
+ :done-override="value ? value.doneOverride : null"
417
471
  @update:value="$emit('input', $event)"
472
+ @error="e=>errors.push(e)"
418
473
  />
419
474
 
420
475
  <component
@@ -205,58 +205,6 @@ export default {
205
205
  cm.getMode().fold = saved;
206
206
  },
207
207
 
208
- onChanges(cm, changes) {
209
- if ( changes.length !== 1 ) {
210
- return;
211
- }
212
-
213
- const change = changes[0];
214
-
215
- if ( change.from.line !== change.to.line ) {
216
- return;
217
- }
218
-
219
- let line = change.from.line;
220
- let str = cm.getLine(line);
221
- let maxIndent = indentChars(str);
222
-
223
- if ( maxIndent === null ) {
224
- return;
225
- }
226
-
227
- cm.replaceRange('', { line, ch: 0 }, { line, ch: 1 }, '+input');
228
-
229
- while ( line > 0 ) {
230
- line--;
231
- str = cm.getLine(line);
232
- const indent = indentChars(str);
233
-
234
- if ( indent === null ) {
235
- break;
236
- }
237
-
238
- if ( indent < maxIndent ) {
239
- cm.replaceRange('', { line, ch: 0 }, { line, ch: 1 }, '+input');
240
-
241
- if ( indent === 0 ) {
242
- break;
243
- }
244
-
245
- maxIndent = indent;
246
- }
247
- }
248
-
249
- function indentChars(str) {
250
- const match = str.match(/^#(\s+)/);
251
-
252
- if ( match ) {
253
- return match[1].length;
254
- }
255
-
256
- return null;
257
- }
258
- },
259
-
260
208
  updateValue(value) {
261
209
  this.$refs.yamleditor.updateValue(value);
262
210
  },
@@ -354,7 +302,6 @@ export default {
354
302
  class="yaml-editor flex-content"
355
303
  :editor-mode="editorMode"
356
304
  @onReady="onReady"
357
- @onChanges="onChanges"
358
305
  />
359
306
  <slot
360
307
  name="yamlFooter"
@@ -6,7 +6,7 @@ import { _EDIT } from '@shell/config/query-params';
6
6
  export default {
7
7
  components: { LabeledInput },
8
8
 
9
- emits: ['update:value'],
9
+ emits: ['update:value', 'update:validation', 'change', 'blur'],
10
10
 
11
11
  props: {
12
12
  /**
@@ -184,7 +184,7 @@ describe('component: UnitInput', () => {
184
184
  input.trigger('blur');
185
185
 
186
186
  expect(wrapper.emitted('update:value')).toBeTruthy();
187
- expect(wrapper.emitted('update:value')[4][0]).toBe(value);
187
+ expect(wrapper.emitted('update:value')[3][0]).toBe(value);
188
188
  });
189
189
 
190
190
  describe.each([
@@ -1119,7 +1119,7 @@ export default {
1119
1119
  display: block;
1120
1120
  font-size: $icon-size;
1121
1121
  margin-right: 14px;
1122
- &.group-icon {
1122
+ &:not(.pin){
1123
1123
  width: 42px;
1124
1124
  }
1125
1125
  }
@@ -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
@@ -1,4 +1,6 @@
1
- import { ref, computed, ComputedRef, Ref } from 'vue';
1
+ import {
2
+ ref, computed, ComputedRef, Ref, defineEmits
3
+ } from 'vue';
2
4
  import { _VIEW, _EDIT } from '@shell/config/query-params';
3
5
 
4
6
  interface LabeledFormElementProps {
@@ -70,7 +72,9 @@ export const labeledFormElementProps = {
70
72
  }
71
73
  };
72
74
 
73
- export const useLabeledFormElement = (props: LabeledFormElementProps, emit: (event: string, ...args: any[]) => void): UseLabeledFormElement => {
75
+ const labeledFormElementEmits = defineEmits(['update:validation']);
76
+
77
+ export const useLabeledFormElement = (props: LabeledFormElementProps, emit: typeof labeledFormElementEmits): UseLabeledFormElement => {
74
78
  const raised = ref(props.mode === _VIEW || !!`${ props.value }`);
75
79
  const focused = ref(false);
76
80
  const blurred = ref<number | null>(null);
@@ -4,7 +4,6 @@ import { install as installAuthentication } from '@shell/config/router/navigatio
4
4
  import { install as installRuntimeExtensionRoute } from '@shell/config/router/navigation-guards/runtime-extension-route';
5
5
  import { install as installI18N } from '@shell/config/router/navigation-guards/i18n';
6
6
  import { install as installProducts } from '@shell/config/router/navigation-guards/products';
7
- import { install as installHistory } from '@shell/config/router/navigation-guards/history';
8
7
  import { install as installClusters } from '@shell/config/router/navigation-guards/clusters';
9
8
  import { install as installHandleInstallRedirect } from '@shell/config/router/navigation-guards/install-redirect';
10
9
  import { install as installPageTitle } from '@shell/config/router/navigation-guards/page-title';
@@ -17,7 +16,7 @@ export function installNavigationGuards(router, context) {
17
16
  // NOTE: the order of the installation matters.
18
17
  // Be intentional when adding, removing or modifying the guards that are installed.
19
18
 
20
- const navigationGuardInstallers = [installLoadInitialSettings, installAttemptFirstLogin, installAuthentication, installProducts, installHistory, installClusters, installRuntimeExtensionRoute, installI18N, installHandleInstallRedirect, installPageTitle, installServerUpgradeGrowl];
19
+ const navigationGuardInstallers = [installLoadInitialSettings, installAttemptFirstLogin, installAuthentication, installProducts, installClusters, installRuntimeExtensionRoute, installI18N, installHandleInstallRedirect, installPageTitle, installServerUpgradeGrowl];
21
20
 
22
21
  navigationGuardInstallers.forEach((installer) => installer(router, context));
23
22
  }
@@ -51,7 +51,6 @@ export const SETTING = {
51
51
  INGRESS_IP_DOMAIN: 'ingress-ip-domain',
52
52
  SERVER_URL: 'server-url',
53
53
  RKE_METADATA_CONFIG: 'rke-metadata-config',
54
- TELEMETRY: 'telemetry-opt',
55
54
  EULA_AGREED: 'eula-agreed',
56
55
  AUTH_USER_INFO_MAX_AGE_SECONDS: 'auth-user-info-max-age-seconds',
57
56
  AUTH_USER_SESSION_TTL_MINUTES: 'auth-user-session-ttl-minutes',
@@ -156,12 +155,8 @@ export const ALLOWED_SETTINGS: GlobalSetting = {
156
155
  },
157
156
  [SETTING.BRAND]: { canReset: true },
158
157
  [SETTING.CLUSTER_TEMPLATE_ENFORCEMENT]: { kind: 'boolean' },
159
- [SETTING.TELEMETRY]: {
160
- kind: 'enum',
161
- options: ['prompt', 'in', 'out']
162
- },
163
- [SETTING.HIDE_LOCAL_CLUSTER]: { kind: 'boolean' },
164
- [SETTING.AGENT_TLS_MODE]: {
158
+ [SETTING.HIDE_LOCAL_CLUSTER]: { kind: 'boolean' },
159
+ [SETTING.AGENT_TLS_MODE]: {
165
160
  kind: 'enum',
166
161
  options: ['strict', 'system-store'],
167
162
  warning: 'agent-tls-mode'
@@ -415,7 +415,7 @@ export default {
415
415
  >
416
416
  <Tab
417
417
  :label="t('workload.storage.title')"
418
- name="storage"
418
+ name="storage-pod"
419
419
  :weight="tabWeightMap['storage']"
420
420
  @active="$refs.storage.refresh()"
421
421
  >
@@ -44,6 +44,33 @@ export default {
44
44
 
45
45
  ...mapGetters({ t: 'i18n/t' })
46
46
  },
47
+
48
+ methods: {
49
+ /**
50
+ * Retrieves the label for a given option
51
+ * @param option The option for which to retrieve the label. option can be
52
+ * either a string or an object. If it is an object, is should have a `label`
53
+ * property associated with it.
54
+ */
55
+ getOptionLabel(option) {
56
+ if (typeof option === 'string') {
57
+ return this.getOptionLabelString(option);
58
+ }
59
+
60
+ const { label } = option;
61
+
62
+ return this.getOptionLabelString(label);
63
+ },
64
+ /**
65
+ * Translates a given key into a localized string.
66
+ * @param key The key to be translated.
67
+ */
68
+ getOptionLabelString(key) {
69
+ // Periods are replaced with `-` to prevent conflict with the default key
70
+ // separator.
71
+ return this.t(`workload.storage.csi.drivers.'${ key.replaceAll('.', '-') }'`);
72
+ }
73
+ }
47
74
  };
48
75
  </script>
49
76
 
@@ -71,10 +98,11 @@ export default {
71
98
  <div class="col span-6">
72
99
  <LabeledSelect
73
100
  v-model:value="value.csi.driver"
101
+ data-testid="workload-storage-driver"
74
102
  :mode="mode"
75
103
  :label="t('workload.storage.driver')"
76
104
  :options="driverOpts"
77
- :get-option-label="opt=>t(`workload.storage.csi.drivers.'${opt}'`)"
105
+ :get-option-label="getOptionLabel"
78
106
  :required="true"
79
107
  />
80
108
  </div>
@@ -301,6 +301,7 @@ export default {
301
301
  :button-label="t('workload.storage.addVolume')"
302
302
  :dropdown-options="volumeTypeOptions"
303
303
  size="sm"
304
+ data-testid="dropdown-button-storage-volume"
304
305
  @click-action="e=>addVolume(e.value)"
305
306
  />
306
307
  </template>