mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-04 05:06:05 +08:00
Add pkg/harvester components + shell portings - 1
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
|
||||
export default {
|
||||
name: 'CloneVMModal',
|
||||
|
||||
components: {
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.name = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async create(buttonCb) {
|
||||
// shadow clone
|
||||
if (!this.cloneData) {
|
||||
this.resources[0].goToClone();
|
||||
buttonCb(false);
|
||||
this.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// deep clone
|
||||
try {
|
||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('harvester.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this.$set(this, 'errors', error);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.cloneVM.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Checkbox v-model="cloneData" class="mb-10" label-key="harvester.modal.cloneVM.type" />
|
||||
|
||||
<LabeledInput
|
||||
v-show="cloneData"
|
||||
v-model="name"
|
||||
class="mb-20"
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="create"
|
||||
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
|
||||
:disabled="cloneData && !name"
|
||||
@click="create"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { alternateLabel } from '@shell/utils/platform';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Card } from '@components/Card';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
import CopyToClipboardText from '@shell/components/CopyToClipboardText';
|
||||
|
||||
/**
|
||||
* @name ConfirmRelatedToRemoveDialog
|
||||
* @description Dialog component to confirm the related resources before removing the resource.
|
||||
*/
|
||||
export default {
|
||||
name: 'ConfirmRelatedToRemoveDialog',
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
Card,
|
||||
CopyToClipboardText
|
||||
},
|
||||
|
||||
props: {
|
||||
/**
|
||||
* @property resources to be deleted.
|
||||
* @type {Resource[]} Array of the resource model's instance
|
||||
*/
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { errors: [], confirmName: '' };
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState('action-menu', ['modalData']),
|
||||
|
||||
warningMessageKey() {
|
||||
return this.modalData.warningMessageKey;
|
||||
},
|
||||
|
||||
names() {
|
||||
return this.resources.map(obj => obj.nameDisplay).slice(0, 5);
|
||||
},
|
||||
|
||||
resourceNames() {
|
||||
return this.names.reduce((res, name, i) => {
|
||||
if (i >= 5) {
|
||||
return res;
|
||||
}
|
||||
res += `<b>${ escapeHtml(name) }</b>`;
|
||||
if (i === this.names.length - 1) {
|
||||
res += this.plusMore;
|
||||
} else {
|
||||
res += i === this.resources.length - 2 ? ' and ' : ', ';
|
||||
}
|
||||
|
||||
return res;
|
||||
}, '');
|
||||
},
|
||||
|
||||
nameToMatch() {
|
||||
return this.resources[0].nameDisplay;
|
||||
},
|
||||
|
||||
plusMore() {
|
||||
const remaining = this.resources.length - this.names.length;
|
||||
|
||||
return this.t('promptRemove.andOthers', { count: remaining });
|
||||
},
|
||||
|
||||
type() {
|
||||
const types = new Set(this.resources.reduce((array, each) => {
|
||||
array.push(each.type);
|
||||
|
||||
return array;
|
||||
}, []));
|
||||
|
||||
if (types.size > 1) {
|
||||
return this.t('generic.resource', { count: this.resources.length });
|
||||
}
|
||||
|
||||
const schema = this.resources[0]?.schema;
|
||||
|
||||
if ( !schema ) {
|
||||
return `resource${ this.resources.length === 1 ? '' : 's' }`;
|
||||
}
|
||||
|
||||
return this.$store.getters['type-map/labelFor'](schema, this.resources.length);
|
||||
},
|
||||
|
||||
deleteDisabled() {
|
||||
return this.confirmName !== this.nameToMatch;
|
||||
},
|
||||
|
||||
protip() {
|
||||
return this.t('promptRemove.protip', { alternateLabel });
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
escapeHtml,
|
||||
|
||||
close() {
|
||||
this.confirmName = '';
|
||||
this.errors = [];
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async remove(buttonDone) {
|
||||
try {
|
||||
for (const resource of this.resources) {
|
||||
await resource.remove();
|
||||
}
|
||||
buttonDone(true);
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this.errors = exceptionToErrorsArray(e);
|
||||
buttonDone(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="prompt-related" :show-highlight-border="false">
|
||||
<h4 slot="title" class="text-default-text">
|
||||
{{ t('promptRemove.title') }}
|
||||
</h4>
|
||||
<div slot="body" class="pl-10 pr-10">
|
||||
<span
|
||||
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
|
||||
></span>
|
||||
|
||||
<div class="mt-10 mb-10">
|
||||
<span
|
||||
v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)"
|
||||
></span>
|
||||
</div>
|
||||
<div class="mb-10">
|
||||
<CopyToClipboardText :text="nameToMatch" />
|
||||
</div>
|
||||
<input id="confirm" v-model="confirmName" type="text" />
|
||||
<div class="text-info mt-20">
|
||||
{{ protip }}
|
||||
</div>
|
||||
<Banner v-for="(error, i) in errors" :key="i" class="" color="error" :label="error" />
|
||||
</div>
|
||||
<template #actions>
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton mode="delete" class="btn bg-error ml-10" :disabled="deleteDisabled" @click="remove" />
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
|
||||
export default {
|
||||
name: 'CloneVMModal',
|
||||
|
||||
components: {
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.name = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async create(buttonCb) {
|
||||
// shadow clone
|
||||
if (!this.cloneData) {
|
||||
this.resources[0].goToClone();
|
||||
buttonCb(false);
|
||||
this.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// deep clone
|
||||
try {
|
||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('harvester.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this.$set(this, 'errors', error);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.cloneVM.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Checkbox v-model="cloneData" class="mb-10" label-key="harvester.modal.cloneVM.type" />
|
||||
|
||||
<LabeledInput
|
||||
v-show="cloneData"
|
||||
v-model="name"
|
||||
class="mb-20"
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="create"
|
||||
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
|
||||
:disabled="cloneData && !name"
|
||||
@click="create"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<script>
|
||||
import { HCI } from '../types';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Card } from '@components/Card';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEnablePassthrough',
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Card,
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
// isSingleProduct == this is a standalone Harvester cluster
|
||||
const isSingleProduct = this.$store.getters['isSingleProduct'];
|
||||
let userName = 'admin';
|
||||
|
||||
// if this is imported Harvester, there may be users other than 'admin
|
||||
if (!isSingleProduct) {
|
||||
const user = this.$store.getters['auth/v3User'];
|
||||
|
||||
userName = user?.username || user?.id;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.resources.length; i++) {
|
||||
const actionResource = this.resources[i];
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const pt = await this.$store.dispatch(`${ inStore }/create`, {
|
||||
type: HCI.PCI_CLAIM,
|
||||
metadata: {
|
||||
name: actionResource.metadata.name,
|
||||
ownerReferences: [{
|
||||
apiVersion: 'devices.harvesterhci.io/v1beta1',
|
||||
kind: 'PCIDevice',
|
||||
name: actionResource.metadata.name,
|
||||
uid: actionResource.metadata.uid,
|
||||
}]
|
||||
},
|
||||
spec: {
|
||||
address: actionResource.status.address,
|
||||
nodeName: actionResource.status.nodeName,
|
||||
userName
|
||||
}
|
||||
} );
|
||||
|
||||
try {
|
||||
await pt.save();
|
||||
buttonCb(true);
|
||||
this.close();
|
||||
} catch (err) {
|
||||
this.$store.dispatch('growl/fromError', {
|
||||
title: this.t('harvester.pci.claimError', { name: escapeHtml(actionResource.metadata.name) }),
|
||||
err,
|
||||
}, { root: true });
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<h4
|
||||
slot="title"
|
||||
v-clean-html="t('promptRemove.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
|
||||
<template #body>
|
||||
{{ t('harvester.pci.enablePassthroughWarning') }}
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton mode="enable" @click="save" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<script>
|
||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Card } from '@components/Card';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEnableSriovDevice',
|
||||
|
||||
components: {
|
||||
LabeledInput,
|
||||
AsyncButton,
|
||||
Card,
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const numVFs = this.resources[0].spec?.numVFs || 1;
|
||||
|
||||
return { numVFs, numVFsHistory: numVFs };
|
||||
},
|
||||
|
||||
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||
|
||||
watch: {
|
||||
numVFs(neu) {
|
||||
if (!parseFloat(neu) || parseFloat(neu) < 1) {
|
||||
this.numVFs = 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
const actionResource = this.resources[0];
|
||||
|
||||
try {
|
||||
this.resources[0].spec.numVFs = this.numVFs;
|
||||
await actionResource.save();
|
||||
buttonCb(true);
|
||||
this.close();
|
||||
} catch (err) {
|
||||
this.resources[0].spec.numVFs = this.numVFsHistory;
|
||||
this.$store.dispatch('growl/fromError', {
|
||||
title: this.t('generic.notification.title.error', { name: escapeHtml(actionResource.metadata.name) }),
|
||||
err,
|
||||
}, { root: true });
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template v-slot:title>
|
||||
<h4
|
||||
v-clean-html="t('promptRemove.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<LabeledInput
|
||||
v-model.number="numVFs"
|
||||
type="number"
|
||||
min="1"
|
||||
required
|
||||
:label="t('harvester.sriov.numVFs')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-slot:actions>
|
||||
<div class="buttons actions">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton mode="enable" @click="save" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Card } from '@components/Card';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEnableVGpuDevice',
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Card,
|
||||
LabeledSelect,
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
type: this.resources[0].spec?.vGPUTypeName,
|
||||
availableTypes: Object.keys(this.resources[0].status?.availableTypes || {}),
|
||||
};
|
||||
},
|
||||
|
||||
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
const actionResource = this.resources[0];
|
||||
|
||||
try {
|
||||
this.resources[0].spec.vGPUTypeName = this.type;
|
||||
this.resources[0].spec.enabled = true;
|
||||
await actionResource.save();
|
||||
buttonCb(true);
|
||||
this.close();
|
||||
} catch (err) {
|
||||
this.$store.dispatch('growl/fromError', {
|
||||
title: this.t('generic.notification.title.error', { name: escapeHtml(actionResource.metadata.name) }),
|
||||
err,
|
||||
}, { root: true });
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template v-slot:title>
|
||||
<h4
|
||||
v-clean-html="t('harvester.vgpu.enable.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="body">
|
||||
<div class="type-field">
|
||||
<LabeledSelect
|
||||
v-model="type"
|
||||
required
|
||||
:options="availableTypes"
|
||||
:searchable="true"
|
||||
:label="t('harvester.vgpu.enable.type')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:actions>
|
||||
<div class="buttons actions">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton mode="edit" :disabled="!type" @click="save" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-field {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { PVC } from '@shell/config/types';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
name: 'HotplugModal',
|
||||
|
||||
components: {
|
||||
AsyncButton, Card, LabeledInput, LabeledSelect, Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
this.allPVCs = await this.$store.dispatch('harvester/findAll', { type: PVC });
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
diskName: '',
|
||||
volumeName: '',
|
||||
errors: [],
|
||||
allPVCs: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
PVCs() {
|
||||
return this.allPVCs.filter(P => this.actionResource.metadata.namespace === P.metadata.namespace) || [];
|
||||
},
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
|
||||
volumeOption() {
|
||||
return sortBy(
|
||||
this.PVCs
|
||||
.filter( (pvc) => {
|
||||
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !pvc.attachVM;
|
||||
})
|
||||
.map((pvc) => {
|
||||
return {
|
||||
label: pvc.metadata.name,
|
||||
value: pvc.metadata.name
|
||||
};
|
||||
}),
|
||||
'label'
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.diskName = '';
|
||||
this.volumeName = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
if (this.actionResource) {
|
||||
try {
|
||||
const res = await this.actionResource.doAction('addVolume', { volumeSourceName: this.volumeName, diskName: this.diskName }, {}, false);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this.$set(this, 'errors', error);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card ref="modal" name="modal" :show-highlight-border="false">
|
||||
<h4 slot="title" v-clean-html="t('harvester.modal.hotplug.title')" class="text-default-text" />
|
||||
|
||||
<template #body>
|
||||
<LabeledInput
|
||||
v-model="diskName"
|
||||
:label="t('generic.name')"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
v-model="volumeName"
|
||||
:label="t('harvester.fields.volume')"
|
||||
:options="volumeOption"
|
||||
class="mt-20"
|
||||
required
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button type="button" class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
:disabled="!diskName || !volumeName"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterBackupModal',
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Card,
|
||||
LabeledInput,
|
||||
Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
backUpName: '',
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.backUpName = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
if (this.actionResource) {
|
||||
try {
|
||||
const res = await this.actionResource.doAction(
|
||||
'backup',
|
||||
{ name: this.backUpName },
|
||||
{},
|
||||
false
|
||||
);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch(
|
||||
'growl/success',
|
||||
{
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.backup.success', { backUpName: this.backUpName })
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
|
||||
this.close();
|
||||
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this.$set(this, 'errors', error);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<h4
|
||||
slot="title"
|
||||
v-clean-html="t('harvester.modal.backup.addBackup')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
|
||||
<template #body>
|
||||
<LabeledInput v-model="backUpName" :label="t('generic.name')" required />
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton mode="create" :disabled="!backUpName" @click="save" />
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,147 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterCloneTemplateModal',
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
Card,
|
||||
Checkbox,
|
||||
LabeledInput
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
templateName: '',
|
||||
description: '',
|
||||
withData: false,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.templateName = '';
|
||||
this.description = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
try {
|
||||
const res = await this.actionResource.doAction(
|
||||
'createTemplate',
|
||||
{
|
||||
name: this.templateName, description: this.description, withData: this.withData
|
||||
},
|
||||
{},
|
||||
false
|
||||
);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch(
|
||||
'growl/success',
|
||||
{
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t(
|
||||
'harvester.modal.createTemplate.message.success',
|
||||
{ templateName: this.templateName }
|
||||
)
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this.$set(this, 'errors', error);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.createTemplate.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Checkbox v-model="withData" class="mb-10" label="With Data" />
|
||||
|
||||
<LabeledInput
|
||||
v-model="templateName"
|
||||
class="mb-20"
|
||||
:label="t('harvester.modal.createTemplate.name')"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model="description"
|
||||
:label="t('harvester.modal.createTemplate.description')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="create"
|
||||
:disabled="!templateName"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEjectCDROMModal',
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Card,
|
||||
Checkbox,
|
||||
Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const allDisk = [];
|
||||
const disks = this.resources[0].spec.template.spec.domain.devices.disks;
|
||||
|
||||
if (Array.isArray(disks)) {
|
||||
disks.forEach((D) => {
|
||||
if (D.cdrom) {
|
||||
allDisk.push({
|
||||
name: D.name,
|
||||
value: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
allDisk,
|
||||
errors: [],
|
||||
diskNames: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
|
||||
isDeleteDisabled() {
|
||||
return this.diskNames.length === 0;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateNames(names) {
|
||||
this.diskNames = names;
|
||||
},
|
||||
|
||||
async remove(buttonDone) {
|
||||
try {
|
||||
await this.actionResource.doAction('ejectCdRom', { diskNames: this.diskNames });
|
||||
this.close();
|
||||
buttonDone(true);
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonDone(false);
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
this.backupName = '';
|
||||
this.errors = [];
|
||||
this.$emit('close');
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
allDisk: {
|
||||
handler(neu) {
|
||||
const diskNames = [];
|
||||
|
||||
neu.forEach((D) => {
|
||||
if (D.value) {
|
||||
diskNames.push(D.name);
|
||||
}
|
||||
});
|
||||
|
||||
this.$set(this, 'diskNames', diskNames);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.ejectCDROM.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<span class="text-info mb-10">
|
||||
{{ t('harvester.modal.ejectCDROM.operationTip') }}
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<Checkbox
|
||||
v-for="disk in allDisk"
|
||||
:key="disk.name"
|
||||
v-model="disk.value"
|
||||
:label="disk.name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner color="warning">
|
||||
<span>{{ t('harvester.modal.ejectCDROM.warnTip') }}</span>
|
||||
</Banner>
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="delete"
|
||||
:disabled="!diskNames.length"
|
||||
class="btn bg-error ml-10"
|
||||
@click="remove"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,195 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterExportImageDialog',
|
||||
|
||||
components: {
|
||||
AsyncButton, Banner, Card, LabeledInput, LabeledSelect
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = { storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }) };
|
||||
|
||||
await allHash(hash);
|
||||
|
||||
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find(s => s.isDefault);
|
||||
|
||||
this.$set(this, 'storageClassName', defaultStorage?.metadata?.name || 'longhorn');
|
||||
},
|
||||
|
||||
data() {
|
||||
const namespace = this.$store.getters['defaultNamespace'] || '';
|
||||
|
||||
return {
|
||||
name: '',
|
||||
namespace,
|
||||
storageClassName: '',
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
|
||||
namespaces() {
|
||||
const choices = this.$store.getters['harvester/all'](NAMESPACE).filter( N => !N.isSystem);
|
||||
|
||||
const out = sortBy(
|
||||
choices.map((obj) => {
|
||||
return {
|
||||
label: obj.nameDisplay,
|
||||
value: obj.id,
|
||||
};
|
||||
}),
|
||||
'label'
|
||||
);
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
disableSave() {
|
||||
return !(this.name && this.namespace && this.storageClassName);
|
||||
},
|
||||
|
||||
storageClassOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||
|
||||
const out = storages.filter(s => !s.parameters?.backingImage).map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return out;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.name = '';
|
||||
this.namespace = '';
|
||||
this.storageClassName = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
try {
|
||||
const res = await this.actionResource.doAction('export', {
|
||||
displayName: this.name,
|
||||
namespace: this.namespace,
|
||||
storageClassName: this.storageClassName,
|
||||
});
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.exportImage.message.success', { name: this.name })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this.$set(this, 'errors', error);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.exportImage.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<LabeledSelect
|
||||
v-model="namespace"
|
||||
:label="t('harvester.modal.exportImage.namespace')"
|
||||
:options="namespaces"
|
||||
class="mb-20"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model="name"
|
||||
:label="t('harvester.modal.exportImage.name')"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
v-model="storageClassName"
|
||||
:options="storageClassOptions"
|
||||
:label="t('harvester.storage.storageClass.label')"
|
||||
class="mt-20"
|
||||
required
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="create"
|
||||
:disabled="disableSave"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,151 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { BadgeState } from '@components/BadgeState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Card,
|
||||
Checkbox,
|
||||
AsyncButton,
|
||||
Banner,
|
||||
BadgeState
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
errors: [],
|
||||
unhealthyVM: '',
|
||||
force: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async apply(buttonCb) {
|
||||
this.errors = [];
|
||||
this.unhealthyVM = '';
|
||||
|
||||
try {
|
||||
const res = await this.actionResource.doAction('maintenancePossible');
|
||||
|
||||
if (this.force) {
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
await this.actionResource.doAction('enableMaintenanceMode', { force: 'true' });
|
||||
buttonCb(true);
|
||||
this.close();
|
||||
} else {
|
||||
buttonCb(false);
|
||||
}
|
||||
} else if (res._status === 200 || res._status === 204) {
|
||||
const res = await this.actionResource.doAction('listUnhealthyVM');
|
||||
|
||||
if (res.message) {
|
||||
this.unhealthyVM = res;
|
||||
buttonCb(false);
|
||||
} else {
|
||||
await this.actionResource.doAction('enableMaintenanceMode', { force: 'false' });
|
||||
buttonCb(true);
|
||||
this.close();
|
||||
}
|
||||
} else {
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (e) {
|
||||
const error = [e?.data] || exceptionToErrorsArray(e);
|
||||
|
||||
this.errors = error;
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.host.enableMaintenance.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div>
|
||||
<Checkbox
|
||||
v-model="force"
|
||||
label-key="harvester.host.enableMaintenance.force"
|
||||
/>
|
||||
</div>
|
||||
<Banner color="warning" :label="t('harvester.host.enableMaintenance.protip')" class="mb-0" />
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
|
||||
<div v-if="unhealthyVM">
|
||||
<Banner color="error mb-5">
|
||||
<p>
|
||||
{{ unhealthyVM.message }}
|
||||
</p>
|
||||
</Banner>
|
||||
|
||||
<div class="vm-list mb-5">
|
||||
<BadgeState
|
||||
v-for="vm in unhealthyVM.vms"
|
||||
:key="vm"
|
||||
color="bg-error mb-5 mr-5"
|
||||
:label="vm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
@click="apply"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vm-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,179 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { NODE } from '@shell/config/types';
|
||||
import { HCI } from '../types';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AsyncButton, Banner, Card, LabeledSelect
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
try {
|
||||
if (!this.actionResource.hasAction('findMigratableNodes')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await this.actionResource.$dispatch('resourceAction', {
|
||||
resource: this.actionResource,
|
||||
actionName: 'findMigratableNodes',
|
||||
body: {},
|
||||
opt: {},
|
||||
});
|
||||
|
||||
this.availableNodes = res.nodes;
|
||||
} catch (err) {
|
||||
this.actionResource.$dispatch('growl/fromError', {
|
||||
title: this.t('generic.notification.title.error'),
|
||||
err: err.data || err,
|
||||
}, { root: true });
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
nodeName: '',
|
||||
errors: [],
|
||||
availableNodes: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
|
||||
vmi() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI);
|
||||
const resource = vmiResources.find(VMI => VMI.id === this.actionResource?.id) || null;
|
||||
|
||||
return resource;
|
||||
},
|
||||
|
||||
nodeNameList() {
|
||||
const nodes = this.$store.getters['harvester/all'](NODE);
|
||||
|
||||
return nodes.filter((n) => {
|
||||
// do not allow to migrate to self node
|
||||
return !!this.availableNodes.includes(n.id);
|
||||
}).map((n) => {
|
||||
let label = n?.metadata?.name;
|
||||
const value = n?.metadata?.name;
|
||||
const custom = n?.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CUSTOM_NAME];
|
||||
|
||||
if (custom) {
|
||||
label = custom;
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
value
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.nodeName = '';
|
||||
this.errors = [];
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async apply(buttonDone) {
|
||||
if (!this.actionResource) {
|
||||
buttonDone(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.nodeName) {
|
||||
const name = this.$store.getters['i18n/t']('harvester.modal.migration.fields.nodeName.label');
|
||||
const message = this.$store.getters['i18n/t']('validation.required', { key: name });
|
||||
|
||||
this.$set(this, 'errors', [message]);
|
||||
buttonDone(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false);
|
||||
|
||||
buttonDone(true);
|
||||
this.close();
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonDone(false);
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.migration.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<LabeledSelect
|
||||
v-model="nodeName"
|
||||
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
||||
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
||||
:options="nodeNameList"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
:disabled="!nodeName"
|
||||
@click="apply"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,167 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { HCI } from '../types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterRestoreModal',
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
Card,
|
||||
LabeledSelect
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const hash = await allHash({ backups: this.$store.dispatch('harvester/findAll', { type: HCI.BACKUP }) });
|
||||
|
||||
this.backups = hash.backups;
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
backups: [],
|
||||
backupName: '',
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
|
||||
backupOption() {
|
||||
const attachBackup = this.backups.filter((B) => {
|
||||
return B.attachVM === this.actionResource?.metadata?.name;
|
||||
});
|
||||
|
||||
return attachBackup.map((O) => {
|
||||
return {
|
||||
value: O.metadata.name,
|
||||
label: O.metadata.name
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.backupName = '';
|
||||
this.errors = [];
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async saveRestore(buttonCb) {
|
||||
const name = `restore-${ this.backupName }-${ randomStr(5).toLowerCase() }`;
|
||||
|
||||
if (!this.backupName) {
|
||||
this.$set(this, 'errors', [
|
||||
this.t('harvester.modal.restore.message.backup')
|
||||
]);
|
||||
buttonCb(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.actionResource.doAction(
|
||||
'restore',
|
||||
{ backupName: this.backupName, name },
|
||||
{},
|
||||
false
|
||||
);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch(
|
||||
'growl/success',
|
||||
{
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.restore.success', { name: this.backupName })
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this.$set(this, 'errors', error);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.restore.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<LabeledSelect
|
||||
v-model="backupName"
|
||||
:label="t('harvester.modal.restore.selectBackup')"
|
||||
:localized-label="true"
|
||||
:options="backupOption"
|
||||
required
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="create"
|
||||
:disabled="!backupName"
|
||||
@click="saveRestore"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,221 @@
|
||||
<script>
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import GraphCircle from '@shell/components/graph/Circle';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AppModal from '@shell/components/AppModal.vue';
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default {
|
||||
name: 'SupportBundle',
|
||||
|
||||
components: {
|
||||
LabeledInput,
|
||||
GraphCircle,
|
||||
AsyncButton,
|
||||
Banner,
|
||||
AppModal,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
url: '',
|
||||
description: '',
|
||||
errors: [],
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
bundlePending() {
|
||||
return this.$store.getters['harvester-common/isBundlePending'];
|
||||
},
|
||||
|
||||
isShowBundleModal() {
|
||||
return this.$store.getters['harvester-common/isShowBundleModal'];
|
||||
},
|
||||
|
||||
percentage() {
|
||||
return this.$store.getters['harvester-common/getBundlePercentage'];
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
isShowBundleModal: {
|
||||
handler(show) {
|
||||
if (show) {
|
||||
this.$nextTick(() => {
|
||||
this.isOpen = true;
|
||||
});
|
||||
} else {
|
||||
this.isOpen = false;
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
stringify,
|
||||
|
||||
close() {
|
||||
this.$store.commit('harvester-common/toggleBundleModal', false);
|
||||
this.backUpName = '';
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
this.errors = [];
|
||||
|
||||
const name = `bundle-${ randomStr(5).toLowerCase() }`;
|
||||
const namespace = 'harvester-system';
|
||||
|
||||
const bundleCrd = {
|
||||
apiVersion: 'harvesterhci.io/v1beta1',
|
||||
type: HCI.SUPPORT_BUNDLE,
|
||||
kind: 'SupportBundle',
|
||||
metadata: {
|
||||
name,
|
||||
namespace
|
||||
},
|
||||
spec: {
|
||||
issueURL: this.url,
|
||||
description: this.description
|
||||
}
|
||||
};
|
||||
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||
|
||||
try {
|
||||
await bundleValue.save();
|
||||
|
||||
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
|
||||
this.$store.dispatch('harvester-common/bundleProgress', { root: true });
|
||||
} catch (err) {
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
buttonCb(false);
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bundleModal">
|
||||
<app-modal
|
||||
v-if="isOpen"
|
||||
name="bundle-modal"
|
||||
:click-to-close="false"
|
||||
:width="550"
|
||||
:height="390"
|
||||
class="remove-modal support-modal"
|
||||
>
|
||||
<div class="p-20">
|
||||
<h2>
|
||||
{{ t('harvester.modal.bundle.title') }}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
v-if="!bundlePending"
|
||||
class="content"
|
||||
>
|
||||
<LabeledInput
|
||||
v-model="url"
|
||||
:label="t('harvester.modal.bundle.url')"
|
||||
class="mb-20"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model="description"
|
||||
:label="t('harvester.modal.bundle.description')"
|
||||
type="multiline"
|
||||
:min-height="120"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="content"
|
||||
>
|
||||
<div class="circle">
|
||||
<GraphCircle
|
||||
primary-stroke-color="green"
|
||||
secondary-stroke-color="white"
|
||||
:stroke-width="6"
|
||||
:percentage="percentage"
|
||||
:show-text="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(err, idx) in errors"
|
||||
:key="idx"
|
||||
>
|
||||
<Banner
|
||||
color="error"
|
||||
:label="stringify(err)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="footer mt-20">
|
||||
<button
|
||||
class="btn btn-sm role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.close') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
type="submit"
|
||||
mode="generate"
|
||||
class="btn btn-sm bg-primary"
|
||||
:disabled="bundlePending"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</app-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bundleModal {
|
||||
.support-modal {
|
||||
border-radius: var(--border-radius);
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.bundle {
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.icon-spinner {
|
||||
font-size: 100px;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 218px;
|
||||
|
||||
.circle {
|
||||
padding-top: 20px;
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterHotUnplugModal',
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Card,
|
||||
Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { errors: [] };
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState('action-menu', ['modalData']),
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
diskName() {
|
||||
return this.modalData.diskName;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
try {
|
||||
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName });
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch(
|
||||
'growl/success',
|
||||
{
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName })
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this.$set(this, 'errors', error);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card ref="modal" name="modal" :show-highlight-border="false">
|
||||
<h4
|
||||
slot="title"
|
||||
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
|
||||
class="text-default-text"
|
||||
/>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button type="button" class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
:action-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||
:success-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { HCI } from '../types';
|
||||
import { BACKUP_TYPE } from '../config/types';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterVMSnapshotDialog',
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Card,
|
||||
LabeledInput,
|
||||
Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
snapshotName: '',
|
||||
snapshotNamespace: '',
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.snapshotNamespace = '';
|
||||
this.snapshotName = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
if (this.actionResource) {
|
||||
try {
|
||||
const snapshot = await this.$store.dispatch('harvester/create', {
|
||||
metadata: {
|
||||
name: this.snapshotName,
|
||||
namespace: this.actionResource.metadata.namespace,
|
||||
ownerReferences: this.getOwnerReferencesFromVM(this.actionResource)
|
||||
},
|
||||
spec: {
|
||||
source: {
|
||||
apiGroup: 'kubevirt.io',
|
||||
kind: 'VirtualMachine',
|
||||
name: this.actionResource.metadata.name
|
||||
},
|
||||
type: BACKUP_TYPE.SNAPSHOT
|
||||
},
|
||||
type: HCI.BACKUP
|
||||
});
|
||||
|
||||
await snapshot.save();
|
||||
|
||||
this.$store.dispatch(
|
||||
'growl/success',
|
||||
{
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.vmSnapshot.success', { name: this.snapshotName })
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
|
||||
this.close();
|
||||
|
||||
buttonCb(true);
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getOwnerReferencesFromVM(resource) {
|
||||
const name = resource.metadata.name;
|
||||
const kind = resource.kind;
|
||||
const apiVersion = 'kubevirt.io/v1';
|
||||
const uid = resource?.metadata?.uid;
|
||||
|
||||
return [{
|
||||
name,
|
||||
kind,
|
||||
uid,
|
||||
apiVersion,
|
||||
}];
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<h4
|
||||
slot="title"
|
||||
v-clean-html="t('harvester.modal.vmSnapshot.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
|
||||
<template #body>
|
||||
<LabeledInput v-model="actionResource.metadata.namespace" :disabled="true" :label="t('generic.namespace')" />
|
||||
<LabeledInput v-model="snapshotName" class="mt-20" :label="t('generic.name')" required />
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton mode="create" :disabled="!snapshotName" @click="save" />
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { HCI } from '@pkg/harvester/types';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { clone } from '@shell/utils/object';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
Card,
|
||||
LabeledSelect,
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
clusterNetwork: '',
|
||||
errors: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
|
||||
clusterNetworks() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK);
|
||||
|
||||
return clusterNetworks.filter((c) => {
|
||||
return c.id !== this.actionResource.spec?.clusterNetwork && c.id !== 'mgmt';
|
||||
}).map((c) => {
|
||||
const label = c.id;
|
||||
const value = c.id;
|
||||
|
||||
return {
|
||||
label,
|
||||
value,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.nodeName = '';
|
||||
this.errors = [];
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async apply(buttonDone) {
|
||||
try {
|
||||
const data = clone(this.actionResource);
|
||||
|
||||
data.spec.clusterNetwork = this.clusterNetwork;
|
||||
|
||||
await this.$store.dispatch('harvester/request', {
|
||||
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
|
||||
buttonDone(true);
|
||||
this.close();
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonDone(false);
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.migration.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<LabeledSelect
|
||||
v-model="clusterNetwork"
|
||||
:label="t('harvester.harvesterVlanConfigMigrateDialog.targetClusterNetwork.label')"
|
||||
:placeholder="t('harvester.harvesterVlanConfigMigrateDialog.targetClusterNetwork.placeholder')"
|
||||
:options="clusterNetworks"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
:disabled="!clusterNetwork"
|
||||
@click="apply"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import { Card } from '@components/Card';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterHotUnplugModal',
|
||||
|
||||
components: { Card },
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState('action-menu', ['modalData']),
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
ok() {
|
||||
this.modalData?.callback('ok');
|
||||
this.$emit('close');
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card ref="modal" name="modal" :show-highlight-border="false">
|
||||
<h4
|
||||
slot="title"
|
||||
class="text-default-text"
|
||||
>
|
||||
{{ t('generic.tip') }}
|
||||
</h4>
|
||||
|
||||
<template #body>
|
||||
<p v-clean-html="t(modalData.contentKey)"></p>
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button type="button" class="btn role-secondary mr-10" @click="ok">
|
||||
{{ t('generic.ok') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
export default {
|
||||
components: {
|
||||
Card,
|
||||
AsyncButton,
|
||||
Banner,
|
||||
},
|
||||
props: {
|
||||
vm: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { errors: [], resolve: null };
|
||||
},
|
||||
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||
methods: {
|
||||
close() {
|
||||
this.resolve();
|
||||
this.$emit('close');
|
||||
},
|
||||
apply(buttonDone) {
|
||||
try {
|
||||
this.vm.doActionGrowl('restart', {});
|
||||
buttonDone(true);
|
||||
this.resolve();
|
||||
this.close();
|
||||
} catch (err) {
|
||||
console.error(err); // eslint-disable-line
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
buttonDone(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-modal
|
||||
class="restart-modal"
|
||||
name="restartDialog"
|
||||
:width="600"
|
||||
height="auto"
|
||||
:click-to-close="false"
|
||||
@close="close"
|
||||
>
|
||||
<Card
|
||||
class="prompt-restart"
|
||||
:show-highlight-border="false"
|
||||
>
|
||||
<h4
|
||||
slot="title"
|
||||
v-clean-html="t('harvester.modal.restart.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
|
||||
<template slot="body">
|
||||
<slot name="body">
|
||||
<div
|
||||
v-clean-html="t('harvester.modal.restart.tip')"
|
||||
class="pl-10 pr-10"
|
||||
>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<div
|
||||
slot="actions"
|
||||
class="bottom"
|
||||
>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('harvester.modal.restart.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="restart"
|
||||
@click="apply"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</app-modal>
|
||||
</template>
|
||||
<style lang='scss' scoped>
|
||||
.restart-modal {
|
||||
z-index: 45;
|
||||
}
|
||||
.prompt-restart {
|
||||
margin: 0;
|
||||
}
|
||||
.bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
.banner {
|
||||
margin-top: 0
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { STORAGE_CLASS } from '@shell/config/types';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterRestoreSnapshotDialog',
|
||||
components: {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
Card,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
},
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
errors: [],
|
||||
storageClassName: '',
|
||||
};
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = { storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }) };
|
||||
|
||||
await allHash(hash);
|
||||
|
||||
if (this.showStorageClass) {
|
||||
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find(s => s.isDefault);
|
||||
|
||||
const currentStorageName = this.resources[0].metadata?.annotations[HCI_ANNOTATIONS.STORAGE_CLASS];
|
||||
|
||||
this.$set(this, 'storageClassName', currentStorageName || defaultStorage?.metadata?.name || 'longhorn');
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
actionResource() {
|
||||
return this.resources[0] || {};
|
||||
},
|
||||
disableSave() {
|
||||
return !(this.name && (this.showStorageClass ? this.storageClassName : true));
|
||||
},
|
||||
|
||||
storageClassOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||
|
||||
const out = storages.filter(s => !s.parameters?.backingImage).filter((s) => {
|
||||
return s.provisioner === this.actionResource.metadata?.annotations[HCI_ANNOTATIONS.STORAGE_PROVISIONER];
|
||||
}).map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
showStorageClass() {
|
||||
return this.actionResource?.volume?.source === 'data';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.name = '';
|
||||
this.storageClassName = '';
|
||||
|
||||
this.$emit('close');
|
||||
},
|
||||
async save(buttonCb) {
|
||||
try {
|
||||
const payload = { name: this.name };
|
||||
|
||||
if (this.showStorageClass) {
|
||||
payload.storageClassName = this.storageClassName;
|
||||
}
|
||||
|
||||
const res = await this.actionResource.doAction('restore', payload);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('harvester.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.restoreSnapshot.success', { name: this.name })
|
||||
}, { root: true });
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this.$set(this, 'errors', error);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.restoreSnapshot.title') }}
|
||||
</template>
|
||||
<template #body>
|
||||
<LabeledInput
|
||||
v-model="name"
|
||||
:label="t('harvester.modal.restoreSnapshot.name')"
|
||||
required
|
||||
/>
|
||||
<LabeledSelect
|
||||
v-if="showStorageClass"
|
||||
v-model="storageClassName"
|
||||
:options="storageClassOptions"
|
||||
:label="t('harvester.storage.storageClass.label')"
|
||||
class="mt-20"
|
||||
required
|
||||
/>
|
||||
</template>
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
mode="create"
|
||||
:disabled="disableSave"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
export default {
|
||||
name: 'HarvesterSnapshotDialog',
|
||||
components: {
|
||||
AsyncButton, Banner, Card, LabeledInput
|
||||
},
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
disableSave() {
|
||||
return !this.name;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.name = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
async save(buttonCb) {
|
||||
try {
|
||||
const res = await this.actionResource.doAction('snapshot', { name: this.name });
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('harvester.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.snapshot.message.success', { name: this.name })
|
||||
}, { root: true });
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this.$set(this, 'errors', error);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.snapshot.title') }}
|
||||
</template>
|
||||
<template #body>
|
||||
<LabeledInput
|
||||
v-model="name"
|
||||
:label="t('harvester.modal.snapshot.name')"
|
||||
required
|
||||
/>
|
||||
</template>
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
mode="create"
|
||||
:disabled="disableSave"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterPvcCloneDialog',
|
||||
components: {
|
||||
AsyncButton, Banner, Card, LabeledInput, Checkbox
|
||||
},
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
disableSave() {
|
||||
return this.cloneData && !this.name;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.name = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
async save(buttonCb) {
|
||||
if (!this.cloneData) {
|
||||
this.resources[0].goToClone();
|
||||
buttonCb(false);
|
||||
this.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.actionResource.doAction('clone', { name: this.name });
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('harvester.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.volumeClone.message.success', { name: this.name })
|
||||
}, { root: true });
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this.$set(this, 'errors', error);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this.$set(this, 'errors', message);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.volumeClone.title') }}
|
||||
</template>
|
||||
<template #body>
|
||||
<Checkbox v-model="cloneData" class="mb-10" label-key="harvester.modal.cloneVM.type" />
|
||||
|
||||
<LabeledInput
|
||||
v-show="cloneData"
|
||||
v-model="name"
|
||||
class="mb-20"
|
||||
:label="t('harvester.modal.volumeClone.name')"
|
||||
required
|
||||
/>
|
||||
</template>
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
mode="create"
|
||||
:disabled="disableSave"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user