@rancher/shell 0.3.24 → 0.3.25

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 (111) hide show
  1. package/assets/styles/themes/_light.scss +1 -1
  2. package/assets/translations/en-us.yaml +29 -7
  3. package/assets/translations/zh-hans.yaml +1 -1
  4. package/components/ClusterIconMenu.vue +143 -0
  5. package/components/CruResource.vue +7 -1
  6. package/components/ExplorerProjectsNamespaces.vue +11 -1
  7. package/components/FixedBanner.vue +17 -1
  8. package/components/Markdown.vue +1 -1
  9. package/components/Questions/__tests__/Yaml.test.ts +3 -2
  10. package/components/SortableTable/index.vue +3 -2
  11. package/components/auth/RoleDetailEdit.vue +15 -2
  12. package/components/auth/login/saml.vue +12 -1
  13. package/components/form/LabeledSelect.vue +12 -5
  14. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  15. package/components/form/Members/MembershipEditor.vue +6 -1
  16. package/components/form/__tests__/KeyValue.test.ts +6 -3
  17. package/components/form/__tests__/LabeledSelect.test.ts +18 -0
  18. package/components/formatter/PodsUsage.vue +11 -36
  19. package/components/formatter/PrincipalGroupBindings.vue +8 -5
  20. package/components/formatter/__tests__/PodsUsage.test.ts +36 -19
  21. package/components/nav/Group.vue +25 -27
  22. package/components/nav/Header.vue +12 -5
  23. package/components/nav/Pinned.vue +47 -0
  24. package/components/nav/TopLevelMenu.vue +233 -60
  25. package/components/nav/Type.vue +57 -3
  26. package/config/home-links.js +1 -1
  27. package/config/product/istio.js +15 -5
  28. package/config/router.js +3 -9
  29. package/config/table-headers.js +5 -6
  30. package/config/uiplugins.js +1 -0
  31. package/core/plugin-helpers.js +3 -0
  32. package/core/types.ts +6 -1
  33. package/creators/app/files/.vscode/settings.json +0 -1
  34. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +118 -0
  35. package/detail/autoscaling.horizontalpodautoscaler/index.vue +4 -4
  36. package/detail/provisioning.cattle.io.cluster.vue +7 -5
  37. package/edit/__tests__/management.cattle.io.clusterroletemplatebinding.test.ts +58 -0
  38. package/edit/__tests__/namespace.test.ts +5 -3
  39. package/edit/management.cattle.io.clusterroletemplatebinding.vue +3 -11
  40. package/edit/namespace.vue +8 -4
  41. package/edit/provisioning.cattle.io.cluster/Basics.vue +662 -0
  42. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +6 -0
  43. package/edit/provisioning.cattle.io.cluster/DrainOptions.vue +13 -8
  44. package/edit/provisioning.cattle.io.cluster/MachinePool.vue +11 -2
  45. package/edit/provisioning.cattle.io.cluster/MemberRoles.vue +40 -0
  46. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +237 -0
  47. package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +71 -23
  48. package/edit/provisioning.cattle.io.cluster/__tests__/DrainOptions.test.ts +52 -0
  49. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -142
  50. package/edit/provisioning.cattle.io.cluster/rke2.vue +194 -598
  51. package/edit/workload/storage/__tests__/Storage.test.ts +2 -2
  52. package/edit/workload/storage/persistentVolumeClaim/__tests__/persistentvolumeclaim.test.ts +36 -0
  53. package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +15 -7
  54. package/initialize/index.js +5 -5
  55. package/layouts/default.vue +6 -6
  56. package/layouts/home.vue +6 -2
  57. package/layouts/plain.vue +9 -2
  58. package/list/fleet.cattle.io.cluster.vue +2 -2
  59. package/list/management.cattle.io.feature.vue +1 -1
  60. package/machine-config/vmwarevsphere.vue +48 -7
  61. package/mixins/brand.js +0 -8
  62. package/mixins/child-hook.js +2 -2
  63. package/mixins/create-edit-view/impl.js +3 -3
  64. package/models/__tests__/management.cattle.io.node.ts +96 -0
  65. package/models/__tests__/node.ts +74 -0
  66. package/models/cluster/node.js +6 -5
  67. package/models/cluster.x-k8s.io.machinedeployment.js +2 -2
  68. package/models/management.cattle.io.cluster.js +22 -1
  69. package/models/management.cattle.io.clusterroletemplatebinding.js +3 -3
  70. package/models/management.cattle.io.globalrole.js +17 -2
  71. package/models/management.cattle.io.node.js +6 -4
  72. package/models/management.cattle.io.projectroletemplatebinding.js +3 -3
  73. package/models/management.cattle.io.roletemplate.js +17 -2
  74. package/package.json +2 -6
  75. package/pages/about.vue +2 -0
  76. package/pages/auth/setup.vue +5 -4
  77. package/pages/c/_cluster/monitoring/index.vue +8 -3
  78. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +9 -66
  79. package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +182 -0
  80. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +15 -32
  81. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +8 -46
  82. package/pages/c/_cluster/uiplugins/index.vue +64 -64
  83. package/pages/diagnostic.vue +0 -39
  84. package/pages/home.vue +1 -1
  85. package/plugins/dashboard-store/normalize.js +4 -4
  86. package/plugins/int-number.js +5 -2
  87. package/plugins/positive-int-number.js +19 -0
  88. package/plugins/steve/__tests__/getters.spec.ts +15 -0
  89. package/plugins/steve/getters.js +22 -10
  90. package/rancher-components/Form/LabeledInput/LabeledInput.vue +0 -8
  91. package/rancher-components/Form/Radio/RadioButton.test.ts +3 -7
  92. package/store/index.js +4 -0
  93. package/store/prefs.js +1 -0
  94. package/types/shell/index.d.ts +13 -4
  95. package/utils/__tests__/cluster.test.ts +55 -0
  96. package/utils/__tests__/object.test.ts +21 -2
  97. package/utils/cluster.js +47 -1
  98. package/utils/object.js +12 -5
  99. package/utils/validators/formRules/__tests__/index.test.ts +13 -1
  100. package/utils/validators/formRules/index.ts +4 -0
  101. package/utils/validators/role-template.js +9 -1
  102. package/utils/version.js +1 -1
  103. package/yarn-error.log +16 -16
  104. package/components/ClusterProviderIconMenu.vue +0 -161
  105. package/content/docs/en-us/getting-started.md +0 -224
  106. package/content/docs/en-us/whats-new.md +0 -29
  107. package/content/docs/zh-hans/getting-started.md +0 -224
  108. package/content/docs/zh-hans/whats-new.md +0 -28
  109. package/pages/docs/_doc.vue +0 -345
  110. package/pages/docs/toc.js +0 -27
  111. package/plugins/console.js +0 -34
@@ -33,7 +33,7 @@ describe('component: Storage', () => {
33
33
  t: (text: string) => text, // Mock i18n global function used as alternative to the getter
34
34
  $store: {
35
35
  getters: {
36
- 'i18n/t': jest.fn(),
36
+ 'i18n/t': jest.fn().mockImplementation((key: string) => key),
37
37
  'i18n/exists': jest.fn()
38
38
  }
39
39
  }
@@ -63,7 +63,7 @@ describe('component: Storage', () => {
63
63
  t: (text: string) => text, // Mock i18n global function used as alternative to the getter
64
64
  $store: {
65
65
  getters: {
66
- 'i18n/t': jest.fn(),
66
+ 'i18n/t': jest.fn().mockImplementation((key: string) => key),
67
67
  'i18n/exists': jest.fn()
68
68
  }
69
69
  }
@@ -0,0 +1,36 @@
1
+ import { createLocalVue, mount } from '@vue/test-utils';
2
+ import PVC from '@shell/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue';
3
+
4
+ describe('component: PVC', () => {
5
+ // TODO: Enable test after allowing to test async data with either #9711 or #9322
6
+ // eslint-disable-next-line jest/no-disabled-tests
7
+ it.skip('should initialize storage class on create mode', async() => {
8
+ const localVue = createLocalVue();
9
+ const name = 'test';
10
+ const wrapper = mount(PVC, {
11
+ localVue,
12
+ propsData: {
13
+ savePvcHookName: '',
14
+ value: { spec: { resources: { requests: {} } } }
15
+ },
16
+ mocks: {
17
+ $store: {
18
+ getters: {
19
+ 'cluster/findAll': [{
20
+ metadata: {
21
+ name,
22
+ annotations: { 'storageclass.beta.kubernetes.io/is-default-class': true }
23
+ }
24
+ }],
25
+ 'i18n/t': jest.fn()
26
+ }
27
+ }
28
+ },
29
+ stubs: { LabeledSelect: { template: '<input />' } }
30
+ });
31
+
32
+ const inputElement = wrapper.find('[data-testid="storage-class-name"]').element as HTMLInputElement;
33
+
34
+ expect(inputElement.value).toBe(name);
35
+ });
36
+ });
@@ -48,6 +48,7 @@ export default {
48
48
 
49
49
  this.storageClasses = hash.storageClasses;
50
50
  this.persistentVolumes = hash.persistentVolumes;
51
+ this.$set(this.spec, 'storageClassName', (this.spec.storageClassName || this.defaultStorageClassName));
51
52
  },
52
53
 
53
54
  data() {
@@ -62,7 +63,7 @@ export default {
62
63
  return {
63
64
  storageClasses: [],
64
65
  persistentVolumes: [],
65
- createPV: true,
66
+ isCreatePV: true,
66
67
  spec,
67
68
  uniqueId: new Date().getTime() // Allows form state to be individually deleted
68
69
  };
@@ -73,6 +74,13 @@ export default {
73
74
  return this.storageClasses.map((sc) => sc.metadata.name);
74
75
  },
75
76
 
77
+ /**
78
+ * Required to initialize with default SC on creation
79
+ */
80
+ defaultStorageClassName() {
81
+ return this.storageClasses.find((sc) => sc.metadata?.annotations?.['storageclass.beta.kubernetes.io/is-default-class'] || sc.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'])?.metadata.name;
82
+ },
83
+
76
84
  availablePVs() {
77
85
  return this.persistentVolumes.reduce((total, each) => {
78
86
  if (each?.status?.phase === 'Available') {
@@ -91,12 +99,11 @@ export default {
91
99
  },
92
100
 
93
101
  watch: {
94
- createPV(neu, old) {
102
+ isCreatePV(neu) {
95
103
  if (neu) {
96
104
  delete this.spec.volumeName;
97
105
  this.spec.resources.requests.storage = null;
98
106
  } else {
99
- this.spec.storageClassName = '';
100
107
  this.spec.resources.requests.storage = null;
101
108
  }
102
109
  },
@@ -161,8 +168,8 @@ export default {
161
168
  <div class="row mb-10">
162
169
  <div class="col span-6">
163
170
  <RadioGroup
164
- v-model="createPV"
165
- name="createPV"
171
+ v-model="isCreatePV"
172
+ name="isCreatePV"
166
173
  :options="[true, false]"
167
174
  :labels="[t('persistentVolumeClaim.source.options.new'), t('persistentVolumeClaim.source.options.existing')]"
168
175
  :mode="mode"
@@ -170,8 +177,9 @@ export default {
170
177
  </div>
171
178
  <div class="col span-6">
172
179
  <LabeledSelect
173
- v-if="createPV"
180
+ v-if="isCreatePV"
174
181
  v-model="spec.storageClassName"
182
+ data-testid="storage-class-name"
175
183
  :mode="mode"
176
184
  :required="true"
177
185
  :label="t('persistentVolumeClaim.storageClass')"
@@ -221,7 +229,7 @@ export default {
221
229
  </div>
222
230
  </div>
223
231
  <div
224
- v-if="createPV"
232
+ v-if="isCreatePV"
225
233
  class="col span-6"
226
234
  >
227
235
  <UnitInput
@@ -36,8 +36,8 @@ import '../plugins/global-formatters';
36
36
  import '../plugins/trim-whitespace';
37
37
  import '../plugins/extend-router';
38
38
 
39
- import consolePlugin from '../plugins/console';
40
39
  import intNumber from '../plugins/int-number';
40
+ import positiveIntNumber from '../plugins/positive-int-number.js';
41
41
  import nuxtClientInit from '../plugins/nuxt-client-init';
42
42
  import replaceAll from '../plugins/replaceall';
43
43
  import backButton from '../plugins/back-button';
@@ -274,14 +274,14 @@ async function createApp(ssrContext, config = {}) {
274
274
  await axiosShell(app.context, inject);
275
275
  }
276
276
 
277
- if (process.client && typeof consolePlugin === 'function') {
278
- await consolePlugin(app.context, inject);
279
- }
280
-
281
277
  if (process.client && typeof intNumber === 'function') {
282
278
  await intNumber(app.context, inject);
283
279
  }
284
280
 
281
+ if (process.client && typeof positiveIntNumber === 'function') {
282
+ await positiveIntNumber(app.context, inject);
283
+ }
284
+
285
285
  if (process.client && typeof nuxtClientInit === 'function') {
286
286
  await nuxtClientInit(app.context, inject);
287
287
  }
@@ -70,7 +70,7 @@ export default {
70
70
 
71
71
  computed: {
72
72
  ...mapState(['managementReady', 'clusterReady']),
73
- ...mapGetters(['clusterId', 'currentProduct', 'isRancherInHarvester']),
73
+ ...mapGetters(['clusterId', 'currentProduct', 'isRancherInHarvester', 'showTopLevelMenu']),
74
74
 
75
75
  afterLoginRoute: mapPref(AFTER_LOGIN_ROUTE),
76
76
 
@@ -237,7 +237,7 @@ export default {
237
237
  <div
238
238
  v-if="managementReady"
239
239
  class="dashboard-content"
240
- :class="{[pinClass]: true}"
240
+ :class="{[pinClass]: true, 'dashboard-padding-left': showTopLevelMenu}"
241
241
  >
242
242
  <Header />
243
243
  <SideNav
@@ -318,12 +318,12 @@ export default {
318
318
  overflow-y: auto;
319
319
  min-height: 0px;
320
320
 
321
- &:has(.side-menu) {
321
+ &.dashboard-padding-left {
322
322
  padding-left: $app-bar-collapsed-width;
323
323
 
324
- .overlay-content-mode {
325
- left: calc(var(--nav-width) + $app-bar-collapsed-width);
326
- }
324
+ .overlay-content-mode {
325
+ left: calc(var(--nav-width) + $app-bar-collapsed-width);
326
+ }
327
327
  }
328
328
 
329
329
  &.pin-right {
package/layouts/home.vue CHANGED
@@ -8,7 +8,7 @@ import AwsComplianceBanner from '@shell/components/AwsComplianceBanner';
8
8
  import AzureWarning from '@shell/components/auth/AzureWarning';
9
9
  import BrowserTabVisibility from '@shell/mixins/browser-tab-visibility';
10
10
  import Inactivity from '@shell/components/Inactivity';
11
- import { mapState } from 'vuex';
11
+ import { mapState, mapGetters } from 'vuex';
12
12
 
13
13
  export default {
14
14
 
@@ -36,6 +36,7 @@ export default {
36
36
  computed: {
37
37
  themeShortcut: mapPref(THEME_SHORTCUT),
38
38
  ...mapState(['managementReady']),
39
+ ...mapGetters(['showTopLevelMenu']),
39
40
  },
40
41
 
41
42
  methods: {
@@ -57,7 +58,10 @@ export default {
57
58
  <AwsComplianceBanner />
58
59
  <AzureWarning />
59
60
 
60
- <div class="dashboard-content">
61
+ <div
62
+ class="dashboard-content"
63
+ :class="{'dashboard-padding-left': showTopLevelMenu}"
64
+ >
61
65
  <Header
62
66
  v-if="managementReady"
63
67
  :simple="true"
package/layouts/plain.vue CHANGED
@@ -12,6 +12,7 @@ import AwsComplianceBanner from '@shell/components/AwsComplianceBanner';
12
12
  import AzureWarning from '@shell/components/auth/AzureWarning';
13
13
  import BrowserTabVisibility from '@shell/mixins/browser-tab-visibility';
14
14
  import Inactivity from '@shell/components/Inactivity';
15
+ import { mapGetters } from 'vuex';
15
16
 
16
17
  export default {
17
18
 
@@ -40,7 +41,10 @@ export default {
40
41
  };
41
42
  },
42
43
 
43
- computed: { themeShortcut: mapPref(THEME_SHORTCUT) },
44
+ computed: {
45
+ themeShortcut: mapPref(THEME_SHORTCUT),
46
+ ...mapGetters(['showTopLevelMenu']),
47
+ },
44
48
 
45
49
  methods: {
46
50
  toggleTheme() {
@@ -59,7 +63,10 @@ export default {
59
63
  <AwsComplianceBanner />
60
64
  <AzureWarning />
61
65
 
62
- <div class="dashboard-content">
66
+ <div
67
+ class="dashboard-content"
68
+ :class="{'dashboard-padding-left': showTopLevelMenu}"
69
+ >
63
70
  <Header :simple="true" />
64
71
  <main class="main-layout">
65
72
  <IndentedPanel class="pt-20">
@@ -1,7 +1,7 @@
1
1
  <script>
2
2
  import FleetClusters from '@shell/components/fleet/FleetClusters';
3
3
  import { FLEET, MANAGEMENT } from '@shell/config/types';
4
- import { isHarvesterCluster } from '@shell/utils/cluster';
4
+ import { filterOnlyKubernetesClusters } from '@shell/utils/cluster';
5
5
  import { Banner } from '@components/Banner';
6
6
  import ResourceFetch from '@shell/mixins/resource-fetch';
7
7
 
@@ -56,7 +56,7 @@ export default {
56
56
  },
57
57
 
58
58
  filteredRows() {
59
- return this.fleetClusters.filter((c) => !isHarvesterCluster(c));
59
+ return filterOnlyKubernetesClusters(this.fleetClusters, this.$store);
60
60
  },
61
61
 
62
62
  fleetClusters() {
@@ -82,7 +82,7 @@ export default {
82
82
  },
83
83
 
84
84
  enableRowActions() {
85
- const schema = this.$store.getters[`management/schemaFor`](MANAGEMENT.SETTING);
85
+ const schema = this.$store.getters[`management/schemaFor`](MANAGEMENT.FEATURE);
86
86
 
87
87
  return schema?.resourceMethods?.includes('PUT');
88
88
  },
@@ -14,7 +14,7 @@ import ArrayListSelect from '@shell/components/form/ArrayListSelect';
14
14
  import YamlEditor from '@shell/components/YamlEditor';
15
15
  import { get, set } from '@shell/utils/object';
16
16
  import { integerString, keyValueStrings } from '@shell/utils/computed';
17
- import { _CREATE } from '@shell/config/query-params';
17
+ import { _CREATE, _EDIT, _VIEW } from '@shell/config/query-params';
18
18
 
19
19
  const SENTINEL = '__SENTINEL__';
20
20
  const VAPP_MODE = {
@@ -136,6 +136,11 @@ function createOptionHelpers(name) {
136
136
  };
137
137
  }
138
138
 
139
+ const errorActions = Object.freeze({
140
+ CREATE: 'create',
141
+ DELETE: 'delete',
142
+ });
143
+
139
144
  export default {
140
145
  components: {
141
146
  ArrayListSelect, Card, KeyValue, Loading, LabeledInput, LabeledSelect, Banner, UnitInput, RadioGroup, YamlEditor
@@ -144,6 +149,10 @@ export default {
144
149
  mixins: [CreateEditView],
145
150
 
146
151
  props: {
152
+ poolId: {
153
+ type: String,
154
+ default: '',
155
+ },
147
156
  credentialId: {
148
157
  type: String,
149
158
  required: true,
@@ -246,6 +255,7 @@ export default {
246
255
  vAppOptions,
247
256
  vappMode: getInitialVappMode(this.value),
248
257
  osOptions: OS_OPTIONS,
258
+ validationErrors: {},
249
259
  };
250
260
  },
251
261
 
@@ -347,6 +357,9 @@ export default {
347
357
  }
348
358
 
349
359
  this.updateVappOptions(INITIAL_VAPP_OPTIONS);
360
+ },
361
+ validationErrors(value) {
362
+ this.$emit('error', value);
350
363
  }
351
364
  },
352
365
 
@@ -384,9 +397,17 @@ export default {
384
397
  const valueInContent = content.find((c) => c.value === this.value.datacenter );
385
398
 
386
399
  if (!valueInContent) {
387
- set(this.value, 'datacenter', options[0]);
388
- set(this.value, 'cloneFrom', undefined);
389
- set(this.value, 'useDataStoreCluster', false);
400
+ if (this.mode === _CREATE) {
401
+ set(this.value, 'datacenter', options[0]);
402
+ set(this.value, 'cloneFrom', undefined);
403
+ set(this.value, 'useDataStoreCluster', false);
404
+ }
405
+
406
+ if ([_EDIT, _VIEW].includes(this.mode)) {
407
+ this.manageErrors(errorActions.CREATE, 'datacenter');
408
+ }
409
+ } else {
410
+ this.manageErrors(errorActions.DELETE, 'datacenter');
390
411
  }
391
412
 
392
413
  set(this, 'dataCentersResults', content);
@@ -582,11 +603,19 @@ export default {
582
603
  };
583
604
 
584
605
  if (!isValueInContent()) {
585
- const value = isArray ? [] : content[0]?.value;
606
+ if (this.mode === _CREATE) {
607
+ const value = isArray ? [] : content[0]?.value;
608
+
609
+ if (value !== SENTINEL) {
610
+ set(this.value, key, value);
611
+ }
612
+ }
586
613
 
587
- if (value !== SENTINEL) {
588
- set(this.value, key, value);
614
+ if ([_EDIT, _VIEW].includes(this.mode)) {
615
+ this.manageErrors(errorActions.CREATE, key);
589
616
  }
617
+ } else {
618
+ this.manageErrors(errorActions.DELETE, key);
590
619
  }
591
620
  },
592
621
 
@@ -654,6 +683,18 @@ export default {
654
683
  set(this.value, 'vappProperty', opts.vappProperty);
655
684
  this.initKeyValueParams('value.vappProperty', 'initVappArray');
656
685
  },
686
+
687
+ manageErrors(action = errorActions.CREATE, key) {
688
+ if (action === errorActions.CREATE) {
689
+ const keys = [key, ...(this.validationErrors[this.poolId] || [])];
690
+
691
+ this.validationErrors = Object.assign({}, this.validationErrors, { [this.poolId]: keys });
692
+ }
693
+
694
+ if (action === errorActions.DELETE && this.validationErrors[this.poolId]) {
695
+ this.validationErrors = Object.assign({}, this.validationErrors, { [this.poolId]: this.validationErrors[this.poolId].filter((x) => x === key) }) ;
696
+ }
697
+ },
657
698
  }
658
699
  };
659
700
  </script>
package/mixins/brand.js CHANGED
@@ -141,14 +141,6 @@ export default {
141
141
  }).then((setting) => setting.save());
142
142
  }
143
143
  }
144
- } else if (!neu) {
145
- const brandSetting = findBy(this.globalSettings, 'id', SETTING.BRAND);
146
-
147
- if (brandSetting && brandSetting.value !== '') {
148
- // 2) There should not be a brand... but there is a brand setting
149
- brandSetting.value = '';
150
- brandSetting.save();
151
- }
152
144
  }
153
145
  }
154
146
  },
@@ -25,14 +25,14 @@ export default {
25
25
  },
26
26
 
27
27
  async applyHooks(key, ...args) {
28
- if ( !key ) {
28
+ if (!key) {
29
29
  throw new Error('Must specify key');
30
30
  }
31
31
 
32
32
  const hooks = sortBy(this[key] || [], ['priority', 'name']);
33
33
  const out = {};
34
34
 
35
- for ( const x of hooks ) {
35
+ for (const x of hooks) {
36
36
  console.debug('Applying hook', x.name); // eslint-disable-line no-console
37
37
  out[x.name] = await x.fn.apply(x.fnContext || this, args);
38
38
  }
@@ -114,8 +114,8 @@ export default {
114
114
  // Detect and resolve conflicts from a 409 response.
115
115
  // If they are resolved, return a false-y value
116
116
  // Else they can't be resolved, return an array of errors to show to the user.
117
- conflict() {
118
- return handleConflict(this.initialValue.toJSON(), this.value, this.liveValue, this.$store.getters, this.$store);
117
+ async conflict() {
118
+ return await handleConflict(this.initialValue.toJSON(), this.value, this.liveValue, this.$store.getters, this.$store, this.storeOverride || this.$store.getters['currentStore'](this.value.type));
119
119
  },
120
120
 
121
121
  async save(buttonDone, url, depth = 0) {
@@ -159,7 +159,7 @@ export default {
159
159
  } catch (err) {
160
160
  // Conflict, the resource being edited has changed since starting editing
161
161
  if ( err.status === 409 && depth === 0 && this.isEdit) {
162
- const errors = this.conflict();
162
+ const errors = await this.conflict();
163
163
 
164
164
  if ( errors === false ) {
165
165
  // It was automatically figured out, save again
@@ -0,0 +1,96 @@
1
+ import MgmtNode from '@shell/models/management.cattle.io.node';
2
+
3
+ describe('class MgmtNode', () => {
4
+ const foo = 'foo';
5
+ const bar = 'bar';
6
+ const t = jest.fn(() => bar);
7
+ const ctx = { rootGetters: { 'i18n/t': t } };
8
+
9
+ const resetMocks = () => {
10
+ // Clear all mock function calls:
11
+ jest.clearAllMocks();
12
+ };
13
+
14
+ it('should not return addresses if they are not present in the resource status, the internalNodeStatus, or the rkeNode key in status', () => {
15
+ const mgmtNode = new MgmtNode({ status: {} });
16
+
17
+ expect(mgmtNode.addresses).toStrictEqual([]);
18
+ resetMocks();
19
+ });
20
+
21
+ describe('should return addresses', () => {
22
+ const addresses = [foo];
23
+
24
+ it('if they are present directly on the resource status', () => {
25
+ const mgmtNode = new MgmtNode({ status: { addresses } });
26
+
27
+ expect(mgmtNode.addresses).toStrictEqual(addresses);
28
+ });
29
+ it('if they are not present directly on the resource status but are on "status.internalNodeStatus"', () => {
30
+ const mgmtNode = new MgmtNode({ status: { internalNodeStatus: { addresses } } });
31
+
32
+ expect(mgmtNode.addresses).toStrictEqual(addresses);
33
+ });
34
+ });
35
+
36
+ describe('should return an internalIp', () => {
37
+ const addresses = [{ type: 'InternalIP', address: foo }];
38
+ const internalAddress = foo;
39
+
40
+ it('if addresses includes an object with an appropriate type and address', () => {
41
+ const mgmtNode = new MgmtNode({ status: { addresses } });
42
+
43
+ expect(mgmtNode.internalIp).toStrictEqual(foo);
44
+ });
45
+ it('if internalNodeStatus.addresses includes an object with an appropriate type and address', () => {
46
+ const mgmtNode = new MgmtNode({ status: { internalNodeStatus: { addresses } } });
47
+
48
+ expect(mgmtNode.internalIp).toStrictEqual(foo);
49
+ });
50
+ it('if addresses and internalNodeStatus.addresses do not provide an internal ip and the status includes an rkeNode key with an appropriate type and address', () => {
51
+ const mgmtNode = new MgmtNode({ status: { rkeNode: { internalAddress } } });
52
+
53
+ expect(mgmtNode.internalIp).toStrictEqual(internalAddress);
54
+ });
55
+ });
56
+
57
+ describe('should return an externalIp', () => {
58
+ const addresses = [{ type: 'ExternalIP', address: foo }];
59
+ const address = foo;
60
+
61
+ it('if addresses includes an object with an appropriate type and address', () => {
62
+ const mgmtNode = new MgmtNode({ status: { addresses } });
63
+
64
+ expect(mgmtNode.externalIp).toStrictEqual(foo);
65
+ });
66
+ it('if internalNodeStatus.addresses includes an object with an appropriate type and address', () => {
67
+ const mgmtNode = new MgmtNode({ status: { internalNodeStatus: { addresses } } });
68
+
69
+ expect(mgmtNode.externalIp).toStrictEqual(foo);
70
+ });
71
+ it('if addresses and internalNodeStatus.addresses do not provide an external ip and the status includes an rkeNode key with an appropriate type and address', () => {
72
+ const mgmtNode = new MgmtNode({ status: { rkeNode: { address } } });
73
+
74
+ expect(mgmtNode.externalIp).toStrictEqual(address);
75
+ });
76
+ });
77
+
78
+ describe('should return an appropriate message', () => {
79
+ it('if there is no internalIp to display', () => {
80
+ const mgmtNode = new MgmtNode({ status: {} }, ctx);
81
+
82
+ expect(mgmtNode.internalIp).toStrictEqual(bar);
83
+ expect(t).toHaveBeenCalledTimes(1);
84
+ expect(t).toHaveBeenCalledWith('generic.none');
85
+ resetMocks();
86
+ });
87
+ it('if there is no externalIp to display', () => {
88
+ const mgmtNode = new MgmtNode({ status: {} }, ctx);
89
+
90
+ expect(mgmtNode.externalIp).toStrictEqual(bar);
91
+ expect(t).toHaveBeenCalledTimes(1);
92
+ expect(t).toHaveBeenCalledWith('generic.none');
93
+ resetMocks();
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,74 @@
1
+ import Node from '@shell/models/management.cattle.io.node';
2
+
3
+ describe('class Node', () => {
4
+ const foo = 'foo';
5
+ const bar = 'bar';
6
+ const t = jest.fn(() => bar);
7
+ const ctx = { rootGetters: { 'i18n/t': t } };
8
+
9
+ const resetMocks = () => {
10
+ // Clear all mock function calls:
11
+ jest.clearAllMocks();
12
+ };
13
+
14
+ it('should not return addresses if they are not present in the resource status', () => {
15
+ const node = new Node({ status: {} });
16
+
17
+ expect(node.addresses).toStrictEqual([]);
18
+ resetMocks();
19
+ });
20
+
21
+ describe('should return addresses', () => {
22
+ const addresses = [foo];
23
+
24
+ it('if they are present directly on the resource status', () => {
25
+ const node = new Node({ status: { addresses } });
26
+
27
+ expect(node.addresses).toStrictEqual(addresses);
28
+ });
29
+ });
30
+
31
+ describe('should return an internalIp', () => {
32
+ const addresses = [{ type: 'InternalIP', address: foo }];
33
+
34
+ it('if addresses includes an object with an appropriate type and address', () => {
35
+ const node = new Node({ status: { addresses } });
36
+
37
+ expect(node.internalIp).toStrictEqual(foo);
38
+ });
39
+ });
40
+
41
+ describe('should return an externalIp', () => {
42
+ const addresses = [{ type: 'ExternalIP', address: foo }];
43
+
44
+ it('if addresses includes an object with an appropriate type and address', () => {
45
+ const node = new Node({ status: { addresses } });
46
+
47
+ expect(node.externalIp).toStrictEqual(foo);
48
+ });
49
+ it('if internalNodeStatus.addresses includes an object with an appropriate type and address', () => {
50
+ const node = new Node({ status: { internalNodeStatus: { addresses } } });
51
+
52
+ expect(node.externalIp).toStrictEqual(foo);
53
+ });
54
+ });
55
+
56
+ describe('should return an appropriate message', () => {
57
+ it('if there is no internalIp to display', () => {
58
+ const node = new Node({ status: {} }, ctx);
59
+
60
+ expect(node.internalIp).toStrictEqual(bar);
61
+ expect(t).toHaveBeenCalledTimes(1);
62
+ expect(t).toHaveBeenCalledWith('generic.none');
63
+ resetMocks();
64
+ });
65
+ it('if there is no externalIp to display', () => {
66
+ const node = new Node({ status: {} }, ctx);
67
+
68
+ expect(node.externalIp).toStrictEqual(bar);
69
+ expect(t).toHaveBeenCalledTimes(1);
70
+ expect(t).toHaveBeenCalledWith('generic.none');
71
+ resetMocks();
72
+ });
73
+ });
74
+ });
@@ -92,16 +92,17 @@ export default class ClusterNode extends SteveModel {
92
92
  return this.metadata.name;
93
93
  }
94
94
 
95
- get internalIp() {
96
- const addresses = this.status?.addresses || [];
95
+ get addresses() {
96
+ return this.status?.addresses || [];
97
+ }
97
98
 
98
- return findLast(addresses, (address) => address.type === 'InternalIP')?.address;
99
+ get internalIp() {
100
+ return findLast(this.addresses, (address) => address.type === 'InternalIP')?.address;
99
101
  }
100
102
 
101
103
  get externalIp() {
102
- const addresses = this.status?.addresses || [];
103
104
  const annotationAddress = this.metadata.annotations[RKE.EXTERNAL_IP];
104
- const statusAddress = findLast(addresses, (address) => address.type === 'ExternalIP')?.address;
105
+ const statusAddress = findLast(this.addresses, (address) => address.type === 'ExternalIP')?.address;
105
106
 
106
107
  return statusAddress || annotationAddress;
107
108
  }
@@ -127,11 +127,11 @@ export default class CapiMachineDeployment extends SteveModel {
127
127
  }
128
128
 
129
129
  this.scaleTimer = setTimeout(() => {
130
- this.cluster.save().catch((err) => {
130
+ this.cluster.save().catch(async(err) => {
131
131
  let errors = exceptionToErrorsArray(err);
132
132
 
133
133
  if ( err.status === 409 && depth < 2 ) {
134
- const conflicts = handleConflict(initialValue, value, liveModel, this.$rootGetters, this.$store);
134
+ const conflicts = await handleConflict(initialValue, value, liveModel, this.$rootGetters, { dispatch: this.$dispatch }, 'management');
135
135
 
136
136
  if ( conflicts === false ) {
137
137
  // It was automatically figured out, save again