mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2025-12-13 21:21:44 +00:00
499 lines
12 KiB
Vue
499 lines
12 KiB
Vue
<script>
|
|
import { Sortable } from "sortablejs-vue3";
|
|
import InfoBox from '@shell/components/InfoBox';
|
|
import { Banner } from '@components/Banner';
|
|
import BadgeStateFormatter from '@shell/components/formatter/BadgeStateFormatter';
|
|
import UnitInput from '@shell/components/form/UnitInput';
|
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
|
|
|
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
|
import { clone } from '@shell/utils/object';
|
|
import { removeObject } from '@shell/utils/array';
|
|
import { randomStr } from '@shell/utils/string';
|
|
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
|
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
|
import { SOURCE_TYPE } from '../../../config/harvester-map';
|
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
|
import { HCI } from '../../../types';
|
|
|
|
export default {
|
|
emits: ['update:value'],
|
|
|
|
components: {
|
|
Banner, BadgeStateFormatter, Sortable, InfoBox, LabeledInput, UnitInput, LabeledSelect, ModalWithCard
|
|
},
|
|
|
|
props: {
|
|
vm: {
|
|
type: Object,
|
|
default: () => {
|
|
return {};
|
|
}
|
|
},
|
|
|
|
mode: {
|
|
type: String,
|
|
default: _CREATE
|
|
},
|
|
|
|
value: {
|
|
type: Array,
|
|
default: () => {
|
|
return [];
|
|
}
|
|
},
|
|
|
|
namespace: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
|
|
existingVolumeDisabled: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
validateRequired: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
customVolumeMode: {
|
|
type: String,
|
|
default: 'Block'
|
|
},
|
|
|
|
customAccessMode: {
|
|
type: String,
|
|
default: 'ReadWriteMany'
|
|
},
|
|
|
|
resourceType: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
},
|
|
|
|
async fetch() {
|
|
await this.$store.dispatch('harvester/findAll', { type: PVC });
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
SOURCE_TYPE,
|
|
rows: clone(this.value),
|
|
nameIdx: 1,
|
|
vol: null,
|
|
isOpen: false,
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
dev() {
|
|
try {
|
|
return this.$store.getters['prefs/get'](PLUGIN_DEVELOPER);
|
|
} catch {
|
|
return this.$store.getters['prefs/get'](DEV);
|
|
}
|
|
},
|
|
|
|
isVirtualType() {
|
|
return this.resourceType === HCI.VM;
|
|
},
|
|
|
|
isView() {
|
|
return this.mode === _VIEW;
|
|
},
|
|
|
|
isEdit() {
|
|
return this.mode === _EDIT;
|
|
},
|
|
|
|
isCreate() {
|
|
return this.mode === _CREATE;
|
|
},
|
|
|
|
showVolumeTip() {
|
|
const imageName = this.getImageDisplayName(this.rows[0]?.image);
|
|
|
|
if (this.rows.length === 1 && this.rows[0].type === 'cd-rom' && /.iso$/i.test(imageName)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
pvcs() {
|
|
return this.$store.getters['harvester/all'](PVC) || [];
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
value: {
|
|
handler(neu) {
|
|
const rows = clone(neu).map((V) => {
|
|
if (!this.isCreate && V.source !== SOURCE_TYPE.CONTAINER && !V.newCreateId) {
|
|
V.to = {
|
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-namespace-id`,
|
|
params: {
|
|
resource: HCI.VOLUME,
|
|
namespace: this.namespace,
|
|
id: V.realName
|
|
},
|
|
query: { mode: _EDIT }
|
|
};
|
|
|
|
V.pvc = this.pvcs.find(pvc => pvc.metadata.name === V.realName);
|
|
}
|
|
|
|
return V;
|
|
});
|
|
|
|
this['rows'] = rows;
|
|
},
|
|
deep: true,
|
|
immediate: true,
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
addVolume(type) {
|
|
const name = this.generateName();
|
|
const neu = {
|
|
id: randomStr(5),
|
|
name,
|
|
source: type,
|
|
size: '10Gi',
|
|
type: 'disk',
|
|
accessMode: this.customAccessMode,
|
|
volumeMode: this.customVolumeMode,
|
|
volumeName: '',
|
|
bus: 'virtio',
|
|
newCreateId: randomStr(10), // judge whether it is a disk that has been created
|
|
};
|
|
|
|
if (type === SOURCE_TYPE.NEW) {
|
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find( O => O.isDefault);
|
|
|
|
neu.storageClassName = defaultStorage?.metadata?.name || 'longhorn';
|
|
}
|
|
|
|
this.rows.push(neu);
|
|
this.update();
|
|
},
|
|
|
|
generateName() {
|
|
let name = '';
|
|
let hasName = true;
|
|
|
|
while (hasName) {
|
|
name = `disk-${ this.nameIdx }`;
|
|
hasName = this.rows.find(O => O.name === name);
|
|
this.nameIdx++;
|
|
}
|
|
|
|
return name;
|
|
},
|
|
|
|
removeVolume(vol) {
|
|
this.vol = vol;
|
|
if (!vol.newCreateId && this.isEdit && this.isVirtualType) {
|
|
this.isOpen = true;
|
|
} else {
|
|
removeObject(this.rows, vol);
|
|
this.update();
|
|
}
|
|
},
|
|
|
|
unplugVolume(volume) {
|
|
this.vm.unplugVolume(volume.name);
|
|
},
|
|
|
|
componentFor(type) {
|
|
switch (type) {
|
|
case SOURCE_TYPE.NEW:
|
|
return require(`./type/volume.vue`).default;
|
|
case SOURCE_TYPE.IMAGE:
|
|
return require(`./type/vmImage.vue`).default;
|
|
case SOURCE_TYPE.ATTACH_VOLUME:
|
|
return require(`./type/existing.vue`).default;
|
|
case SOURCE_TYPE.CONTAINER:
|
|
return require(`./type/container.vue`).default;
|
|
}
|
|
},
|
|
|
|
headerFor(type) {
|
|
return {
|
|
[SOURCE_TYPE.NEW]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.volume'),
|
|
[SOURCE_TYPE.IMAGE]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.vmImage'),
|
|
[SOURCE_TYPE.ATTACH_VOLUME]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.existingVolume'),
|
|
[SOURCE_TYPE.CONTAINER]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.container'),
|
|
}[type];
|
|
},
|
|
|
|
update() {
|
|
this.$emit('update:value', this.rows);
|
|
},
|
|
|
|
deleteVolume() {
|
|
removeObject(this.rows, this.vol);
|
|
this.update();
|
|
this.cancel();
|
|
},
|
|
|
|
cancel() {
|
|
this.isOpen = false;
|
|
},
|
|
|
|
changeSort(idx, type) {
|
|
// true: down, false: up
|
|
this.rows.splice(type ? idx : idx - 1, 1, ...this.rows.splice(type ? idx + 1 : idx, 1, this.rows[type ? idx : idx - 1]));
|
|
this.update();
|
|
},
|
|
|
|
getImageDisplayName(id) {
|
|
return this.$store.getters['harvester/all'](HCI.IMAGE).find(image => image.id === id)?.spec?.displayName;
|
|
}
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<Banner
|
|
v-if="!isView"
|
|
color="info"
|
|
label-key="harvester.virtualMachine.volume.dragTip"
|
|
/>
|
|
<Sortable
|
|
:list="rows"
|
|
:options="{disabled: isView}"
|
|
@end="update"
|
|
item-key="id"
|
|
>
|
|
<template #item="{element: volume, index: i}">
|
|
<div :key="volume.name">
|
|
<InfoBox class="box">
|
|
<button
|
|
v-if="!isView"
|
|
type="button"
|
|
class="role-link btn btn-sm remove"
|
|
@click="removeVolume(volume)"
|
|
>
|
|
<i class="icon icon-x" />
|
|
</button>
|
|
<button
|
|
v-if="volume.hotpluggable && isView"
|
|
type="button"
|
|
class="role-link btn remove"
|
|
@click="unplugVolume(volume)"
|
|
>
|
|
{{ t('harvester.virtualMachine.unplug.detachVolume') }}
|
|
</button>
|
|
<h3>
|
|
<span
|
|
v-if="volume.to && isVirtualType"
|
|
class="title"
|
|
>
|
|
<router-link :to="volume.to">
|
|
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
|
</router-link>
|
|
|
|
<BadgeStateFormatter
|
|
v-if="volume.pvc"
|
|
class="ml-10 state"
|
|
:arbitrary="true"
|
|
:row="volume.pvc"
|
|
:value="volume.pvc.state"
|
|
/>
|
|
<a
|
|
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
|
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
|
class="ml-5 resource-external"
|
|
rel="nofollow noopener noreferrer"
|
|
target="_blank"
|
|
:href="volume.pvc.resourceExternalLink.url"
|
|
>
|
|
<i class="icon icon-external-link" />
|
|
</a>
|
|
</span>
|
|
|
|
<span v-else>
|
|
{{ headerFor(volume.source) }}
|
|
</span>
|
|
</h3>
|
|
<div>
|
|
<component
|
|
:is="componentFor(volume.source)"
|
|
:value="rows[i]"
|
|
:rows="rows"
|
|
:namespace="namespace"
|
|
:is-create="isCreate"
|
|
:is-edit="isEdit"
|
|
:is-view="isView"
|
|
:is-virtual-type="isVirtualType"
|
|
:mode="mode"
|
|
:idx="i"
|
|
:validate-required="validateRequired"
|
|
@update="update"
|
|
/>
|
|
</div>
|
|
|
|
<div class="bootOrder">
|
|
<div
|
|
v-if="!isView"
|
|
class="mr-15"
|
|
>
|
|
<button
|
|
:disabled="i === 0"
|
|
class="btn btn-sm role-primary"
|
|
@click.prevent="changeSort(i, false)"
|
|
>
|
|
<i class="icon icon-lg icon-chevron-up"></i>
|
|
</button>
|
|
|
|
<button
|
|
:disabled="i === rows.length -1"
|
|
class="btn btn-sm role-primary"
|
|
@click.prevent="changeSort(i, true)"
|
|
>
|
|
<i class="icon icon-lg icon-chevron-down"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="text-muted">
|
|
bootOrder: {{ i + 1 }}
|
|
</div>
|
|
</div>
|
|
|
|
<Banner
|
|
v-if="volume.volumeStatus && !isCreate"
|
|
class="mt-15 volume-status"
|
|
color="warning"
|
|
:label="volume.volumeStatus"
|
|
/>
|
|
</InfoBox>
|
|
</div>
|
|
</template>
|
|
</Sortable>
|
|
<Banner
|
|
v-if="showVolumeTip"
|
|
color="warning"
|
|
:label="t('harvester.virtualMachine.volume.volumeTip')"
|
|
/>
|
|
|
|
<div v-if="!isView">
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm bg-primary mr-15 mb-10"
|
|
:disabled="rows.length === 0"
|
|
@click="addVolume(SOURCE_TYPE.NEW)"
|
|
>
|
|
{{ t('harvester.virtualMachine.volume.addVolume') }}
|
|
</button>
|
|
|
|
<button
|
|
v-if="!existingVolumeDisabled"
|
|
type="button"
|
|
class="btn btn-sm bg-primary mr-15 mb-10"
|
|
@click="addVolume(SOURCE_TYPE.ATTACH_VOLUME)"
|
|
>
|
|
{{ t('harvester.virtualMachine.volume.addExistingVolume') }}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm bg-primary mr-15 mb-10"
|
|
@click="addVolume(SOURCE_TYPE.IMAGE)"
|
|
>
|
|
{{ t('harvester.virtualMachine.volume.addVmImage') }}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm bg-primary mb-10"
|
|
@click="addVolume(SOURCE_TYPE.CONTAINER)"
|
|
>
|
|
{{ t('harvester.virtualMachine.volume.addContainer') }}
|
|
</button>
|
|
</div>
|
|
|
|
<ModalWithCard
|
|
v-if="isOpen"
|
|
name="deleteTip"
|
|
:width="400"
|
|
>
|
|
<template #title>
|
|
{{ t('harvester.virtualMachine.volume.unmount.title') }}
|
|
</template>
|
|
|
|
<template #content>
|
|
<span>{{ t('harvester.virtualMachine.volume.unmount.message') }}</span>
|
|
</template>
|
|
|
|
<template #footer>
|
|
<div class="buttons">
|
|
<button
|
|
class="btn role-secondary mr-20"
|
|
@click.prevent="cancel"
|
|
>
|
|
{{ t('generic.no') }}
|
|
</button>
|
|
|
|
<button
|
|
class="btn bg-primary mr-20"
|
|
@click.prevent="deleteVolume"
|
|
>
|
|
{{ t('generic.yes') }}
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</ModalWithCard>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang='scss' scoped>
|
|
.box {
|
|
position: relative;
|
|
}
|
|
|
|
.title {
|
|
display: flex;
|
|
|
|
.state {
|
|
font-size: 16px;
|
|
}
|
|
}
|
|
|
|
.remove {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
padding: 0px;
|
|
}
|
|
|
|
.bootOrder {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.buttons {
|
|
width: 100%;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.volume-status:first-letter {
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.resource-external {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
</style>
|