Add pkg/harvester components + shell portings - 1

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia
2024-10-23 17:00:46 +02:00
parent f8408469f7
commit 4f2688f6ab
265 changed files with 42316 additions and 82 deletions
+134
View File
@@ -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>
+134
View File
@@ -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>
+118
View File
@@ -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>
+107
View File
@@ -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>
+114
View File
@@ -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>
+68
View File
@@ -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>
+120
View File
@@ -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>
+103
View File
@@ -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>
+118
View File
@@ -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>