mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-06-13 05:22:21 +00:00
311 lines
7.5 KiB
Vue
311 lines
7.5 KiB
Vue
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import { Banner } from '@components/Banner';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
import { CONFIG_MAP, SECRET, SERVICE_ACCOUNT } from '@shell/config/types';
|
|
import { _VIEW } from '@shell/config/query-params';
|
|
import CopyToClipboard from '@shell/components/CopyToClipboard';
|
|
import MessageLink from '@shell/components/MessageLink';
|
|
import { FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
|
|
|
const MAX_FILESYSTEMS = 3;
|
|
|
|
const { CONFIGMAP: FS_TYPE_CONFIGMAP, SECRET: FS_TYPE_SECRET, SERVICEACCOUNT: FS_TYPE_SERVICEACCOUNT } = FILESYSTEM_SOURCE_TYPE;
|
|
|
|
const DEFAULT_VOLUME_NAMES = {
|
|
[FS_TYPE_CONFIGMAP]: 'appconfigfs',
|
|
[FS_TYPE_SECRET]: 'appsecretfs',
|
|
[FS_TYPE_SERVICEACCOUNT]: 'appserviceaccountfs',
|
|
};
|
|
|
|
const FS_TYPE_OPTIONS = [
|
|
{ label: 'ConfigMap', value: FS_TYPE_CONFIGMAP },
|
|
{ label: 'Secret', value: FS_TYPE_SECRET },
|
|
{ label: 'ServiceAccount', value: FS_TYPE_SERVICEACCOUNT },
|
|
];
|
|
|
|
function emptyRow() {
|
|
return {
|
|
fsType: FS_TYPE_CONFIGMAP,
|
|
volumeName: DEFAULT_VOLUME_NAMES[FS_TYPE_CONFIGMAP],
|
|
resourceName: '',
|
|
};
|
|
}
|
|
|
|
export default {
|
|
name: 'VirtualMachineFilesystem',
|
|
|
|
emits: ['update:value'],
|
|
|
|
components: {
|
|
Banner,
|
|
LabeledSelect,
|
|
LabeledInput,
|
|
CopyToClipboard,
|
|
MessageLink,
|
|
},
|
|
|
|
props: {
|
|
mode: {
|
|
type: String,
|
|
default: 'create',
|
|
},
|
|
namespace: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
value: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
},
|
|
|
|
data() {
|
|
return { rows: this.value.length > 0 ? this.value.map((r) => ({ ...r })) : [emptyRow()] };
|
|
},
|
|
|
|
watch: {
|
|
value(newVal) {
|
|
if (newVal) {
|
|
const incoming = JSON.stringify(newVal);
|
|
const current = JSON.stringify(this.rows);
|
|
|
|
if (incoming !== current) {
|
|
this.rows = newVal.map((r) => ({ ...r }));
|
|
}
|
|
}
|
|
},
|
|
|
|
rows: {
|
|
deep: true,
|
|
handler(val) {
|
|
this.$emit('update:value', val.map((r) => ({ ...r })));
|
|
},
|
|
},
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters({ t: 'i18n/t' }),
|
|
|
|
inStore() {
|
|
return this.$store.getters['currentProduct'].inStore;
|
|
},
|
|
|
|
configMaps() {
|
|
return this.$store.getters[`${ this.inStore }/all`](CONFIG_MAP)
|
|
.filter((cm) => !this.namespace || cm.metadata.namespace === this.namespace)
|
|
.map((cm) => ({ label: cm.metadata.name, value: cm.metadata.name }));
|
|
},
|
|
|
|
secrets() {
|
|
return this.$store.getters[`${ this.inStore }/all`](SECRET)
|
|
.filter((s) => !this.namespace || s.metadata.namespace === this.namespace)
|
|
.map((s) => ({ label: s.metadata.name, value: s.metadata.name }));
|
|
},
|
|
|
|
serviceAccounts() {
|
|
return this.$store.getters[`${ this.inStore }/all`](SERVICE_ACCOUNT)
|
|
.filter((sa) => !this.namespace || sa.metadata.namespace === this.namespace)
|
|
.map((sa) => ({ label: sa.metadata.name, value: sa.metadata.name }));
|
|
},
|
|
|
|
canAddRow() {
|
|
return this.rows.length < MAX_FILESYSTEMS;
|
|
},
|
|
|
|
isView() {
|
|
return this.mode === _VIEW;
|
|
},
|
|
|
|
completedRows() {
|
|
return this.rows.filter((r) => r.fsType && r.volumeName && r.resourceName);
|
|
},
|
|
|
|
allMountCommands() {
|
|
return this.completedRows.map((r) => this.mountCommands(r)).join('\n');
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
fsTypeOptions(currentIndex) {
|
|
const usedTypes = this.rows
|
|
.filter((_, i) => i !== currentIndex)
|
|
.map((r) => r.fsType);
|
|
|
|
return FS_TYPE_OPTIONS.filter((opt) => !usedTypes.includes(opt.value));
|
|
},
|
|
|
|
resourceOptions(fsType) {
|
|
if (fsType === FS_TYPE_CONFIGMAP) return this.configMaps;
|
|
if (fsType === FS_TYPE_SECRET) return this.secrets;
|
|
if (fsType === FS_TYPE_SERVICEACCOUNT) return this.serviceAccounts;
|
|
|
|
return [];
|
|
},
|
|
|
|
onFsTypeChange(row, newType) {
|
|
row.fsType = newType;
|
|
row.volumeName = DEFAULT_VOLUME_NAMES[newType] || '';
|
|
row.resourceName = '';
|
|
},
|
|
|
|
addRow() {
|
|
if (this.canAddRow) {
|
|
const usedTypes = this.rows.map((r) => r.fsType);
|
|
const nextType = FS_TYPE_OPTIONS.find((opt) => !usedTypes.includes(opt.value))?.value || FS_TYPE_CONFIGMAP;
|
|
|
|
this.rows.push({
|
|
fsType: nextType,
|
|
volumeName: DEFAULT_VOLUME_NAMES[nextType] || '',
|
|
resourceName: '',
|
|
});
|
|
}
|
|
},
|
|
|
|
removeRow(index) {
|
|
this.rows.splice(index, 1);
|
|
},
|
|
|
|
mountCommands(row) {
|
|
const vol = row.volumeName || '<volume-name>';
|
|
|
|
return `- mkdir -p /mnt/${ vol }\n- mount -t virtiofs ${ vol } /mnt/${ vol }`;
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="vm-filesystem">
|
|
<p class="mb-20">
|
|
{{ t('harvester.virtualMachine.filesystem.description') }}
|
|
</p>
|
|
|
|
<div
|
|
v-for="(row, index) in rows"
|
|
:key="index"
|
|
class="filesystem-row mb-15"
|
|
>
|
|
<div class="row">
|
|
<div class="col span-3">
|
|
<LabeledSelect
|
|
:value="row.fsType"
|
|
:label="t('harvester.virtualMachine.filesystem.type')"
|
|
:options="fsTypeOptions(index)"
|
|
:mode="mode"
|
|
required
|
|
@update:value="onFsTypeChange(row, $event)"
|
|
/>
|
|
</div>
|
|
|
|
<div class="col span-3">
|
|
<LabeledInput
|
|
v-model:value="row.volumeName"
|
|
:label="t('harvester.virtualMachine.filesystem.volume')"
|
|
:mode="mode"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="col span-5">
|
|
<LabeledSelect
|
|
v-model:value="row.resourceName"
|
|
:label="t('harvester.virtualMachine.filesystem.resource')"
|
|
:options="resourceOptions(row.fsType)"
|
|
:mode="mode"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="!isView"
|
|
class="col span-1 remove-col"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="btn role-link remove-btn"
|
|
@click="removeRow(index)"
|
|
>
|
|
{{ t('generic.remove') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Banner
|
|
v-if="completedRows.length > 0 && mode === 'create'"
|
|
color="warning"
|
|
class="mt-10"
|
|
>
|
|
<div>
|
|
<MessageLink
|
|
:to="{ hash: '#advanced' }"
|
|
prefix-label="harvester.virtualMachine.filesystem.mountBannerHint"
|
|
middle-label="harvester.virtualMachine.filesystem.mountBannerHintLink"
|
|
suffix-label="harvester.virtualMachine.filesystem.mountBannerHintSuffix"
|
|
/>
|
|
<div class="pre-wrapper mt-10">
|
|
<pre class="mt-5 mb-0">{{ allMountCommands }}</pre>
|
|
<CopyToClipboard
|
|
:text="allMountCommands"
|
|
:show-label="false"
|
|
class="icon-btn"
|
|
action-color="bg-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Banner>
|
|
|
|
<button
|
|
v-if="!isView && canAddRow"
|
|
type="button"
|
|
class="btn role-tertiary add"
|
|
@click="addRow"
|
|
>
|
|
{{ t('harvester.virtualMachine.filesystem.add') }}
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.vm-filesystem {
|
|
padding: 10px 0;
|
|
}
|
|
|
|
.filesystem-row {
|
|
border-bottom: 1px solid var(--border);
|
|
padding-bottom: 15px;
|
|
}
|
|
|
|
.remove-col {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.remove-btn {
|
|
padding: 0;
|
|
}
|
|
|
|
.pre-wrapper {
|
|
position: relative;
|
|
|
|
pre {
|
|
padding-right: 36px;
|
|
}
|
|
|
|
.icon-btn {
|
|
position: absolute;
|
|
top: 0px;
|
|
right: 5px;
|
|
padding: 2px 4px;
|
|
line-height: 1;
|
|
|
|
&:active {
|
|
background-color: var(--success) !important;
|
|
}
|
|
}
|
|
}
|
|
</style>
|