@rancher/shell 0.3.17 → 0.3.18

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 (110) hide show
  1. package/assets/translations/en-us.yaml +8 -4
  2. package/assets/translations/zh-hans.yaml +64 -8
  3. package/components/AsyncButton.vue +1 -1
  4. package/components/Inactivity.vue +10 -0
  5. package/components/LazyImage.vue +2 -2
  6. package/components/PromptRestore.vue +7 -5
  7. package/components/ResourceDetail/Masthead.vue +1 -1
  8. package/components/ResourceDetail/index.vue +4 -2
  9. package/components/__tests__/PromptRestore.test.ts +72 -0
  10. package/components/auth/AzureWarning.vue +1 -1
  11. package/components/fleet/FleetResources.vue +3 -64
  12. package/components/form/FileImageSelector.vue +9 -0
  13. package/components/form/FileSelector.vue +2 -1
  14. package/components/form/MatchExpressions.vue +1 -3
  15. package/components/form/__tests__/FileImageSelector.test.ts +42 -0
  16. package/components/form/__tests__/FileSelector.test.ts +76 -0
  17. package/components/formatter/ClusterProvider.vue +3 -1
  18. package/components/formatter/__tests__/ClusterProvider.test.ts +24 -0
  19. package/components/nav/WindowManager/ContainerShell.vue +60 -36
  20. package/components/nav/WindowManager/__tests__/ContainerShell.test.ts +561 -0
  21. package/config/labels-annotations.js +2 -1
  22. package/config/persistentVolume.ts +108 -0
  23. package/config/product/manager.js +5 -1
  24. package/config/types.js +2 -0
  25. package/core/plugin-helpers.js +19 -3
  26. package/core/types.ts +4 -0
  27. package/detail/fleet.cattle.io.gitrepo.vue +10 -2
  28. package/detail/pod.vue +36 -3
  29. package/detail/workload/index.vue +40 -9
  30. package/edit/__tests__/ui.cattle.io.navlink.test.ts +110 -0
  31. package/edit/fleet.cattle.io.clustergroup.vue +14 -3
  32. package/edit/persistentvolume/__tests__/persistentvolume.test.ts +82 -0
  33. package/edit/persistentvolume/index.vue +2 -1
  34. package/edit/persistentvolume/plugins/csi.vue +3 -1
  35. package/edit/persistentvolume/plugins/longhorn.vue +12 -12
  36. package/edit/provisioning.cattle.io.cluster/RegistryConfigs.vue +15 -11
  37. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  38. package/edit/provisioning.cattle.io.cluster/rke2.vue +5 -1
  39. package/edit/storage.k8s.io.storageclass/index.vue +1 -2
  40. package/edit/ui.cattle.io.navlink.vue +213 -187
  41. package/layouts/default.vue +1 -1
  42. package/list/group.principal.vue +1 -1
  43. package/middleware/authenticated.js +12 -4
  44. package/mixins/create-edit-view/impl.js +2 -2
  45. package/models/chart.js +1 -1
  46. package/models/fleet.cattle.io.cluster.js +33 -4
  47. package/models/fleet.cattle.io.gitrepo.js +112 -38
  48. package/models/management.cattle.io.kontainerdriver.js +14 -0
  49. package/models/persistentvolume.js +2 -111
  50. package/models/pod.js +30 -0
  51. package/models/rke.cattle.io.etcdsnapshot.js +10 -7
  52. package/package.json +1 -1
  53. package/pages/c/_cluster/auth/group.principal/assign-edit.vue +1 -1
  54. package/pages/c/_cluster/auth/roles/index.vue +1 -1
  55. package/pages/c/_cluster/explorer/index.vue +1 -1
  56. package/pages/c/_cluster/manager/cloudCredential/_id.vue +0 -1
  57. package/pages/c/_cluster/manager/cloudCredential/create.vue +0 -1
  58. package/pages/c/_cluster/settings/brand.vue +11 -8
  59. package/pages/c/_cluster/uiplugins/index.vue +9 -4
  60. package/pages/home.vue +1 -1
  61. package/plugins/dashboard-store/__tests__/actions.spec.ts +165 -0
  62. package/plugins/dashboard-store/__tests__/getters.spec.ts +100 -0
  63. package/plugins/dashboard-store/__tests__/{mutations.spec.js → mutations.spec.ts} +2 -2
  64. package/plugins/dashboard-store/actions.js +1 -1
  65. package/plugins/dashboard-store/resource-class.js +4 -0
  66. package/plugins/steve/__tests__/getters.spec.ts +93 -0
  67. package/plugins/steve/getters.js +21 -1
  68. package/plugins/steve/subscribe.js +1 -3
  69. package/rancher-components/components/BadgeState/BadgeState.spec.ts +12 -0
  70. package/rancher-components/components/BadgeState/BadgeState.vue +111 -0
  71. package/rancher-components/components/BadgeState/index.ts +1 -0
  72. package/rancher-components/components/Banner/Banner.test.ts +63 -0
  73. package/rancher-components/components/Banner/Banner.vue +244 -0
  74. package/rancher-components/components/Banner/index.ts +1 -0
  75. package/rancher-components/components/Card/Card.test.ts +37 -0
  76. package/rancher-components/components/Card/Card.vue +167 -0
  77. package/rancher-components/components/Card/index.ts +1 -0
  78. package/rancher-components/components/Form/Checkbox/Checkbox.test.ts +68 -0
  79. package/rancher-components/components/Form/Checkbox/Checkbox.vue +420 -0
  80. package/rancher-components/components/Form/Checkbox/index.ts +1 -0
  81. package/rancher-components/components/Form/LabeledInput/LabeledInput.test.ts +23 -0
  82. package/rancher-components/components/Form/LabeledInput/LabeledInput.vue +355 -0
  83. package/rancher-components/components/Form/LabeledInput/index.ts +1 -0
  84. package/rancher-components/components/Form/Radio/RadioButton.test.ts +31 -0
  85. package/rancher-components/components/Form/Radio/RadioButton.vue +287 -0
  86. package/rancher-components/components/Form/Radio/RadioGroup.vue +254 -0
  87. package/rancher-components/components/Form/Radio/index.ts +2 -0
  88. package/rancher-components/components/Form/TextArea/TextAreaAutoGrow.vue +170 -0
  89. package/rancher-components/components/Form/TextArea/index.ts +1 -0
  90. package/rancher-components/components/Form/ToggleSwitch/ToggleSwitch.test.ts +94 -0
  91. package/rancher-components/components/Form/ToggleSwitch/ToggleSwitch.vue +149 -0
  92. package/rancher-components/components/Form/ToggleSwitch/index.ts +1 -0
  93. package/rancher-components/components/Form/index.ts +5 -0
  94. package/rancher-components/components/LabeledTooltip/LabeledTooltip.vue +151 -0
  95. package/rancher-components/components/LabeledTooltip/index.ts +1 -0
  96. package/rancher-components/components/StringList/StringList.test.ts +484 -0
  97. package/rancher-components/components/StringList/StringList.vue +611 -0
  98. package/rancher-components/components/StringList/index.ts +1 -0
  99. package/scripts/typegen.sh +10 -2
  100. package/store/index.js +1 -3
  101. package/store/store-types.js +2 -0
  102. package/types/api.d.ts +1 -0
  103. package/types/fleet.d.ts +1 -0
  104. package/types/shell/index.d.ts +695 -2
  105. package/types/userPreferences.d.ts +1 -1
  106. package/utils/__mocks__/socket.js +21 -0
  107. package/utils/grafana.js +23 -11
  108. package/utils/selector.js +2 -1
  109. package/utils/validators/formRules/index.ts +3 -3
  110. package/plugins/steve/urloptions.js +0 -47
@@ -0,0 +1,76 @@
1
+ /* eslint-disable jest/no-hooks */
2
+ import FileSelector from '@shell/components/form/FileSelector';
3
+ import { mount } from '@vue/test-utils';
4
+
5
+ describe('component: FileSelector', () => {
6
+ let wrapper: any;
7
+
8
+ beforeEach(() => {
9
+ jest.restoreAllMocks();
10
+ });
11
+ afterEach(() => {
12
+ wrapper.destroy();
13
+ });
14
+
15
+ const binaryString = Buffer.from('/9j/4AAQSkZJRgABAQAASABIAAD/4QCARXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAKgAgAEAAAAAQAAADmgAwAEAAAAAQAAAFEAAAAA/+0AOFBob3Rvc2hvcCAzLjAAOEJJTQQEAAAAAAAAOEJJTQQlAAAAAAAQ1B2M2Y8AsgTpgAmY7PhCfv/iAihJQ0NfUFJPRklMRQABAQAAAhgAAAAABDAAAG1udHJSR0IgWFlaIAAAAAAAAAAAAAAAAGFjc3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD21gABAAAAANMtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACWRlc2MAAADwAAAAdHJYWVoAAAFkAAAAFGdYWVoAAAF4AAAAFGJYWVoAAAGMAAAAFHJUUkMAAAGgAAAAKGdUUkMAAAGgAAAAKGJUUkMAAAGgAAAAKHd0cHQAAAHIAAAAFGNwcnQAAAHcAAAAPG1sdWMAAAAAAAAAAQAAAAxlblVTAAAAWAAAABwAcwBSAEcAQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAG+iAAA49QAAA5BYWVogAAAAAAAAYpkAALeFAAAY2lhZWiAAAAAAAAAkoAAAD4QAALbPcGFyYQAAAAAABAAAAAJmZgAA8qcAAA1ZAAAT0AAAClsAAAAAAAAAAFhZWiAAAAAAAAD21gABAAAAANMtbWx1YwAAAAAAAAABAAAADGVuVVMAAAAgAAAAHABHAG8AbwBnAGwAZQAgAEkAbgBjAC4AIAAyADAAMQA2/8AAEQgAUQA5AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//dAAQABP/aAAwDAQACEQMRAD8AdRRRX0B4gUUUUAFFFFABRRRQB//QdRRRX0B4gUUUUAFFFFABRRRQB//RdRRRX0B4gUUUUAFFFFABRRRQB//SdRRRX0B4gUUUUAFFFFABRRRQB//TdRRRX0B4gUUUUAFFFFAwooooA//UdRRRX0B4gUUUUAFFFFAwooooA//Z', 'base64'); // Binary data string
16
+ const jpegBlobFile = new Blob([binaryString], { type: 'image/jpeg' });
17
+ const obj = { hello: 'world' };
18
+ const jsonBlobFile = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' });
19
+
20
+ it('should render', () => {
21
+ wrapper = mount(FileSelector, {
22
+ propsData: { label: 'upload' },
23
+ mocks: {},
24
+ methods: {},
25
+ });
26
+
27
+ const uploadButton = wrapper.find('.btn');
28
+
29
+ expect(wrapper.isVisible()).toBe(true);
30
+ expect(uploadButton.exists()).toBeTruthy();
31
+ });
32
+
33
+ it('should succeed when loading an image', async() => {
34
+ wrapper = mount(FileSelector, {
35
+ propsData: { label: 'upload', accept: 'image/jpeg,image/png,image/svg+xml' },
36
+ mocks: {},
37
+ methods: {},
38
+ });
39
+ const readAsTextSpy = jest.spyOn(FileReader.prototype, 'readAsText');
40
+
41
+ const event = {
42
+ target: {
43
+ files: [
44
+ jpegBlobFile
45
+ ]
46
+ }
47
+ };
48
+
49
+ await wrapper.vm.fileChange(event);
50
+ expect(wrapper.emitted('selected')).toHaveLength(1);
51
+ expect(readAsTextSpy).toHaveBeenCalledWith(jpegBlobFile);
52
+ });
53
+
54
+ it('should fail when file is too big', async() => {
55
+ wrapper = mount(FileSelector, {
56
+ propsData: {
57
+ label: 'upload', accept: 'image/jpeg,image/png,image/svg+xml', byteLimit: 10
58
+ },
59
+ mocks: {},
60
+ methods: {},
61
+ });
62
+ const readAsTextSpy = jest.spyOn(FileReader.prototype, 'readAsText');
63
+
64
+ const event = {
65
+ target: {
66
+ files: [
67
+ jpegBlobFile
68
+ ]
69
+ }
70
+ };
71
+
72
+ await wrapper.vm.fileChange(event);
73
+ expect(wrapper.emitted('error')).toHaveLength(1);
74
+ expect(readAsTextSpy).not.toHaveBeenCalledWith(jsonBlobFile);
75
+ });
76
+ });
@@ -12,7 +12,9 @@ export default {
12
12
  // model doesn't work for imported K3s clusters, in
13
13
  // which case it returns 'k3s' instead of 'imported.'
14
14
  // This is the workaround.
15
- isImported: props.row.mgmt?.providerForEmberParam === 'import'
15
+ isImported: props.row?.mgmt?.providerForEmberParam === 'import' ||
16
+ // when imported cluster is Google GKE
17
+ props.row?.mgmt?.spec?.gkeConfig?.imported
16
18
  };
17
19
  },
18
20
  };
@@ -0,0 +1,24 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import ClusterProvider from '@shell/components/formatter/ClusterProvider.vue';
3
+
4
+ describe('component: ClusterProvider', () => {
5
+ const importedGkeClusterInfo = { mgmt: { spec: { gkeConfig: { imported: true } } } };
6
+ const notImportedGkeClusterInfo = { mgmt: { spec: { gkeConfig: { imported: false } } } };
7
+ const importedClusterInfoWithProviderForEmberParam = { mgmt: { providerForEmberParam: 'import' } };
8
+
9
+ describe('isImported', () => {
10
+ const testCases = [
11
+ [importedGkeClusterInfo, true],
12
+ [notImportedGkeClusterInfo, false],
13
+ [importedClusterInfoWithProviderForEmberParam, true],
14
+ [{}, undefined],
15
+ ];
16
+
17
+ it.each(testCases)('should return the isImported value properly based on the props data', (row, expected) => {
18
+ const wrapper = mount(ClusterProvider, { propsData: { row } });
19
+
20
+ expect(wrapper.vm.$data.isImported).toBe(expected);
21
+ }
22
+ );
23
+ });
24
+ });
@@ -3,7 +3,6 @@ import { allHash } from '@shell/utils/promise';
3
3
  import { addParams } from '@shell/utils/url';
4
4
  import { base64Decode, base64Encode } from '@shell/utils/crypto';
5
5
  import Select from '@shell/components/form/Select';
6
- import isEmpty from 'lodash/isEmpty';
7
6
  import { NODE } from '@shell/config/types';
8
7
 
9
8
  import Socket, {
@@ -16,11 +15,14 @@ import Socket, {
16
15
  } from '@shell/utils/socket';
17
16
  import Window from './Window';
18
17
 
19
- const DEFAULT_COMMAND = [
20
- '/bin/sh',
21
- '-c',
22
- 'TERM=xterm-256color; export TERM; [ -x /bin/bash ] && ([ -x /usr/bin/script ] && /usr/bin/script -q -c "/bin/bash" /dev/null || exec /bin/bash) || exec /bin/sh',
23
- ];
18
+ const commands = {
19
+ linux: [
20
+ '/bin/sh',
21
+ '-c',
22
+ 'TERM=xterm-256color; export TERM; [ -x /bin/bash ] && ([ -x /usr/bin/script ] && /usr/bin/script -q -c "/bin/bash" /dev/null || exec /bin/bash) || exec /bin/sh',
23
+ ],
24
+ windows: ['cmd']
25
+ };
24
26
 
25
27
  export default {
26
28
  components: { Window, Select },
@@ -40,8 +42,8 @@ export default {
40
42
 
41
43
  // The height of the window
42
44
  height: {
43
- type: Number,
44
- required: true,
45
+ type: Number,
46
+ default: undefined,
45
47
  },
46
48
 
47
49
  // The width of the window
@@ -82,6 +84,10 @@ export default {
82
84
  backlog: [],
83
85
  node: null,
84
86
  keepAliveTimer: null,
87
+ errorMsg: '',
88
+ backupShells: ['linux', 'windows'],
89
+ os: undefined,
90
+ retries: 0
85
91
  };
86
92
  },
87
93
 
@@ -120,7 +126,16 @@ export default {
120
126
  },
121
127
 
122
128
  async mounted() {
123
- await this.fetchNode();
129
+ const nodeId = this.pod.spec?.nodeName;
130
+
131
+ try {
132
+ const schema = this.$store.getters[`cluster/schemaFor`](NODE);
133
+
134
+ if (schema) {
135
+ await this.$store.dispatch('cluster/find', { type: NODE, id: nodeId });
136
+ }
137
+ } catch {}
138
+
124
139
  await this.setupTerminal();
125
140
  await this.connect();
126
141
 
@@ -131,26 +146,6 @@ export default {
131
146
  },
132
147
 
133
148
  methods: {
134
- async fetchNode() {
135
- if (this.node) {
136
- return;
137
- }
138
-
139
- const nodeId = this.pod.spec?.nodeName;
140
-
141
- if ( !nodeId ) {
142
- return;
143
- }
144
-
145
- try {
146
- this.node = await this.$store.dispatch('cluster/find', {
147
- type: NODE,
148
- id: nodeId,
149
- });
150
- } catch (e) {
151
- console.error('Failed to fetch node', nodeId); // eslint-disable-line no-console
152
- }
153
- },
154
149
  async setupTerminal() {
155
150
  const docStyle = getComputedStyle(document.querySelector('body'));
156
151
  const xterm = await import(/* webpackChunkName: "xterm" */ 'xterm');
@@ -220,11 +215,11 @@ export default {
220
215
  return;
221
216
  }
222
217
 
223
- const { node } = this;
224
- let cmd = DEFAULT_COMMAND;
225
-
226
- if (!isEmpty(node) && node?.status?.nodeInfo?.operatingSystem === 'windows') {
227
- cmd = ['cmd'];
218
+ if (this.pod.os) {
219
+ this.os = this.pod.os;
220
+ this.backupShells = this.backupShells.filter((shell) => shell !== this.pod.os);
221
+ } else {
222
+ this.os = this.backupShells.shift();
228
223
  }
229
224
 
230
225
  const url = addParams(
@@ -235,7 +230,7 @@ export default {
235
230
  stdin: 1,
236
231
  stderr: 1,
237
232
  tty: 1,
238
- command: cmd,
233
+ command: commands[this.os],
239
234
  }
240
235
  );
241
236
 
@@ -260,6 +255,7 @@ export default {
260
255
  this.socket.addEventListener(EVENT_CONNECTING, (e) => {
261
256
  this.isOpen = false;
262
257
  this.isOpening = true;
258
+ this.errorMsg = '';
263
259
  });
264
260
 
265
261
  this.socket.addEventListener(EVENT_CONNECT_ERROR, (e) => {
@@ -282,16 +278,44 @@ export default {
282
278
  this.socket.addEventListener(EVENT_DISCONNECTED, (e) => {
283
279
  this.isOpen = false;
284
280
  this.isOpening = false;
281
+
282
+ // If we had an error message, try connecting with the next command
283
+ if (this.errorMsg) {
284
+ this.terminal.write(this.errorMsg);
285
+ if (this.backupShells.length && this.retries < 2) {
286
+ this.retries++;
287
+ // we're not really counting on this being a reactive change so there's no need to fire the whole action
288
+ this.pod.os = undefined;
289
+ // the pod will still return an os if one's been defined in the node so we'll skip the backups if that's the case and rely on retry count to break the retry loop
290
+ if (!this.pod.os) {
291
+ this.os = this.backupShells.shift();
292
+ }
293
+ this.connect();
294
+ } else {
295
+ // Output an message to let he user know none of the shell commands worked
296
+ this.terminal.write(this.t('wm.containerShell.failed'));
297
+ }
298
+ }
285
299
  });
286
300
 
287
301
  this.socket.addEventListener(EVENT_MESSAGE, (e) => {
288
302
  const type = e.detail.data.substr(0, 1);
289
303
  const msg = base64Decode(e.detail.data.substr(1));
290
304
 
305
+ this.errorMsg = '';
306
+
291
307
  if (`${ type }` === '1') {
308
+ if (msg) {
309
+ // we're not really counting on this being a reactive change so there's no need to fire the whole action
310
+ this.pod.os = this.os;
311
+ }
292
312
  this.terminal.write(msg);
293
313
  } else {
294
314
  console.error(msg); // eslint-disable-line no-console
315
+
316
+ if (`${ type }` === '3') {
317
+ this.errorMsg = msg;
318
+ }
295
319
  }
296
320
  });
297
321
 
@@ -316,7 +340,7 @@ export default {
316
340
 
317
341
  this.fitAddon.fit();
318
342
 
319
- const { rows, cols } = this.fitAddon.proposeDimensions();
343
+ const { rows, cols } = this.fitAddon.proposeDimensions() || {};
320
344
 
321
345
  if (!this.isOpen) {
322
346
  return;