@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.
- package/assets/styles/themes/_light.scss +1 -1
- package/assets/translations/en-us.yaml +29 -7
- package/assets/translations/zh-hans.yaml +1 -1
- package/components/ClusterIconMenu.vue +143 -0
- package/components/CruResource.vue +7 -1
- package/components/ExplorerProjectsNamespaces.vue +11 -1
- package/components/FixedBanner.vue +17 -1
- package/components/Markdown.vue +1 -1
- package/components/Questions/__tests__/Yaml.test.ts +3 -2
- package/components/SortableTable/index.vue +3 -2
- package/components/auth/RoleDetailEdit.vue +15 -2
- package/components/auth/login/saml.vue +12 -1
- package/components/form/LabeledSelect.vue +12 -5
- package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
- package/components/form/Members/MembershipEditor.vue +6 -1
- package/components/form/__tests__/KeyValue.test.ts +6 -3
- package/components/form/__tests__/LabeledSelect.test.ts +18 -0
- package/components/formatter/PodsUsage.vue +11 -36
- package/components/formatter/PrincipalGroupBindings.vue +8 -5
- package/components/formatter/__tests__/PodsUsage.test.ts +36 -19
- package/components/nav/Group.vue +25 -27
- package/components/nav/Header.vue +12 -5
- package/components/nav/Pinned.vue +47 -0
- package/components/nav/TopLevelMenu.vue +233 -60
- package/components/nav/Type.vue +57 -3
- package/config/home-links.js +1 -1
- package/config/product/istio.js +15 -5
- package/config/router.js +3 -9
- package/config/table-headers.js +5 -6
- package/config/uiplugins.js +1 -0
- package/core/plugin-helpers.js +3 -0
- package/core/types.ts +6 -1
- package/creators/app/files/.vscode/settings.json +0 -1
- package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +118 -0
- package/detail/autoscaling.horizontalpodautoscaler/index.vue +4 -4
- package/detail/provisioning.cattle.io.cluster.vue +7 -5
- package/edit/__tests__/management.cattle.io.clusterroletemplatebinding.test.ts +58 -0
- package/edit/__tests__/namespace.test.ts +5 -3
- package/edit/management.cattle.io.clusterroletemplatebinding.vue +3 -11
- package/edit/namespace.vue +8 -4
- package/edit/provisioning.cattle.io.cluster/Basics.vue +662 -0
- package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +6 -0
- package/edit/provisioning.cattle.io.cluster/DrainOptions.vue +13 -8
- package/edit/provisioning.cattle.io.cluster/MachinePool.vue +11 -2
- package/edit/provisioning.cattle.io.cluster/MemberRoles.vue +40 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +237 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +71 -23
- package/edit/provisioning.cattle.io.cluster/__tests__/DrainOptions.test.ts +52 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -142
- package/edit/provisioning.cattle.io.cluster/rke2.vue +194 -598
- package/edit/workload/storage/__tests__/Storage.test.ts +2 -2
- package/edit/workload/storage/persistentVolumeClaim/__tests__/persistentvolumeclaim.test.ts +36 -0
- package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +15 -7
- package/initialize/index.js +5 -5
- package/layouts/default.vue +6 -6
- package/layouts/home.vue +6 -2
- package/layouts/plain.vue +9 -2
- package/list/fleet.cattle.io.cluster.vue +2 -2
- package/list/management.cattle.io.feature.vue +1 -1
- package/machine-config/vmwarevsphere.vue +48 -7
- package/mixins/brand.js +0 -8
- package/mixins/child-hook.js +2 -2
- package/mixins/create-edit-view/impl.js +3 -3
- package/models/__tests__/management.cattle.io.node.ts +96 -0
- package/models/__tests__/node.ts +74 -0
- package/models/cluster/node.js +6 -5
- package/models/cluster.x-k8s.io.machinedeployment.js +2 -2
- package/models/management.cattle.io.cluster.js +22 -1
- package/models/management.cattle.io.clusterroletemplatebinding.js +3 -3
- package/models/management.cattle.io.globalrole.js +17 -2
- package/models/management.cattle.io.node.js +6 -4
- package/models/management.cattle.io.projectroletemplatebinding.js +3 -3
- package/models/management.cattle.io.roletemplate.js +17 -2
- package/package.json +2 -6
- package/pages/about.vue +2 -0
- package/pages/auth/setup.vue +5 -4
- package/pages/c/_cluster/monitoring/index.vue +8 -3
- package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +9 -66
- package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +182 -0
- package/pages/c/_cluster/uiplugins/CatalogList/index.vue +15 -32
- package/pages/c/_cluster/uiplugins/UninstallDialog.vue +8 -46
- package/pages/c/_cluster/uiplugins/index.vue +64 -64
- package/pages/diagnostic.vue +0 -39
- package/pages/home.vue +1 -1
- package/plugins/dashboard-store/normalize.js +4 -4
- package/plugins/int-number.js +5 -2
- package/plugins/positive-int-number.js +19 -0
- package/plugins/steve/__tests__/getters.spec.ts +15 -0
- package/plugins/steve/getters.js +22 -10
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +0 -8
- package/rancher-components/Form/Radio/RadioButton.test.ts +3 -7
- package/store/index.js +4 -0
- package/store/prefs.js +1 -0
- package/types/shell/index.d.ts +13 -4
- package/utils/__tests__/cluster.test.ts +55 -0
- package/utils/__tests__/object.test.ts +21 -2
- package/utils/cluster.js +47 -1
- package/utils/object.js +12 -5
- package/utils/validators/formRules/__tests__/index.test.ts +13 -1
- package/utils/validators/formRules/index.ts +4 -0
- package/utils/validators/role-template.js +9 -1
- package/utils/version.js +1 -1
- package/yarn-error.log +16 -16
- package/components/ClusterProviderIconMenu.vue +0 -161
- package/content/docs/en-us/getting-started.md +0 -224
- package/content/docs/en-us/whats-new.md +0 -29
- package/content/docs/zh-hans/getting-started.md +0 -224
- package/content/docs/zh-hans/whats-new.md +0 -28
- package/pages/docs/_doc.vue +0 -345
- package/pages/docs/toc.js +0 -27
- 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
|
-
|
|
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
|
-
|
|
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="
|
|
165
|
-
name="
|
|
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="
|
|
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="
|
|
232
|
+
v-if="isCreatePV"
|
|
225
233
|
class="col span-6"
|
|
226
234
|
>
|
|
227
235
|
<UnitInput
|
package/initialize/index.js
CHANGED
|
@@ -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
|
}
|
package/layouts/default.vue
CHANGED
|
@@ -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
|
-
|
|
321
|
+
&.dashboard-padding-left {
|
|
322
322
|
padding-left: $app-bar-collapsed-width;
|
|
323
323
|
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
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: {
|
|
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
|
|
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 {
|
|
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
|
|
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.
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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 (
|
|
588
|
-
|
|
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
|
},
|
package/mixins/child-hook.js
CHANGED
|
@@ -25,14 +25,14 @@ export default {
|
|
|
25
25
|
},
|
|
26
26
|
|
|
27
27
|
async applyHooks(key, ...args) {
|
|
28
|
-
if (
|
|
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 (
|
|
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
|
+
});
|
package/models/cluster/node.js
CHANGED
|
@@ -92,16 +92,17 @@ export default class ClusterNode extends SteveModel {
|
|
|
92
92
|
return this.metadata.name;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
get
|
|
96
|
-
|
|
95
|
+
get addresses() {
|
|
96
|
+
return this.status?.addresses || [];
|
|
97
|
+
}
|
|
97
98
|
|
|
98
|
-
|
|
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.$
|
|
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
|