Files
cloudron-box/dashboard/src/views/VolumesView.vue
T

320 lines
12 KiB
Vue

<script setup>
import { useI18n } from 'vue-i18n';
const i18n = useI18n();
const t = i18n.t;
import { computed, ref, useTemplateRef, onMounted } from 'vue';
import { Button, ButtonGroup, Checkbox, Dialog, SingleSelect, FormGroup, InputDialog, NumberInput, PasswordInput, TableView, TextInput } from 'pankow';
import Section from '../components/Section.vue';
import StateLED from '../components/StateLED.vue';
import VolumesModel from '../models/VolumesModel.js';
const volumesModel = VolumesModel.create();
const mountTypeOptions = [
{ name: 'CIFS', value: 'cifs' },
{ name: 'EXT4', value: 'ext4' },
{ name: 'Filesystem', value: 'filesystem' },
{ name: 'Filesystem (Mountpoint)', value: 'mountpoint' },
{ name: 'NFS', value: 'nfs' },
{ name: 'SSHFS', value: 'sshfs' },
{ name: 'XFS', value: 'xfs' },
];
const columns = {
status: {},
name: {
label: 'Name',
sort: true
},
mountType: {
label: 'Type',
sort: true ,
hideMobile: true,
},
target: {
label: 'Target',
sort: true,
hideMobile: true,
},
actions: {}
};
const busy = ref(true);
const volumes = ref([]);
const volumeDialogData = ref({
error: null,
busy: false,
mode: '', // edit or new
name: '',
// dynamic extra props from openVolumeDialog
});
const volumeDialogValid = computed(() => {
const data = volumeDialogData.value;
if (data.mode === 'new') {
if (!data.name) return false;
if (!data.mountType) return false;
}
switch (data.mountType) {
case 'filesystem':
case 'mountpoint':
if (!data.hostPath) return false;
if (!data.hostPath) return false;
break;
case 'ext4':
case 'xfs':
if (!data.diskPath) return false;
break;
case 'nfs':
if (!data.host) return false;
if (!data.remoteDir) return false;
break;
case 'sshfs':
if (!data.host) return false;
if (!data.remoteDir) return false;
if (!data.post) return false;
if (!data.user) return false;
if (!data.privateKey) return false;
break;
case 'cifs':
if (!data.host) return false;
if (!data.remoteDir) return false;
if (!data.username) return false;
if (!data.password) return false;
break;
default:
return false;
}
return true;
});
async function refresh() {
busy.value = true;
const [error, result] = await volumesModel.list();
if (error) return console.error(error);
result.forEach(v => { v.busy = true; });
volumes.value = result;
busy.value = false;
for (const v of volumes.value) {
const status = await volumesModel.getStatus(v.id);
v.state = status.state === 'inactive' ? 'danger' : (status.state === 'active' ? 'success' : '');
v.message = status.message;
v.busy = false;
}
};
const volumeDialog = useTemplateRef('volumeDialog');
const inputDialog = useTemplateRef('inputDialog');
async function openVolumeDialog(volume) {
volumeDialogData.value.error = null;
volumeDialogData.value.mode = volume ? 'edit' : 'new';
volumeDialogData.value.id = volume ? volume.id : '';
volumeDialogData.value.name = volume ? volume.name : '';
volumeDialogData.value.mountType = volume ? volume.mountType : '';
volumeDialogData.value.host = volume ? volume.mountOptions.host : '';
volumeDialogData.value.seal = volume ? volume.mountOptions.seal : false;
volumeDialogData.value.port = volume ? volume.mountOptions.port : 0;
volumeDialogData.value.remoteDir = volume ? volume.mountOptions.remoteDir : '';
volumeDialogData.value.username = volume ? volume.mountOptions.username : '';
volumeDialogData.value.password = volume ? volume.mountOptions.password : '';
volumeDialogData.value.diskPath = volume ? volume.mountOptions.diskPath : '';
volumeDialogData.value.hostPath = volume ? volume.mountOptions.hostPath : '';
let blockDevices = await volumesModel.getBlockDevices();
// only offer unmounted disks
blockDevices = blockDevices.filter(d => !d.mountpoint);
// amend label for UI
blockDevices.forEach(d => d.label = d.path);
volumeDialogData.value.ext4BlockDevices = blockDevices.filter(d => d.type === 'ext4');
volumeDialogData.value.xfsBlockDevices = blockDevices.filter(d => d.type === 'xfs');
volumeDialog.value.open();
}
async function submitVolumeDialog() {
volumeDialogData.value.busy = true;
const mountOptions = {
host: volumeDialogData.value.host,
seal: volumeDialogData.value.seal,
port: volumeDialogData.value.port,
remoteDir: volumeDialogData.value.remoteDir,
username: volumeDialogData.value.username,
password: volumeDialogData.value.password,
diskPath: volumeDialogData.value.diskPath,
hostPath: volumeDialogData.value.hostPath,
};
try {
if (volumeDialogData.value.mode === 'new') {
await volumesModel.add(volumeDialogData.value.name, volumeDialogData.value.mountType, mountOptions);
} else {
await volumesModel.update(volumeDialogData.value.id, mountOptions);
}
} catch (error) {
volumeDialogData.value.error = error.body ? error.body.message : 'Internal error';
volumeDialogData.value.busy = false;
console.error(error);
return;
}
await refresh();
volumeDialog.value.close();
volumeDialogData.value.busy = false;
}
async function onRemove(volume) {
const yes = await inputDialog.value.confirm({
message: `Really remove volume ${volume.name}?`,
confirmStyle: 'danger',
confirmLabel: t('volumes.removeVolumeDialog.removeAction'),
rejectLabel: t('main.dialog.cancel')
});
if (!yes) return;
await volumesModel.remove(volume.id);
await refresh();
}
async function remount(volume) {
await volumesModel.remount(volume.id);
const status = await volumesModel.getStatus(volume.id);
volume.state = status.state;
volume.message = status.message;
window.pankow.notify('Remount attempt finished');
}
onMounted(async () =>{
await refresh();
});
</script>
<template>
<div class="content">
<InputDialog ref="inputDialog" />
<Dialog ref="volumeDialog"
:title="volumeDialogData.mode === 'edit' ? $t('volumes.editVolumeDialog.title', { name: volumeDialogData.name }) : $t('volumes.addVolumeDialog.title')"
:reject-label="$t('main.dialog.cancel')"
reject-style="secondary"
:confirm-label="$t('main.dialog.save')"
:confirm-active="volumeDialogValid"
:confirm-busy="volumeDialogData.busy"
@confirm="submitVolumeDialog()"
>
<form @submit.prevent="submitVolumeDialog()" autocomplete="off">
<fieldset :disabled="volumeDialogData.busy">
<input style="display: none;" type="submit" :disabled="!volumeDialogValid" />
<p class="has-error" v-show="volumeDialogData.error">{{ volumeDialogData.error }}</p>
<FormGroup v-if="volumeDialogData.mode === 'new'">
<label for="volumeName">{{ $t('volumes.name') }}</label>
<TextInput id="volumeName" v-model="volumeDialogData.name" />
</FormGroup>
<FormGroup>
<label for="volumeMountType">{{ $t('volumes.mountType') }}</label>
<SingleSelect id="volumeMountType" v-model="volumeDialogData.mountType" :options="mountTypeOptions" option-label="name" option-key="value" :disabled="volumeDialogData.mode === 'edit'"/>
</FormGroup>
<FormGroup v-if="volumeDialogData.mountType === 'filesystem' || volumeDialogData.mountType === 'mountpoint'">
<label for="volumeHostPath">{{ $t('volumes.localDirectory') }}</label>
<TextInput id="volumeHostPath" v-model="volumeDialogData.hostPath" :placeholder="volumeDialogData.mountType === 'filesystem' ? '/srv/shared' : '/mnt/data'" />
</FormGroup>
<FormGroup v-if="volumeDialogData.mountType === 'ext4' || volumeDialogData.mountType === 'xfs'">
<label for="volumeDiskPath">{{ $t('volumes.addVolumeDialog.diskPath') }}</label>
<SingleSelect id="volumeMountType" v-if="volumeDialogData.mountType === 'ext4'" v-model="volumeDialogData.diskPath" :options="volumeDialogData.ext4BlockDevices" option-label="label" option-key="path" :disabled="volumeDialogData.mode === 'edit'"/>
<SingleSelect id="volumeMountType" v-if="volumeDialogData.mountType === 'xfs'" v-model="volumeDialogData.diskPath" :options="volumeDialogData.xfsBlockDevices" option-label="label" option-key="path" :disabled="volumeDialogData.mode === 'edit'"/>
</FormGroup>
<FormGroup v-if="volumeDialogData.mountType === 'cifs' || volumeDialogData.mountType === 'nfs' || volumeDialogData.mountType === 'sshfs'">
<label for="volumeHost">{{ $t('volumes.addVolumeDialog.server') }}</label>
<TextInput v-model="volumeDialogData.host" id="volumeHost"/>
</FormGroup>
<Checkbox v-if="volumeDialogData.mountType === 'cifs'" v-model="volumeDialogData.seal" :label="$t('backups.configureBackupStorage.cifsSealSupport')" />
<FormGroup v-if="volumeDialogData.mountType === 'sshfs'">
<label for="volumePort">{{ $t('volumes.addVolumeDialog.port') }}</label>
<NumberInput v-model="volumeDialogData.port" id="volumePort" min="0"/>
</FormGroup>
<FormGroup v-if="volumeDialogData.mountType === 'cifs' || volumeDialogData.mountType === 'nfs' || volumeDialogData.mountType === 'sshfs'">
<label for="volumeRemoteDir">{{ $t('volumes.addVolumeDialog.remoteDirectory') }}</label>
<TextInput v-model="volumeDialogData.remoteDir" id="volumeRemoteDir" placeholder="/share" />
</FormGroup>
<FormGroup v-if="volumeDialogData.mountType === 'cifs'">
<label for="volumeUsername">{{ $t('volumes.addVolumeDialog.username') }}</label>
<TextInput v-model="volumeDialogData.username" id="volumeUsername" />
</FormGroup>
<FormGroup v-if="volumeDialogData.mountType === 'cifs'">
<label for="volumePassword">{{ $t('volumes.addVolumeDialog.password') }}</label>
<PasswordInput v-model="volumeDialogData.password" id="volumePassword" />
</FormGroup>
<FormGroup v-if="volumeDialogData.mountType === 'sshfs'">
<label for="volumeUser">{{ $t('volumes.addVolumeDialog.user') }}</label>
<TextInput v-model="volumeDialogData.user" id="volumeAddUser" />
</FormGroup>
<FormGroup v-if="volumeDialogData.mountType === 'sshfs'">
<label for="volumePrivateKey">{{ $t('volumes.addVolumeDialog.privateKey') }}</label>
<textarea v-model="volumeDialogData.privateKey" id="volumePrivateKey"></textarea>
</FormGroup>
</fieldset>
</form>
</Dialog>
<Section :title="$t('volumes.title')">
<template #header-buttons>
<Button @click="openVolumeDialog()" icon="fa fa-plus">{{ $t('volumes.addVolumeAction') }}</Button>
</template>
<div v-html="$t('volumes.description')"></div>
<br/>
<TableView :columns="columns" :model="volumes" :busy="busy">
<template #target="volume">
{{ (volume.mountType === 'mountpoint' || volume.mountType === 'filesystem') ? volume.hostPath : (volume.mountOptions.host || volume.mountOptions.diskPath || volume.hostPath) + (volume.mountOptions.remoteDir || '') }}
</template>
<template #status="volume">
<div style="text-align: center;" :title="volume.message">
<StateLED :busy="volume.busy" :state="volume.state"/>
</div>
</template>
<template #actions="volume">
<div class="table-actions">
<ButtonGroup>
<Button tool secondary small icon="fa fa-sync-alt" v-if="volume.mountType === 'sshfs' || volume.mountType === 'cifs' || volume.mountType === 'nfs' || volume.mountType === 'ext4' || volume.mountType === 'xfs'" v-tooltip="$t('volumes.remountActionTooltip')" @click="remount(volume)"></Button>
<Button tool secondary small icon="fa fa-pencil-alt" v-if="volume.mountType === 'sshfs' || volume.mountType === 'cifs' || volume.mountType === 'nfs'" v-tooltip="$t('volumes.editActionTooltip')" @click="openVolumeDialog(volume)"></Button>
<Button tool secondary small icon="fas fa-folder" v-tooltip="$t('volumes.openFileManagerActionTooltip')" :href="'/filemanager.html#/home/volume/' + volume.id" target="_blank"></Button>
</ButtonGroup>
<Button tool danger small icon="fa-solid fa-trash-alt" v-tooltip="$t('volumes.removeVolumeActionTooltip')" @click="onRemove(volume)"></Button>
</div>
</template>
</TableView>
</Section>
</div>
</template>