Finish Volumesview.vue

This commit is contained in:
Johannes Zellner
2024-12-27 22:25:00 +01:00
parent 2167b1fc6b
commit dd264617d7
4 changed files with 304 additions and 67 deletions

View File

@@ -1,5 +1,6 @@
<template>
<div>
<Notification />
<SupportView v-if="view === VIEWS.SUPPORT" />
<VolumesView v-if="view === VIEWS.VOLUMES" />
</div>
@@ -7,6 +8,8 @@
<script>
import { Notification } from 'pankow';
import SupportView from './SupportView.vue';
import VolumesView from './VolumesView.vue';
@@ -18,6 +21,7 @@ const VIEWS = {
export default {
name: 'Index',
components: {
Notification,
SupportView,
VolumesView,
},

View File

@@ -1,24 +1,106 @@
<template>
<div class="content">
<h1 class="section-header">{{ $t('volumes.title') }}</h1>
<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')"
:confirm-label="$t('main.dialog.save')"
confirm-style="success"
:confirm-active="volumeDialogValid"
:confirm-busy="volumeDialogData.busy"
@confirm="submitVolumeDialog()"
>
<form @submit="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>
<Dropdown 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>
<select id="volumeDiskPath" v-model="volumeDialogData.diskPath" ng-options="item as item.label for item in ext4BlockDevices track by item.path"></select>
<!-- <select id="volumeXfsDisk" v-model="volumeDialogData.xfsDisk" ng-options="item as item.label for item in xfsBlockDevices track by item.path"></select> -->
</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>
<input type="number" class="form-control" v-model="volumeDialogData.port" id="volumePort">
</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>
<h1 class="section-header">{{ $t('volumes.title') }} <Button @click="openVolumeDialog()" icon="fa fa-plus">{{ $t('volumes.addVolumeAction') }}</Button></h1>
<Card>
<div v-html="$t('volumes.description')"></div>
<br/>
<br/>
<TableView :columns="tableColumns" :model="tableModel">
<TableView :columns="columns" :model="volumes">
<template #target="slotProps">
{{ (slotProps.mountType === 'mountpoint' || slotProps.mountType === 'filesystem') ? slotProps.hostPath : (slotProps.mountOptions.host || slotProps.mountOptions.diskPath || slotProps.hostPath) + (slotProps.mountOptions.remoteDir || '') }}
</template>
<template #status="slotProps">
<div style="text-align: center;" v-tooltip="slotProps.status ? slotProps.status.message : ''">
<i class="fa fa-circle" :style="{ color: slotProps.status.state === 'active' ? '#27CE65' : '#d9534f' }" v-if="slotProps.status"></i>
<i class="fa fa-circle-notch fa-spin" v-if="!slotProps.status.state"></i>
<div style="text-align: center;" v-tooltip="slotProps.message">
<i class="fa fa-circle" :style="{ color: slotProps.state === 'active' ? '#27CE65' : '#d9534f' }"></i>
</div>
</template>
<template #actions="slotProps">
<div style="text-align: right;">
<Button tool outline icon="fa fa-sync-alt" v-show="isMountProvider(slotProps.volume)" v-tooltip="$t('volumes.remountActionTooltip')"></Button>
<Button tool outline icon="fa fa-pencil-alt" v-tooltip="$t('volumes.editActionTooltip')"></Button>
<Button tool outline icon="fas fa-folder" v-tooltip="$t('volumes.openFileManagerActionTooltip')"></Button>
<Button tool danger icon="far fa-trash-alt" v-tooltip="$t('volumes.removeVolumeActionTooltip')"></Button>
<div class="actions">
<ButtonGroup>
<Button tool icon="fa fa-sync-alt" v-if="slotProps.mountType === 'sshfs' || slotProps.mountType === 'cifs' || slotProps.mountType === 'nfs' || slotProps.mountType === 'ext4' || slotProps.mountType === 'xfs'" v-tooltip="$t('volumes.remountActionTooltip')" @click="remount(slotProps)"></Button>
<Button tool icon="fa fa-pencil-alt" v-if="slotProps.mountType === 'sshfs' || slotProps.mountType === 'cifs' || slotProps.mountType === 'nfs'" v-tooltip="$t('volumes.editActionTooltip')" @click="openVolumeDialog(slotProps)"></Button>
<Button tool icon="fas fa-folder" v-tooltip="$t('volumes.openFileManagerActionTooltip')" :href="'/filemanager.html#/home/volume/' + slotProps.id" target="_blank"></Button>
</ButtonGroup>
<Button tool danger icon="far fa-trash-alt" v-tooltip="$t('volumes.removeVolumeActionTooltip')" @click="onRemove(slotProps)"></Button>
</div>
</template>
</TableView>
@@ -28,7 +110,7 @@
<script>
import { fetcher, Button, TableView } from 'pankow';
import { Button, ButtonGroup, Checkbox, Dialog, Dropdown, FormGroup, InputDialog, PasswordInput, TableView, TextInput } from 'pankow';
import Card from './Card.vue';
@@ -43,76 +125,186 @@ export default {
name: 'VolumesView',
components: {
Button,
ButtonGroup,
Card,
Checkbox,
Dialog,
Dropdown,
FormGroup,
InputDialog,
PasswordInput,
TableView,
TextInput,
},
computed: {
volumeDialogValid() {
const data = this.volumeDialogData;
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.remoteDirectory) return false;
break;
case 'sshfs':
if (!data.host) return false;
if (!data.remoteDirectory) 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.remoteDirectory) return false;
if (!data.username) return false;
if (!data.password) return false;
break;
default:
return false;
}
return true;
}
},
data() {
return {
ready: false,
tableColumns: {
status: {
label: ''
},
name: {
label: 'Name',
sort: true
},
type: {
label: 'Type',
sort: true
},
target: {
label: 'Target',
sort: true
},
actions: {
label: '',
sort: false
}
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' },
],
columns: {
status: { label: '' },
name: { label: 'Name', sort: true },
mountType: { label: 'Type', sort: true },
target: { label: 'Target', sort: true },
actions: { label: '', sort: false }
},
tableModel: [],
volumes: [],
volumeDialogData: {
error: null,
busy: false,
mode: '', // edit or new
name: '',
// dynamic extra props from openVolumeDialog
},
};
},
methods: {
isMountProvider(v) {
return v.mountType === 'sshfs' || v.mountType === 'cifs' || v.mountType === 'nfs' || v.mountType === 'ext4' || v.mountType === 'xfs';
async refresh() {
this.volumes = await volumesModel.list();
for (const v of this.volumes) {
const status = await volumesModel.getStatus(v.id);
v.state = status.state;
v.message = status.message;
}
},
async toggleSshSupport() {
this.toggleSshSupportError = '';
openVolumeDialog(volume) {
this.volumeDialogData.error = null;
this.volumeDialogData.mode = volume ? 'edit' : 'new';
this.volumeDialogData.id = volume ? volume.id : '';
this.volumeDialogData.name = volume ? volume.name : '';
this.volumeDialogData.mountType = volume ? volume.mountType : '';
this.volumeDialogData.host = volume ? volume.mountOptions.host : '';
this.volumeDialogData.seal = volume ? volume.mountOptions.seal : false;
this.volumeDialogData.port = volume ? volume.mountOptions.port : 0;
this.volumeDialogData.remoteDir = volume ? volume.mountOptions.remoteDir : '';
this.volumeDialogData.username = volume ? volume.mountOptions.username : '';
this.volumeDialogData.password = volume ? volume.mountOptions.password : '';
this.volumeDialogData.diskPath = volume ? volume.mountOptions.diskPath : '';
this.volumeDialogData.hostPath = volume ? volume.mountOptions.hostPath : '';
const res = await fetcher.post(`${API_ORIGIN}/api/v1/support/remote_support`, { enable: !this.sshSupportEnabled }, { access_token: accessToken });
if (res.status === 412 || res.status === 417) this.toggleSshSupportError = res.body;
else if (res.status !== 202) console.error(res.body);
this.$refs.volumeDialog.open();
},
async submitVolumeDialog() {
this.volumeDialogData.busy = true;
this.sshSupportEnabled = !this.sshSupportEnabled;
const mountOptions = {
host: this.volumeDialogData.host,
seal: this.volumeDialogData.seal,
port: this.volumeDialogData.port,
remoteDir: this.volumeDialogData.remoteDir,
username: this.volumeDialogData.username,
password: this.volumeDialogData.password,
diskPath: this.volumeDialogData.diskPath,
hostPath: this.volumeDialogData.hostPath,
};
try {
if (this.volumeDialogData.mode === 'new') {
await volumesModel.add(this.volumeDialogData.name, this.volumeDialogData.mountType, mountOptions);
} else {
await volumesModel.update(this.volumeDialogData.id, mountOptions);
}
} catch (error) {
this.volumeDialogData.error = error.body ? error.body.message : 'Internal error';
this.volumeDialogData.busy = false;
console.error(error);
return;
}
await this.refresh();
this.$refs.volumeDialog.close();
this.volumeDialogData.busy = false;
},
async onRemove(volume) {
const yes = await this.$refs.inputDialog.confirm({
message: `Really remove volume ${volume.name}?`,
confirmStyle: 'danger',
confirmLabel: this.$t('volumes.removeVolumeDialog.removeAction'),
rejectLabel: this.$t('main.dialog.cancel')
});
if (!yes) return;
await volumesModel.remove(volume.id);
await this.refresh();
},
async 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');
}
},
async mounted() {
this.volumes = await volumesModel.list();
this.tableModel = this.volumes.map(v => {
const m = {};
m.status = {};
m.name = v.name;
m.type = v.mountType;
if (v.mountType === 'mountpoint' || v.mountType === 'filesystem') m.target = v.hostPath;
else m.target = (v.mountOptions.host || v.mountOptions.diskPath || v.hostPath) + (v.mountOptions.remoteDir || '');
m.actions = '';
m.volume = v;
return m;
});
this.ready = true;
this.tableModel.forEach(async v => {
v.status = await volumesModel.getStatus(v.volume.id);
console.log(v.status)
});
await this.refresh();
}
};
</script>
<style scoped>
.actions {
text-align: right;
visibility: hidden;
}
tr:hover .actions {
visibility: visible;
}
</style>