232 lines
8.3 KiB
Vue
232 lines
8.3 KiB
Vue
<script setup>
|
|
|
|
import { useI18n } from 'vue-i18n';
|
|
const i18n = useI18n();
|
|
const t = i18n.t;
|
|
|
|
import { ref, onMounted, computed } from 'vue';
|
|
import { Button, FormGroup, TextInput, SingleSelect } from '@cloudron/pankow';
|
|
import { ISTATES } from '../../constants.js';
|
|
import AppsModel from '../../models/AppsModel.js';
|
|
import VolumesModel from '../../models/VolumesModel.js';
|
|
|
|
const props = defineProps([ 'app' ]);
|
|
|
|
const appsModel = AppsModel.create();
|
|
const volumesModel = VolumesModel.create();
|
|
|
|
const DEFAULT_VOLUME_ID = '__default__';
|
|
|
|
const appDataVolumes = ref([]);
|
|
const volumes = ref([]);
|
|
const moveBusy = ref(false);
|
|
const moveError = ref('');
|
|
const volumeId = ref('');
|
|
const volumePrefix = ref('');
|
|
const originalVolumeId = ref('');
|
|
const selectedMountType = computed(() => {
|
|
const v = appDataVolumes.value.find(v => v.id === volumeId.value);
|
|
return v ? v.mountType : '';
|
|
});
|
|
|
|
const mounts = ref([]);
|
|
const originalMounts = ref([]);
|
|
const mountsBusy = ref(false);
|
|
const mountsError = ref('');
|
|
|
|
const mountPermissions = [
|
|
{ name: t('app.storage.mounts.permissions.readOnly'), value: 'true' },
|
|
{ name: t('app.storage.mounts.permissions.readWrite'), value: 'false' },
|
|
];
|
|
|
|
async function onSubmitMove() {
|
|
moveBusy.value = true;
|
|
moveError.value = '';
|
|
|
|
const data = {
|
|
storageVolumeId: volumeId.value !== DEFAULT_VOLUME_ID ? volumeId.value : null,
|
|
storageVolumePrefix: volumeId.value !== DEFAULT_VOLUME_ID ? volumePrefix.value : null ,
|
|
};
|
|
|
|
const [error] = await appsModel.configure(props.app.id, 'storage', data);
|
|
if (error) {
|
|
moveError.value = error.body ? error.body.message : 'Internal error';
|
|
moveBusy.value = false;
|
|
if (error) return window.cloudron.onError(error);
|
|
}
|
|
|
|
originalVolumeId.value = volumeId.value;
|
|
|
|
// give app refresh some time, ideally we wait for the task
|
|
setTimeout(() => moveBusy.value = false, 4000);
|
|
}
|
|
|
|
function onMountAdd() {
|
|
mounts.value.push({
|
|
volume: volumes.value[0],
|
|
readOnly: 'true'
|
|
});
|
|
}
|
|
|
|
function onMountRemove(index) {
|
|
mounts.value.splice(index, 1);
|
|
}
|
|
|
|
async function onSubmitMounts() {
|
|
mountsBusy.value = true;
|
|
mountsError.value = '';
|
|
|
|
const data = mounts.value.map((mount) => {
|
|
return {
|
|
volumeId: mount.volumeId,
|
|
readOnly: mount.readOnly === 'true'
|
|
};
|
|
});
|
|
|
|
const [error] = await appsModel.configure(props.app.id, 'mounts', { mounts: data });
|
|
if (error) {
|
|
mountsError.value = error.body ? error.body.message : 'Internal error';
|
|
mountsBusy.value = false;
|
|
if (error) return window.cloudron.onError(error);
|
|
}
|
|
|
|
// make a copy, cannot clone due to Proxy objects
|
|
originalMounts.value = mounts.value.map(m => { return { volumeId: m.volumeId, readOnly: m.readOnly }; });
|
|
|
|
setTimeout(() => mountsBusy.value = false, 2000);
|
|
}
|
|
|
|
const mountsValid = computed(() => {
|
|
const uniques = {};
|
|
for (const m of mounts.value) {
|
|
if (!m.volumeId) return false;
|
|
|
|
if (uniques[m.volumeId]) return false;
|
|
else uniques[m.volumeId] = true;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
const mountsChanged = computed(() => {
|
|
if (mounts.value.length !== originalMounts.value.length) return true;
|
|
|
|
for (const m in mounts.value) {
|
|
if (originalMounts.value[m].readOnly !== mounts.value[m].readOnly) return true;
|
|
if (originalMounts.value[m].volumeId !== mounts.value[m].volumeId) return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
onMounted(async () => {
|
|
const [error, result] = await volumesModel.list();
|
|
if (error) return window.cloudron.onError(error);
|
|
|
|
props.app.mounts.forEach(mount => { // { volumeId, readOnly }
|
|
const volume = result.find(v => { return v.id === mount.volumeId; });
|
|
mounts.value.push({ volumeId: volume.id, readOnly: mount.readOnly ? 'true' : 'false' });
|
|
});
|
|
|
|
// make a copy, cannot clone due to Proxy objects
|
|
originalMounts.value = mounts.value.map(m => { return { volumeId: m.volumeId, readOnly: m.readOnly }; });
|
|
|
|
appDataVolumes.value = [{
|
|
id: DEFAULT_VOLUME_ID,
|
|
label: 'Default - /home/yellowtent/appsdata/<appId>',
|
|
mountType: '',
|
|
}].concat(result.map(v => {
|
|
return {
|
|
id: v.id,
|
|
label: 'Volume - ' + v.name,
|
|
mountType: v.mountType,
|
|
};
|
|
}));
|
|
|
|
volumes.value = result.map(v => {
|
|
return {
|
|
id: v.id,
|
|
label: 'Volume - ' + v.name,
|
|
mountType: v.mountType,
|
|
};
|
|
});
|
|
|
|
volumeId.value = props.app.storageVolumeId || DEFAULT_VOLUME_ID;
|
|
volumePrefix.value = props.app.storageVolumePrefix || '';
|
|
originalVolumeId.value = volumeId.value;
|
|
});
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<FormGroup>
|
|
<label>{{ $t('app.storage.appdata.title') }} <sup><a href="https://docs.cloudron.io/apps/#data-directory" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
|
<div description v-html="$t('app.storage.appdata.description', { storagePath: ('/home/yellowtent/appsdata/' + app.id) })"></div>
|
|
|
|
<form @submit.prevent="onSubmitMove()" autocomplete="off">
|
|
<fieldset :disabled="moveBusy || (app.error && app.error.installationState !== ISTATES.PENDING_DATA_DIR_MIGRATION) || !!app.taskId">
|
|
<input type="submit" style="display: none"/>
|
|
|
|
<FormGroup>
|
|
<label>{{ $t('app.storage.mounts.volume') }}</label>
|
|
<SingleSelect v-model="volumeId" :options="appDataVolumes" option-key="id" option-label="label"/>
|
|
<div class="warning-label" v-if="volumeId !== DEFAULT_VOLUME_ID && selectedMountType === 'mountpoint'" v-html="$t('app.storage.appdata.mountTypeWarning')"></div>
|
|
</FormGroup>
|
|
|
|
<FormGroup v-if="volumeId !== DEFAULT_VOLUME_ID">
|
|
<label for="volumePrefixInput">Subdirectory</label>
|
|
<TextInput id="volumePrefixInput" placeholder="Prefix within the Volume" v-model="volumePrefix" />
|
|
</FormGroup>
|
|
</fieldset>
|
|
</form>
|
|
</FormGroup>
|
|
|
|
<div v-if="moveError" class="error-label">{{ moveError }}</div>
|
|
<br/>
|
|
|
|
<Button @click="onSubmitMove()" :loading="moveBusy" :disabled="moveBusy || (!app.error && originalVolumeId === volumeId) || (app.error && app.error.installationState !== ISTATES.PENDING_DATA_DIR_MIGRATION) || !!app.taskId">{{ $t('app.storage.appdata.moveAction') }}</Button>
|
|
|
|
<hr style="margin-top: 20px;">
|
|
|
|
<FormGroup>
|
|
<label>{{ $t('app.storage.mounts.title') }} <sup><a href="https://docs.cloudron.io/apps/#mounts" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
|
<div description v-html="$t('storage.mounts.description')"></div>
|
|
<div class="error-label" v-if="mountsError">{{ mountsError }}</div>
|
|
|
|
<table class="table table-hover" style="margin-top: 10px;" v-if="mounts.length">
|
|
<thead>
|
|
<tr>
|
|
<th style="text-align: left;">{{ $t('app.storage.mounts.volume') }}</th>
|
|
<th style="text-align: left;">{{ $t('app.storage.mounts.permissions.label') }}</th>
|
|
<th style="width: 100px; text-align: right;">{{ $t('main.actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(mount, index) in mounts" :key="mount">
|
|
<td>
|
|
<SingleSelect v-model="mount.volumeId" :options="volumes" option-key="id" option-label="label"/>
|
|
</td>
|
|
<td style="vertical-align: middle;">
|
|
<SingleSelect v-model="mount.readOnly" :options="mountPermissions" option-key="value" option-label="name" />
|
|
</td>
|
|
<td style="vertical-align: middle; text-align: right;">
|
|
<Button tool secondary v-show="mount.volumeId" :href="`/filemanager.html#/home/volume/${mount.volumeId}`" target="_blank" v-tooltip="$t('volumes.openFileManagerActionTooltip')" icon="fa-solid fa-folder"/>
|
|
<Button danger tool @click="onMountRemove(index)" icon="fa-solid fa-trash" style="margin-left: 6px"/>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div style="margin-top: 10px;">
|
|
<span v-if="mounts.length === 0">{{ $t('app.storage.mounts.noMounts') }}. </span>
|
|
<span class="actionable" @click="onMountAdd()">{{ $t('app.storage.mounts.addMountAction') }}</span>
|
|
</div>
|
|
</FormGroup>
|
|
|
|
<br/>
|
|
<Button @click="onSubmitMounts()" :loading="mountsBusy" :disabled="mountsBusy || (app.error && app.error.installationState !== ISTATES.PENDING_RECREATE_CONTAINER) || !!app.taskId || (!app.error && !mountsChanged) || !mountsValid">{{ $t('app.storage.mounts.saveAction') }}</Button>
|
|
|
|
</div>
|
|
</template>
|