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,498 @@
|
||||
<script>
|
||||
import draggable from 'vuedraggable';
|
||||
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 { HCI } from '../../../types';
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { removeObject } from '@shell/utils/array';
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { SOURCE_TYPE } from '../../../config/harvester-map';
|
||||
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
||||
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Banner, BadgeStateFormatter, draggable, 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 = 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.$set(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('input', 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"
|
||||
/>
|
||||
<draggable
|
||||
v-model="rows"
|
||||
:disabled="isView"
|
||||
@end="update"
|
||||
>
|
||||
<transition-group>
|
||||
<div
|
||||
v-for="(volume, i) in rows"
|
||||
:key="volume.id"
|
||||
>
|
||||
<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)"
|
||||
v-model="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>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
<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>
|
||||
Reference in New Issue
Block a user