@rancher/shell 0.3.18 → 0.3.20

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.
@@ -3041,6 +3041,7 @@ login:
3041
3041
  noResponse: "No response received"
3042
3042
  error: An error occurred logging in. Please try again.
3043
3043
  clientError: Invalid username or password. Please try again.
3044
+ specificError: 'An error occured logging in: {msg}'
3044
3045
  useLocal: Use a local user
3045
3046
  loginWithProvider: Log in with {provider}
3046
3047
  username: Username
@@ -42,6 +42,9 @@ export default {
42
42
  const isSticky = !!this.modalData?.modalSticky;
43
43
 
44
44
  return !isSticky ? '' : 'display: flex; flex-direction: column; ';
45
+ },
46
+ closeOnClickOutside() {
47
+ return this.modalData?.closeOnClickOutside;
45
48
  }
46
49
  },
47
50
 
@@ -85,6 +88,7 @@ export default {
85
88
  :styles="`background-color: var(--nav-bg); border-radius: var(--border-radius); ${stickyProps} max-height: 95vh; ${cssProps}`"
86
89
  height="auto"
87
90
  :scrollable="true"
91
+ :click-to-close="closeOnClickOutside"
88
92
  @closed="close()"
89
93
  >
90
94
  <component
@@ -116,7 +116,7 @@ export default {
116
116
 
117
117
  if (!cluster?.isRke2) {
118
118
  promise = this.$store.dispatch('rancher/findAll', { type: NORMAN.ETCD_BACKUP }).then((snapshots) => {
119
- return snapshots.filter((s) => s.clusterId === cluster.metadata.name);
119
+ return snapshots.filter((s) => s.state === STATES_ENUM.ACTIVE && s.clusterId === cluster.metadata.name);
120
120
  });
121
121
  } else {
122
122
  promise = this.$store.dispatch('management/findAll', { type: SNAPSHOT }).then((snapshots) => {
@@ -400,7 +400,10 @@ export default {
400
400
 
401
401
  pagingParams() {
402
402
  if ( !this.schema ) {
403
- return {};
403
+ return {
404
+ singularLabel: '',
405
+ pluralLabel: ''
406
+ };
404
407
  }
405
408
 
406
409
  return {
@@ -3,55 +3,87 @@ import PromptRestore from '@shell/components/PromptRestore.vue';
3
3
  import Vuex from 'vuex';
4
4
  import { ExtendedVue, Vue } from 'vue/types/vue';
5
5
  import { DefaultProps } from 'vue/types/options';
6
- import { CAPI } from '@shell/config/types';
6
+ import { CAPI, NORMAN } from '@shell/config/types';
7
7
  import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
8
8
 
9
- const SUCCESSFUL_SNAPSHOT_1 = {
10
- isRke2: true,
9
+ const RKE2_CLUSTER_NAME = 'rke2_cluster_name';
10
+ const RKE2_SUCCESSFUL_SNAPSHOT_1 = {
11
+ clusterName: RKE2_CLUSTER_NAME,
11
12
  type: CAPI.RANCHER_CLUSTER,
12
13
  created: 'Thu Jul 20 2023 11:11:39',
13
14
  snapshotFile: { status: STATES_ENUM.SUCCESSFUL },
14
- id: 'id',
15
- name: 'name'
15
+ id: 'rke2_id_1',
16
+ name: 'rke2_name_1'
16
17
  };
17
- const SUCCESSFUL_SNAPSHOT_2 = {
18
- isRke2: true,
18
+ const RKE2_SUCCESSFUL_SNAPSHOT_2 = {
19
+ clusterName: RKE2_CLUSTER_NAME,
19
20
  type: CAPI.RANCHER_CLUSTER,
20
21
  created: 'Thu Jul 20 2022 11:11:39',
21
22
  snapshotFile: { status: STATES_ENUM.SUCCESSFUL },
22
- id: 'id2',
23
- name: 'name2'
23
+ id: 'rke2_id_2',
24
+ name: 'rke2_name_2'
24
25
  };
25
- const FAILED_SNAPSHOT = {
26
- isRke2: true,
26
+ const RKE2_FAILED_SNAPSHOT = {
27
+ clusterName: RKE2_CLUSTER_NAME,
27
28
  type: CAPI.RANCHER_CLUSTER,
28
29
  created: 'Thu Jul 20 2021 11:11:39',
29
30
  snapshotFile: { status: STATES_ENUM.FAILED },
30
- id: 'id_3',
31
- name: 'name_3'
31
+ id: 'rke2_id_3',
32
+ name: 'rke2_name_3'
32
33
  };
33
34
 
34
- describe('component: GrowlManager', () => {
35
+ const RKE1_CLUSTER_NAME = 'rke1_cluster_name';
36
+ const RKE1_SUCCESSFUL_SNAPSHOT_1 = {
37
+ clusterId: RKE1_CLUSTER_NAME,
38
+ type: NORMAN.ETCD_BACKUP,
39
+ state: STATES_ENUM.ACTIVE,
40
+ created: 'Thu Jul 30 2023 11:11:39',
41
+ id: 'rke1_id_1',
42
+ name: 'rke1_name_1'
43
+ };
44
+ const RKE1_SUCCESSFUL_SNAPSHOT_2 = {
45
+ clusterId: RKE1_CLUSTER_NAME,
46
+ type: NORMAN.ETCD_BACKUP,
47
+ state: STATES_ENUM.ACTIVE,
48
+ created: 'Thu Jul 30 2022 11:11:39',
49
+ id: 'rke1_id_2',
50
+ name: 'rke1_name_2'
51
+ };
52
+ const RKE1_WITH_ERROR_SNAPSHOT = {
53
+ clusterId: RKE1_CLUSTER_NAME,
54
+ type: NORMAN.ETCD_BACKUP,
55
+ state: STATES_ENUM.ERROR,
56
+ created: 'Thu Jul 30 2021 11:11:39',
57
+ id: 'rke1_id_3',
58
+ name: 'rke1_name_3'
59
+ };
60
+
61
+ describe('component: PromptRestore', () => {
35
62
  const localVue = createLocalVue();
36
63
 
37
64
  localVue.use(Vuex);
38
65
 
39
- const testCases = [
66
+ const rke2TestCases = [
40
67
  [[], 0],
41
- [[FAILED_SNAPSHOT], 0],
42
- [[SUCCESSFUL_SNAPSHOT_1], 1],
43
- [[SUCCESSFUL_SNAPSHOT_1, SUCCESSFUL_SNAPSHOT_2], 2],
44
- [[FAILED_SNAPSHOT, SUCCESSFUL_SNAPSHOT_1, SUCCESSFUL_SNAPSHOT_2], 2]
68
+ [[RKE2_FAILED_SNAPSHOT], 0],
69
+ [[RKE2_SUCCESSFUL_SNAPSHOT_1], 1],
70
+ [[RKE2_SUCCESSFUL_SNAPSHOT_1, RKE2_SUCCESSFUL_SNAPSHOT_2], 2],
71
+ [[RKE2_FAILED_SNAPSHOT, RKE2_SUCCESSFUL_SNAPSHOT_1, RKE2_SUCCESSFUL_SNAPSHOT_2], 2]
45
72
  ];
46
73
 
47
- it.each(testCases)('should list RKE2 snapshots properly', async(snapShots, expected) => {
74
+ it.each(rke2TestCases)('should list RKE2 snapshots properly', async(snapShots, expected) => {
48
75
  const store = new Vuex.Store({
49
76
  modules: {
50
77
  'action-menu': {
51
78
  namespaced: true,
52
79
  state: {
53
80
  showPromptRestore: true,
54
- toRestore: snapShots
81
+ toRestore: [{
82
+ isRke2: true,
83
+ type: CAPI.RANCHER_CLUSTER,
84
+ metadata: { name: RKE2_CLUSTER_NAME },
85
+ snapShots
86
+ }]
55
87
  },
56
88
  },
57
89
  },
@@ -69,4 +101,42 @@ describe('component: GrowlManager', () => {
69
101
 
70
102
  expect(wrapper.vm.clusterSnapshots).toHaveLength(expected);
71
103
  });
104
+
105
+ const rke1TestCases = [
106
+ [[], 0],
107
+ [[RKE1_WITH_ERROR_SNAPSHOT], 0],
108
+ [[RKE1_SUCCESSFUL_SNAPSHOT_1], 1],
109
+ [[RKE1_SUCCESSFUL_SNAPSHOT_1, RKE1_SUCCESSFUL_SNAPSHOT_2], 2],
110
+ [[RKE1_WITH_ERROR_SNAPSHOT, RKE1_SUCCESSFUL_SNAPSHOT_1, RKE1_SUCCESSFUL_SNAPSHOT_2], 2]
111
+ ];
112
+
113
+ it.each(rke1TestCases)('should list RKE1 snapshots properly', async(snapShots, expected) => {
114
+ const store = new Vuex.Store({
115
+ modules: {
116
+ 'action-menu': {
117
+ namespaced: true,
118
+ state: {
119
+ showPromptRestore: true,
120
+ toRestore: [{
121
+ type: CAPI.RANCHER_CLUSTER,
122
+ metadata: { name: RKE1_CLUSTER_NAME },
123
+ snapShots
124
+ }]
125
+ },
126
+ },
127
+ },
128
+ getters: { 'i18n/t': () => jest.fn(), 'prefs/get': () => jest.fn() },
129
+ actions: { 'rancher/findAll': jest.fn().mockResolvedValue(snapShots) }
130
+ });
131
+
132
+ const wrapper = shallowMount(PromptRestore as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>, {
133
+ store,
134
+ localVue
135
+ });
136
+
137
+ await wrapper.vm.fetchSnapshots();
138
+ await wrapper.vm.$nextTick();
139
+
140
+ expect(wrapper.vm.clusterSnapshots).toHaveLength(expected);
141
+ });
72
142
  });
@@ -579,6 +579,7 @@ export default {
579
579
  name="storageSource"
580
580
  :label="defaultLabel"
581
581
  class="mb-10"
582
+ data-testid="roletemplate-creator-default-options"
582
583
  :options="newUserDefaultOptions"
583
584
  :mode="mode"
584
585
  />
@@ -592,6 +593,7 @@ export default {
592
593
  name="storageSource"
593
594
  :label="t('rbac.roletemplate.locked.label')"
594
595
  class="mb-10"
596
+ data-testid="roletemplate-locked-options"
595
597
  :options="lockedOptions"
596
598
  :mode="mode"
597
599
  />
@@ -288,7 +288,7 @@ export default {
288
288
  this.pod.os = undefined;
289
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
290
  if (!this.pod.os) {
291
- this.os = this.backupShells.shift();
291
+ this.os = undefined;
292
292
  }
293
293
  this.connect();
294
294
  } else {
@@ -322,11 +322,11 @@ describe('component: ContainerShell', () => {
322
322
  expect(wrapper.vm.isOpening).toBe(false);
323
323
  expect(wrapper.vm.errorMsg).toBe('eventMessageError');
324
324
  // the backup shell that was leftover was windows so it became the new os in dataprops
325
- expect(wrapper.vm.os).toBe('windows');
325
+ expect(wrapper.vm.os).toBeUndefined();
326
326
  // but we still didn't write it to the pod itself since we don't know if it worked
327
327
  expect(defaultContainerShellParams.propsData.pod.os).toBeUndefined();
328
328
  // we can see here that we removed that last backup shell because we're attempting to use it now
329
- expect(wrapper.vm.backupShells).toHaveLength(0);
329
+ expect(wrapper.vm.backupShells).toHaveLength(1);
330
330
  expect(connect.mock.calls).toHaveLength(2);
331
331
  });
332
332
 
@@ -351,8 +351,8 @@ describe('component: ContainerShell', () => {
351
351
  eventMessage({ detail: { data: `3${ linuxErrorMessage }` } });
352
352
  eventDisconnected();
353
353
 
354
- expect(wrapper.vm.backupShells).toHaveLength(0);
355
- expect(wrapper.vm.os).toBe('windows');
354
+ expect(wrapper.vm.backupShells).toHaveLength(1);
355
+ expect(wrapper.vm.os).toBeUndefined();
356
356
  // the pod os was 'linux' but we cleared it out since that didn't work
357
357
  expect(defaultContainerShellParams.propsData.pod.os).toBeUndefined();
358
358
  expect(connect.mock.calls).toHaveLength(2);
@@ -367,11 +367,11 @@ describe('component: ContainerShell', () => {
367
367
  expect(wrapper.vm.isOpen).toBe(false);
368
368
  expect(wrapper.vm.isOpening).toBe(false);
369
369
  expect(wrapper.vm.errorMsg).toBe(windowsErrorMessage);
370
- expect(wrapper.vm.os).toBe('windows');
370
+ expect(wrapper.vm.os).toBeUndefined();
371
371
  // we never found a shell that worked so we're going to leave the pod os as undefined
372
372
  expect(defaultContainerShellParams.propsData.pod.os).toBeUndefined();
373
373
  // we're out of backupShells now so we're not going to retry after that second disconnect
374
- expect(connect.mock.calls).toHaveLength(2);
374
+ expect(connect.mock.calls).toHaveLength(3);
375
375
 
376
376
  resetMocks();
377
377
  });
@@ -409,8 +409,8 @@ describe('component: ContainerShell', () => {
409
409
  eventMessage({ detail: { data: `3${ linuxErrorMessage }` } });
410
410
  eventDisconnected();
411
411
 
412
- expect(wrapper.vm.backupShells).toHaveLength(0);
413
- expect(wrapper.vm.os).toBe('windows');
412
+ expect(wrapper.vm.backupShells).toHaveLength(1);
413
+ expect(wrapper.vm.os).toBeUndefined();
414
414
  expect(testUndefinedOsParams.propsData.pod.os).toBeUndefined();
415
415
 
416
416
  eventConnecting();
@@ -423,9 +423,9 @@ describe('component: ContainerShell', () => {
423
423
  expect(wrapper.vm.isOpen).toBe(false);
424
424
  expect(wrapper.vm.isOpening).toBe(false);
425
425
  expect(wrapper.vm.errorMsg).toBe(windowsErrorMessage);
426
- expect(wrapper.vm.os).toBe('windows');
426
+ expect(wrapper.vm.os).toBeUndefined();
427
427
  expect(testUndefinedOsParams.propsData.pod.os).toBeUndefined();
428
- expect(connect.mock.calls).toHaveLength(2);
428
+ expect(connect.mock.calls).toHaveLength(3);
429
429
 
430
430
  resetMocks();
431
431
  });
@@ -461,8 +461,8 @@ describe('component: ContainerShell', () => {
461
461
  eventMessage({ detail: { data: `3${ linuxErrorMessage }` } });
462
462
  eventDisconnected();
463
463
 
464
- expect(wrapper.vm.backupShells).toHaveLength(0);
465
- expect(wrapper.vm.os).toBe('windows');
464
+ expect(wrapper.vm.backupShells).toHaveLength(1);
465
+ expect(wrapper.vm.os).toBeUndefined();
466
466
  expect(testUndefinedOsParams.propsData.pod.os).toBeUndefined();
467
467
  expect(wrapper.vm.errorMsg).toBe(linuxErrorMessage);
468
468
 
@@ -475,9 +475,9 @@ describe('component: ContainerShell', () => {
475
475
  expect(wrapper.vm.isOpen).toBe(true);
476
476
  expect(wrapper.vm.isOpening).toBe(false);
477
477
  expect(wrapper.vm.errorMsg).toBe('');
478
- expect(wrapper.vm.os).toBe('windows');
478
+ expect(wrapper.vm.os).toBeUndefined();
479
479
  // the second shell worked so we're going to set it on the pod itself so if we need to connect again we'll just use the correct shell on the first attempt
480
- expect(testUndefinedOsParams.propsData.pod.os).toBe('windows');
480
+ expect(testUndefinedOsParams.propsData.pod.os).toBeUndefined();
481
481
  expect(connect.mock.calls).toHaveLength(2);
482
482
 
483
483
  resetMocks();
package/core/types.ts CHANGED
@@ -364,6 +364,23 @@ export interface ConfigureTypeOptions {
364
364
  // showConfigView
365
365
  }
366
366
 
367
+ export interface ConfigureVirtualTypeOptions extends ConfigureTypeOptions {
368
+ /**
369
+ * The translation key displayed anywhere this type is referenced
370
+ */
371
+ labelKey: string;
372
+
373
+ /**
374
+ * An identifier that should be unique across all types
375
+ */
376
+ name: string;
377
+
378
+ /**
379
+ * The route that this type should correspond to {@link PluginRouteConfig} {@link RouteConfig}
380
+ */
381
+ route: PluginRouteConfig | RouteConfig;
382
+ }
383
+
367
384
  export interface DSLReturnType {
368
385
  /**
369
386
  * Register multiple types by name and place them all in a group if desired. Primarily used for grouping things in the cluster explorer navigation.
@@ -404,6 +421,13 @@ export interface DSLReturnType {
404
421
  */
405
422
  mapGroup: (groupName: string, label: string) => void;
406
423
 
424
+ /**
425
+ * Create and configure a myriad of options for a type
426
+ * @param options {@link ConfigureVirtualTypeOptions}
427
+ * @returns {@link void}
428
+ */
429
+ virtualType: (options: ConfigureVirtualTypeOptions) => void;
430
+
407
431
  /**
408
432
  * Leaving these here for completeness but I don't think these should be advertised as useable to plugin creators.
409
433
  */
@@ -417,7 +441,6 @@ export interface DSLReturnType {
417
441
  // moveType: (match, group)
418
442
  // setGroupDefaultType: (input, defaultType)
419
443
  // spoofedType: (obj)
420
- // virtualType: (obj)
421
444
  // weightGroup: (input, weight, forBasic)
422
445
  // weightType: (input, weight, forBasic)
423
446
  }
package/detail/node.vue CHANGED
@@ -82,8 +82,8 @@ export default {
82
82
  }
83
83
  ],
84
84
  imageTableHeaders: [
85
- { ...SIMPLE_NAME, width: 400 },
86
- IMAGE_SIZE
85
+ { ...SIMPLE_NAME, width: null },
86
+ { ...IMAGE_SIZE, width: 100 } // Ensure one header has a size, all other columns will scale
87
87
  ],
88
88
  taintTableHeaders: [
89
89
  KEY,
@@ -111,6 +111,7 @@ export default {
111
111
 
112
112
  <AsyncButton
113
113
  :mode="applyMode"
114
+ data-testid="download-diagnostics-modal-action"
114
115
  @click="apply"
115
116
  />
116
117
  </div>
@@ -8,9 +8,10 @@ import { LabeledInput } from '@components/Form/LabeledInput';
8
8
  import { RadioGroup } from '@components/Form/Radio';
9
9
  import FileImageSelector from '@shell/components/form/FileImageSelector';
10
10
  import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
11
- import NameNsDescription, { normalizeName } from '@shell/components/form/NameNsDescription';
11
+ import NameNsDescription from '@shell/components/form/NameNsDescription';
12
12
  import { Banner } from '@components/Banner';
13
13
  import FormValidation from '@shell/mixins/form-validation';
14
+ import { normalizeName } from '@shell/utils/kube';
14
15
 
15
16
  const LINK_TYPE_URL = 'url';
16
17
  const LINK_TYPE_SERVICE = 'service';
@@ -1,10 +1,11 @@
1
1
  import NormanModel from '@shell/plugins/steve/norman-class';
2
+ import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
2
3
 
3
4
  export default class Rke1EtcdBackup extends NormanModel {
4
5
  get _availableActions() {
5
6
  const restore = {
6
7
  action: 'promptRestore',
7
- enabled: true,
8
+ enabled: this.state === STATES_ENUM.ACTIVE,
8
9
  icon: 'icon icon-fw icon-backup-restore',
9
10
  label: 'Restore'
10
11
  };
@@ -19,6 +19,13 @@ import { KONTAINER_TO_DRIVER } from './management.cattle.io.kontainerdriver';
19
19
  // If the logo is not named with the provider name, add an override here
20
20
  const PROVIDER_LOGO_OVERRIDE = {};
21
21
 
22
+ function findRelationship(verb, type, relationships = []) {
23
+ const from = `${ verb }Type`;
24
+ const id = `${ verb }Id`;
25
+
26
+ return relationships.find((r) => r[from] === type)?.[id];
27
+ }
28
+
22
29
  export default class MgmtCluster extends HybridModel {
23
30
  get details() {
24
31
  const out = [
@@ -440,9 +447,12 @@ export default class MgmtCluster extends HybridModel {
440
447
  // cluster has the less human readable management cluster ID in it: fleet-default/c-khk48
441
448
 
442
449
  const verb = this.isLocal || isRKE1 || this.isHostedKubernetesProvider ? 'to' : 'from';
443
- const from = `${ verb }Type`;
444
- const id = `${ verb }Id`;
450
+ const res = findRelationship(verb, CAPI.RANCHER_CLUSTER, this.metadata?.relationships);
451
+
452
+ if (res) {
453
+ return res;
454
+ }
445
455
 
446
- return this.metadata.relationships.find((r) => r[from] === CAPI.RANCHER_CLUSTER)?.[id];
456
+ return findRelationship(verb === 'to' ? 'from' : 'to', CAPI.RANCHER_CLUSTER, this.metadata?.relationships);
447
457
  }
448
458
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancherlabs/dashboard",
6
6
  "license": "Apache-2.0",
@@ -149,7 +149,7 @@ export default {
149
149
  return this.t('login.error');
150
150
  }
151
151
 
152
- return this.err;
152
+ return this.err?.length ? this.t('login.specificError', { msg: this.err }) : '';
153
153
  },
154
154
 
155
155
  errorToDisplay() {
@@ -81,6 +81,9 @@ export default {
81
81
  ],
82
82
 
83
83
  async fetch() {
84
+ this.errors = [];
85
+ // IMPORTANT! Any exception thrown before this.value is set will result in an empty page
86
+
84
87
  /*
85
88
  fetchChart is defined in shell/mixins. It first checks the URL
86
89
  query for an app name and namespace. It uses those values to check
@@ -91,23 +94,43 @@ export default {
91
94
  it checks for target name and namespace values defined in the
92
95
  Helm chart itself.
93
96
  */
94
- await this.fetchChart();
97
+ try {
98
+ await this.fetchChart();
99
+ } catch (e) {
100
+ console.warn('Unable to fetch chart: ', e); // eslint-disable-line no-console
101
+ }
95
102
 
96
- await this.fetchAutoInstallInfo();
97
- this.errors = [];
103
+ try {
104
+ await this.fetchAutoInstallInfo();
105
+ } catch (e) {
106
+ console.warn('Unable to determine if other charts require install: ', e); // eslint-disable-line no-console
107
+ }
98
108
 
99
109
  // If the chart doesn't contain system `systemDefaultRegistry` properties there's no point applying them
100
110
  if (this.showCustomRegistry) {
101
111
  // Note: Cluster scoped registry is only supported for node driver clusters
102
- this.clusterRegistry = await this.getClusterRegistry();
103
- this.globalRegistry = await this.getGlobalRegistry();
112
+ try {
113
+ this.clusterRegistry = await this.getClusterRegistry();
114
+ } catch (e) {
115
+ console.warn('Unable to get cluster registry: ', e); // eslint-disable-line no-console
116
+ }
117
+
118
+ try {
119
+ this.globalRegistry = await this.getGlobalRegistry();
120
+ } catch (e) {
121
+ console.warn('Unable to get global registry: ', e); // eslint-disable-line no-console
122
+ }
104
123
  this.defaultRegistrySetting = this.clusterRegistry || this.globalRegistry;
105
124
  }
106
125
 
107
- this.serverUrlSetting = await this.$store.dispatch('management/find', {
108
- type: MANAGEMENT.SETTING,
109
- id: 'server-url'
110
- });
126
+ try {
127
+ this.serverUrlSetting = await this.$store.dispatch('management/find', {
128
+ type: MANAGEMENT.SETTING,
129
+ id: 'server-url'
130
+ });
131
+ } catch (e) {
132
+ console.error('Unable to fetch `server-url` setting: ', e); // eslint-disable-line no-console
133
+ }
111
134
 
112
135
  /*
113
136
  Figure out the namespace where the chart is
@@ -137,20 +160,38 @@ export default {
137
160
  }
138
161
 
139
162
  /* Check if the app is deprecated. */
140
- this.legacyApp = this.existing ? await this.existing.deployedAsLegacy() : false;
163
+ try {
164
+ this.legacyApp = this.existing ? await this.existing.deployedAsLegacy() : false;
165
+ } catch (e) {
166
+ this.legacyApp = false;
167
+ console.warn('Unable to determine if existing install is a legacy app: ', e); // eslint-disable-line no-console
168
+ }
141
169
 
142
170
  /* Check if the app is a multicluster deprecated app.
143
171
  (Multicluster apps were replaced by Fleet.) */
144
- this.mcapp = this.existing ? await this.existing.deployedAsMultiCluster() : false;
172
+
173
+ try {
174
+ this.mcapp = this.existing ? await this.existing.deployedAsMultiCluster() : false;
175
+ } catch (e) {
176
+ this.mcapp = false;
177
+ console.warn('Unable to determine if existing install is a mc app: ', e); // eslint-disable-line no-console
178
+ }
145
179
 
146
180
  /* The form state is intialized as a chartInstallAction resource. */
147
- this.value = await this.$store.dispatch('cluster/create', {
148
- type: 'chartInstallAction',
149
- metadata: {
150
- namespace: this.forceNamespace || this.$store.getters['defaultNamespace'],
151
- name: this.existing?.spec?.name || this.query.appName || '',
152
- }
153
- });
181
+ try {
182
+ this.value = await this.$store.dispatch('cluster/create', {
183
+ type: 'chartInstallAction',
184
+ metadata: {
185
+ namespace: this.forceNamespace || this.$store.getters['defaultNamespace'],
186
+ name: this.existing?.spec?.name || this.query.appName || '',
187
+ }
188
+ });
189
+ } catch (e) {
190
+ console.error('Unable to create object of type `chartInstallAction`: ', e); // eslint-disable-line no-console
191
+
192
+ // Nothing's going to work without a `value`. See https://github.com/rancher/dashboard/issues/9452 to handle this and other catches.
193
+ return;
194
+ }
154
195
 
155
196
  /* Logic for when the Helm chart is not already installed */
156
197
  if ( !this.existing) {
@@ -803,12 +844,19 @@ export default {
803
844
 
804
845
  if (hasPermissionToSeeProvCluster) {
805
846
  const mgmCluster = this.$store.getters['currentCluster'];
806
- const provCluster = mgmCluster?.provClusterId ? await this.$store.dispatch('management/find', {
807
- type: CAPI.RANCHER_CLUSTER,
808
- id: mgmCluster.provClusterId
809
- }) : {};
847
+ const provClusterId = mgmCluster?.provClusterId;
848
+ let provCluster;
849
+
850
+ try {
851
+ provCluster = provClusterId ? await this.$store.dispatch('management/find', {
852
+ type: CAPI.RANCHER_CLUSTER,
853
+ id: provClusterId
854
+ }) : {};
855
+ } catch (e) {
856
+ console.error(`Unable to fetch prov cluster '${ provClusterId }': `, e); // eslint-disable-line no-console
857
+ }
810
858
 
811
- if (provCluster.isRke2) { // isRke2 returns true for both RKE2 and K3s clusters.
859
+ if (provCluster?.isRke2) { // isRke2 returns true for both RKE2 and K3s clusters.
812
860
  const agentConfig = provCluster.spec?.rkeConfig?.machineSelectorConfig?.find((x) => !x.machineLabelSelector).config;
813
861
 
814
862
  // If a cluster scoped registry exists,
@@ -819,10 +867,11 @@ export default {
819
867
  return clusterRegistry;
820
868
  }
821
869
  }
822
- if (provCluster.isRke1) {
870
+
871
+ if (provCluster?.isRke1) {
823
872
  // For RKE1 clusters, the cluster scoped private registry is on the management
824
873
  // cluster, not the provisioning cluster.
825
- const rke1Registries = mgmCluster.spec?.rancherKubernetesEngineConfig?.privateRegistries;
874
+ const rke1Registries = mgmCluster?.spec?.rancherKubernetesEngineConfig?.privateRegistries;
826
875
 
827
876
  if (rke1Registries?.length > 0) {
828
877
  const defaultRegistry = rke1Registries.find((registry) => {
@@ -162,9 +162,8 @@ export default {
162
162
  },
163
163
 
164
164
  downloadData(btnCb) {
165
- const date = new Date().toLocaleDateString();
166
- const time = new Date().toLocaleTimeString();
167
- const fileName = `rancher-diagnostic-data-${ date }-${ time.replaceAll(':', '_') }.json`;
165
+ // simplify filename due to e2e tests
166
+ const fileName = 'rancher-diagnostic-data.json';
168
167
  const data = {
169
168
  systemInformation: this.systemInformation,
170
169
  logs: this.latestLogs,
@@ -235,6 +234,7 @@ export default {
235
234
  'aws',
236
235
  'digitalocean',
237
236
  'linode',
237
+ 'targetRoute', // contains circular references, isn't useful (added later to store)
238
238
  ];
239
239
 
240
240
  const clearListsKeys = [
@@ -316,10 +316,12 @@ export default {
316
316
  <AsyncButton
317
317
  mode="timing"
318
318
  class="mr-20"
319
+ data-testid="diagnostics-generate-response-times"
319
320
  @click="gatherResponseTimes"
320
321
  />
321
322
  <AsyncButton
322
323
  mode="diagnostic"
324
+ data-testid="diagnostics-download-diagnostic-package"
323
325
  @click="promptDownload"
324
326
  />
325
327
  </div>
@@ -2,7 +2,6 @@
2
2
 
3
3
  SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
4
4
  BASE_DIR="$(pwd)"
5
- BASE_EXT=$(basename ${BASE_DIR})
6
5
 
7
6
  CYAN="\033[96m"
8
7
  YELLOW="\033[93m"
@@ -17,6 +16,7 @@ REGISTRY_ORG=""
17
16
  IMAGE_PREFIX="ui-extension-"
18
17
  FORCE="false"
19
18
  GITHUB_BUILD="true"
19
+ GITHUB_RELEASE_TAG=""
20
20
 
21
21
  GITHUB_SOURCE=$(git config --get remote.origin.url | sed -e 's/^git@.*:\([[:graph:]]*\).git/\1/')
22
22
  GITHUB_BRANCH="main"
@@ -24,18 +24,19 @@ GITHUB_BRANCH="main"
24
24
  usage() {
25
25
  echo "Usage: $0 [<options>] [plugins]"
26
26
  echo " options:"
27
- echo " -p Push container images on build"
27
+ echo " -s <repo> Specify destination GitHub repository (org/name) - defaults to the git origin"
28
+ echo " -b <branch> Specify destination GitHub branch"
29
+ echo " -t <tag> Specify the Github release tag to build when a release has been tagged and published"
28
30
  echo " -f Force building the chart even if it already exists"
31
+ echo " -c Build as a container image rather than publishing to Github"
32
+ echo " -p Push container images on build"
29
33
  echo " -r <name> Specify destination container registry for built images"
30
34
  echo " -o <name> Specify destination container registry organization for built images"
31
35
  echo " -i <prefix> Specify prefix for the built container image (default: 'ui-extension-')"
32
- echo " -c Build as a container image rather than publishing to Github"
33
- echo " -s <repo> Specify destination GitHub repository (org/name) - defaults to the git origin"
34
- echo " -b <branch> Specify destination GitHub branch"
35
36
  exit 1
36
37
  }
37
38
 
38
- while getopts "hvr:o:pi:fcb:s:" opt; do
39
+ while getopts "hvr:o:pi:fcb:t:s:" opt; do
39
40
  case $opt in
40
41
  h)
41
42
  usage
@@ -66,6 +67,9 @@ while getopts "hvr:o:pi:fcb:s:" opt; do
66
67
  GITHUB_BUILD="true"
67
68
  GITHUB_BRANCH="${OPTARG}"
68
69
  ;;
70
+ t)
71
+ GITHUB_RELEASE_TAG="${OPTARG}"
72
+ ;;
69
73
  *)
70
74
  usage
71
75
  ;;
@@ -90,6 +94,10 @@ if [ "${GITHUB_BUILD}" == "true" ]; then
90
94
 
91
95
  echo -e "${CYAN}GitHub Repository: ${GITHUB_SOURCE}${RESET}"
92
96
  echo -e "${CYAN}GitHub Branch : ${GITHUB_BRANCH}${RESET}"
97
+
98
+ if [[ -n "${GITHUB_RELEASE_TAG}" ]]; then
99
+ echo -e "${CYAN}GitHub Release : ${GITHUB_RELEASE_TAG}${RESET}"
100
+ fi
93
101
  else
94
102
  echo -e ${CYAN}"Image prefix: ${IMAGE_PREFIX}${RESET}"
95
103
  fi
@@ -116,7 +124,11 @@ if ! [[ -d ${BASE_DIR}/node_modules ]]; then
116
124
  exit 1
117
125
  fi
118
126
 
119
- COMMANDS=("node" "jq" "yq" "git" "docker" "helm" "yarn")
127
+ COMMANDS=("node" "jq" "yq" "git" "helm" "yarn")
128
+ if [ "${GITHUB_BUILD}" == "false" ]; then
129
+ COMMANDS+=("docker")
130
+ fi
131
+
120
132
  HAVE_COMMANDS="true"
121
133
  for CMD in "${COMMANDS[@]}"
122
134
  do
@@ -132,7 +144,15 @@ fi
132
144
 
133
145
  # --------------------------------------------------------------------------------
134
146
  # Only do conatiner args checks if not GitHub publish
147
+ # --------------------------------------------------------------------------------
135
148
  if [ "${GITHUB_BUILD}" == "false" ]; then
149
+ BASE_EXT=$(jq -r .name ${BASE_DIR}/package.json)
150
+ EXT_VERSION=$(jq -r .version ${BASE_DIR}/package.json)
151
+
152
+ if [ -z ${EXT_VERSION} ]; then
153
+ EXT_VERSION="0.0.0"
154
+ fi
155
+
136
156
  if [[ -z ${REGISTRY_ORG} ]]; then
137
157
  # Infer that the user has the same Docker registry org as their GitHub org
138
158
  GITHUB_REPO=$(git config --get remote.origin.url | sed -e 's/^git@.*:\([[:graph:]]*\).git/\1/')
@@ -146,6 +166,12 @@ if [ "${GITHUB_BUILD}" == "false" ]; then
146
166
  exit 1
147
167
  fi
148
168
 
169
+ if [ -n "${GITHUB_RELEASE_TAG}" ] && [[ ${GITHUB_RELEASE_TAG} != "${BASE_EXT}-${EXT_VERSION}" ]]; then
170
+ echo -e "${YELLOW}Github tagged release name does not match ${RESET}${BOLD}${BASE_EXT}-${EXT_VERSION}${RESET}"
171
+ echo -e "${YELLOW}Stopping build${RESET}"
172
+ exit 1
173
+ fi
174
+
149
175
  docker images > /dev/null
150
176
  if [ $? -ne 0 ]; then
151
177
  echo "docker is not running - this is required to build container images for the UI Plugins"
@@ -185,6 +211,13 @@ for d in pkg/*/ ; do
185
211
  PKG_NAME="${pkg}-${PKG_VERSION}"
186
212
  PKG_ASSET=${ASSETS}/${pkg}/${PKG_NAME}.tgz
187
213
 
214
+ # Skip the build for a package that does not match the tagged release name
215
+ if [[ -n "${GITHUB_RELEASE_TAG}" ]] && [ ${GITHUB_BUILD} == "true" ] && [[ ${GITHUB_RELEASE_TAG} != ${PKG_NAME} ]]; then
216
+ echo -e "${YELLOW}Github release tag ${RESET}${BOLD}${GITHUB_RELEASE_TAG}${RESET}${YELLOW} does not match asset name${RESET}"
217
+ echo -e "${YELLOW}Skipping ${RESET}${BOLD}${PKG_NAME}${RESET}"
218
+ continue
219
+ fi
220
+
188
221
  echo -e "${CYAN}${BOLD}Building plugin: ${pkg} (${PKG_VERSION}) ${RESET}"
189
222
 
190
223
  echo "Package version: ${PKG_VERSION}"
@@ -299,6 +332,15 @@ for d in pkg/*/ ; do
299
332
  fi
300
333
  done
301
334
 
335
+ if [ "${GITHUB_BUILD}" == "true" ] && [[ -n "${GITHUB_RELEASE_TAG}" ]] && [ "${BUILT}" == "false" ]; then
336
+ echo -e "${YELLOW}Github release tag ${RESET}${BOLD}${GITHUB_RELEASE_TAG}${RESET}${YELLOW} did not match any package name.${RESET}"
337
+ echo -e "${YELLOW}Stopping build${RESET}"
338
+
339
+ rm -rf ${CHART_TMP}
340
+ rm -rf ${TMP}
341
+ exit 1
342
+ fi
343
+
302
344
  if [ -f ${ROOT_INDEX} ] && [ "${GITHUB_BUILD}" == "false" ]; then
303
345
  UPDATE="--merge ${ROOT_INDEX}"
304
346
  helm repo index ${ASSETS} ${UPDATE}
@@ -309,12 +351,6 @@ if [ -f ${ASSETS}/index.yaml ] && ! [ -e "${ROOT_INDEX}" ]; then
309
351
  fi
310
352
 
311
353
  if [ "${GITHUB_BUILD}" == "false" ]; then
312
- EXT_VERSION=$(jq -r .version ${BASE_DIR}/package.json)
313
-
314
- if [ -z ${EXT_VERSION} ]; then
315
- EXT_VERSION="0.0.0"
316
- fi
317
-
318
354
  echo -e "${CYAN}Base extension: ${BASE_EXT}${RESET}"
319
355
  echo -e "${CYAN}Extension version: ${EXT_VERSION}${RESET}"
320
356
 
@@ -325,7 +361,11 @@ else
325
361
  fi
326
362
 
327
363
  if [ "${GITHUB_BUILD}" == "true" ] && [ -f ${ROOT_INDEX} ]; then
328
- cp ${ROOT_INDEX} tmp
364
+ if [[ -n "${GITHUB_RELEASE_TAG}" ]] && [ -f ${ASSETS}/index.yaml ]; then
365
+ cp ${ASSETS}/index.yaml tmp
366
+ else
367
+ cp ${ROOT_INDEX} tmp
368
+ fi
329
369
  fi
330
370
 
331
371
  if [ "${GITHUB_BUILD}" == "true" ] && [ "${BUILT}" == "true" ]; then
package/store/auth.js CHANGED
@@ -334,6 +334,8 @@ export const actions = {
334
334
  } catch (err) {
335
335
  if (err._status === 401) {
336
336
  return Promise.reject(LOGIN_ERRORS.CLIENT_UNAUTHORIZED);
337
+ } else if (err.message) {
338
+ return Promise.reject(err.message);
337
339
  } else if ( err._status >= 400 && err._status <= 499 ) {
338
340
  return Promise.reject(LOGIN_ERRORS.CLIENT);
339
341
  }
@@ -3727,6 +3727,7 @@ export function matching(ary: any, selector: any, labelKey: any): any;
3727
3727
  // @shell/utils/socket
3728
3728
 
3729
3729
  declare module '@shell/utils/socket' {
3730
+ export const addEventListener: any;
3730
3731
  export const STATE_CONNECTING: "connecting";
3731
3732
  export const STATE_CONNECTED: "connected";
3732
3733
  export const EVENT_CONNECTING: "connecting";
package/utils/socket.js CHANGED
@@ -11,6 +11,7 @@ const SECURE = 'wss://';
11
11
 
12
12
  const STATE_DISCONNECTED = 'disconnected';
13
13
 
14
+ export const addEventListener = EventTarget.addEventListener;
14
15
  export const STATE_CONNECTING = 'connecting';
15
16
  export const STATE_CONNECTED = 'connected';
16
17
  const STATE_CLOSING = 'closing';