Files
cloudron-box/filemanager/src/views/Home.vue
T

394 lines
12 KiB
Vue
Raw Normal View History

<template>
2023-02-24 13:39:31 +01:00
<MainLayout>
2023-04-16 13:01:25 +02:00
<template #dialogs>
<!-- have to use v-model instead of : bind - https://github.com/primefaces/primevue/issues/815 -->
<Dialog v-model:visible="newFileDialog.visible" modal :style="{ width: '50vw' }" @show="onDialogShow('newFileDialogNameInput')">
<template #header>
<label class="dialog-header" for="newFileDialogNameInput">New file name</label>
</template>
<template #default>
<form @submit="onNewFileDialogSubmit" @submit.prevent>
<InputText class="dialog-single-input" id="newFileDialogNameInput" v-model="newFileDialog.name" :disabled="newFileDialog.busy" required/>
<Button class="dialog-single-input-submit" type="submit" label="Create" :loading="newFileDialog.busy" :disabled="newFileDialog.busy || !newFileDialog.name"/>
</form>
</template>
</Dialog>
<Dialog v-model:visible="newFolderDialog.visible" modal :style="{ width: '50vw' }" @show="onDialogShow('newFolderDialogNameInput')">
<template #header>
<label class="dialog-header" for="newFolderDialogNameInput">New folder name</label>
</template>
<template #default>
<form @submit="onNewFolderDialogSubmit" @submit.prevent>
<InputText class="dialog-single-input" id="newFolderDialogNameInput" v-model="newFolderDialog.name" :disabled="newFolderDialog.busy" required/>
<Button class="dialog-single-input-submit" type="submit" label="Create" :loading="newFolderDialog.busy" :disabled="newFolderDialog.busy || !newFolderDialog.name"/>
</form>
</template>
</Dialog>
</template>
2023-02-24 13:39:31 +01:00
<template #header>
2023-02-25 03:18:07 +01:00
<TopBar class="navbar">
2023-02-24 13:39:31 +01:00
<template #left>
2023-03-29 10:17:36 +02:00
<Button icon="pi pi-chevron-left" @click="onGoUp()" text :disabled="cwd === '/'"/>
<span style="margin-left: 20px;">{{ cwd }}</span>
2023-02-24 13:39:31 +01:00
</template>
<template #right>
2023-03-28 23:32:44 +02:00
<Button type="button" label="New" icon="pi pi-plus" @click="onCreateMenu" aria-haspopup="true" aria-controls="create_menu" style="margin-right: 10px" />
<Menu ref="createMenu" id="create_menu" :model="createMenuModel" :popup="true" />
<Button type="button" label="Upload" icon="pi pi-upload" @click="onUploadMenu" aria-haspopup="true" aria-controls="upload_menu" style="margin-right: 10px" />
<Menu ref="uploadMenu" id="upload_menu" :model="uploadMenuModel" :popup="true" />
2023-04-16 13:43:48 +02:00
<Dropdown v-model="activeResource" filter :options="resourcesDropdownModel" optionLabel="label" optionGroupLabel="label" optionGroupChildren="items" dataKey="id" @change="onAppChange" placeholder="Select an App or Volume" style="margin-right: 10px" />
2023-02-24 13:39:31 +01:00
</template>
2023-02-25 03:18:07 +01:00
</TopBar>
2023-02-24 13:39:31 +01:00
</template>
<template #body>
<div class="main-view">
2023-02-25 03:18:07 +01:00
<div class="main-view-col">
<DirectoryView
2023-04-17 17:07:25 +02:00
:show-owner="true"
:show-size="true"
:show-modified="true"
@selection-changed="onSelectionChanged"
@item-activated="onItemActivated"
:delete-handler="deleteHandler"
2023-04-01 10:41:35 +02:00
:rename-handler="renameHandler"
2023-04-02 10:41:24 +02:00
:copy-handler="copyHandler"
:cut-handler="cutHandler"
:items="items"
/>
2023-02-25 03:18:07 +01:00
</div>
<div class="main-view-col" style="max-width: 300px;">
<PreviewPanel :item="activeItem || activeDirectoryItem"/>
2023-02-25 03:18:07 +01:00
</div>
2023-02-24 13:39:31 +01:00
</div>
</template>
<template #footer>
2023-04-11 16:29:58 +02:00
<FileUploader
ref="fileUploader"
:cwd="cwd"
:upload-handler="uploadHandler"
@finished="onUploadFinished"
/>
2023-03-29 19:28:32 +02:00
<BottomBar />
2023-02-24 13:39:31 +01:00
</template>
</MainLayout>
</template>
<script>
2023-02-26 01:45:57 +01:00
import superagent from 'superagent';
import Button from 'primevue/button';
2023-04-16 13:01:25 +02:00
import Dialog from 'primevue/dialog';
2023-02-26 01:45:57 +01:00
import Dropdown from 'primevue/dropdown';
2023-04-16 13:01:25 +02:00
import InputText from 'primevue/inputtext';
2023-03-28 23:32:44 +02:00
import Menu from 'primevue/menu';
2023-03-29 09:46:07 +02:00
import { useConfirm } from 'primevue/useconfirm';
2023-04-01 10:41:35 +02:00
import { DirectoryView, TopBar, BottomBar, MainLayout, FileUploader } from 'pankow';
import { sanitize, buildFilePath } from 'pankow/utils';
2023-02-24 13:39:31 +01:00
2023-02-22 19:31:12 +01:00
import PreviewPanel from '../components/PreviewPanel.vue';
2023-02-26 01:45:57 +01:00
import { createDirectoryModel } from '../models/DirectoryModel.js';
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? 'https://' + import.meta.env.VITE_API_ORIGIN : '';
2023-04-16 19:30:18 +02:00
const BASE_URL = import.meta.env.BASE_URL || '/';
export default {
name: 'Home',
components: {
2023-02-25 03:18:07 +01:00
BottomBar,
2023-02-22 19:31:12 +01:00
Button,
2023-04-16 13:01:25 +02:00
Dialog,
2023-03-28 23:32:44 +02:00
DirectoryView,
2023-02-26 01:45:57 +01:00
Dropdown,
2023-03-28 23:32:44 +02:00
FileUploader,
2023-04-16 13:01:25 +02:00
InputText,
2023-02-24 13:39:31 +01:00
MainLayout,
2023-03-28 23:32:44 +02:00
Menu,
PreviewPanel,
TopBar
2023-02-22 19:31:12 +01:00
},
data() {
return {
2023-03-29 10:17:36 +02:00
cwd: '/',
activeItem: null,
activeDirectoryItem: {},
2023-02-26 01:45:57 +01:00
items: [],
selectedItems: [],
2023-04-02 10:41:24 +02:00
clipboard: {},
2023-04-16 18:24:00 +02:00
accessToken: localStorage.token,
apiOrigin: API_ORIGIN || '',
2023-02-26 01:45:57 +01:00
apps: [],
2023-04-11 12:14:47 +02:00
volumes: [],
resources: [],
2023-04-12 14:08:41 +02:00
resourcesDropdownModel: [],
2023-02-26 23:34:31 +01:00
selectedAppId: '',
2023-04-11 12:14:47 +02:00
activeResource: null,
2023-04-16 13:01:25 +02:00
visible: true,
newFileDialog: {
visible: false,
busy: false,
name: ''
},
newFolderDialog: {
visible: false,
busy: false,
name: ''
},
2023-02-26 23:34:31 +01:00
// contextMenuModel will have activeItem attached if any command() is called
2023-03-28 23:32:44 +02:00
createMenuModel: [{
label: 'File',
icon: 'pi pi-file',
2023-04-16 13:01:25 +02:00
command: this.onNewFile
2023-03-28 23:32:44 +02:00
}, {
label: 'Folder',
icon: 'pi pi-folder',
2023-04-16 13:01:25 +02:00
command: this.onNewFolder
2023-03-28 23:32:44 +02:00
}],
uploadMenuModel: [{
label: 'File',
icon: 'pi pi-file',
command: () => {
this.$refs.fileUploader.onUploadFile();
}
}, {
label: 'Folder',
icon: 'pi pi-folder',
command: () => {
this.$refs.fileUploader.onUploadFolder();
}
2023-02-26 01:45:57 +01:00
}]
2023-02-22 19:31:12 +01:00
};
},
2023-02-26 01:45:57 +01:00
watch: {
2023-03-29 17:44:52 +02:00
cwd(newCwd, oldCwd) {
2023-04-11 12:14:47 +02:00
if (this.activeResource) this.$router.push(`/home/${this.activeResource.type}/${this.activeResource.id}${this.cwd}`);
2023-03-29 17:44:52 +02:00
this.loadCwd();
2023-02-26 01:45:57 +01:00
}
},
methods: {
2023-03-28 23:32:44 +02:00
onCreateMenu(event) {
this.$refs.createMenu.toggle(event);
},
onUploadMenu(event) {
this.$refs.uploadMenu.toggle(event);
},
2023-04-16 13:01:25 +02:00
// generic dialog focus handler
onDialogShow(focusElementId) {
setTimeout(() => document.getElementById(focusElementId).focus(), 0);
},
onNewFile() {
this.newFileDialog.busy = false;
this.newFileDialog.name = '';
this.newFileDialog.visible = true;
},
2023-04-16 13:43:48 +02:00
async onNewFileDialogSubmit() {
this.newFileDialog.busy = true;
await this.directoryModel.newFile(buildFilePath(this.cwd, this.newFileDialog.name), this.newFileDialog.name);
await this.loadCwd();
2023-04-16 13:01:25 +02:00
this.newFileDialog.visible = false;
},
onNewFolder() {
this.newFolderDialog.busy = false;
this.newFolderDialog.name = '';
this.newFolderDialog.visible = true;
},
2023-04-16 13:43:48 +02:00
async onNewFolderDialogSubmit() {
this.newFolderDialog.busy = true;
await this.directoryModel.newFolder(buildFilePath(this.cwd, this.newFolderDialog.name));
await this.loadCwd();
2023-04-16 13:01:25 +02:00
this.newFolderDialog.visible = false;
},
2023-03-29 10:17:36 +02:00
onUploadFinished() {
2023-03-29 17:44:52 +02:00
this.loadCwd();
2023-03-29 10:17:36 +02:00
},
2023-02-26 23:34:31 +01:00
onAppChange(event) {
2023-04-11 12:14:47 +02:00
this.$router.push(`/home/${event.value.type}/${event.value.id}`);
2023-03-29 10:17:36 +02:00
this.cwd = '/';
2023-04-11 12:14:47 +02:00
this.loadResource(event.value);
2023-02-26 23:34:31 +01:00
},
2023-02-25 03:18:07 +01:00
onSelectionChanged(items) {
this.activeItem = items[0] || null;
this.selectedItems = items;
2023-02-26 01:45:57 +01:00
},
2023-02-26 02:55:19 +01:00
onGoUp() {
2023-03-29 10:17:36 +02:00
this.cwd = sanitize(this.cwd.split('/').slice(0, -1).join('/'));
2023-02-26 02:55:19 +01:00
},
2023-02-26 01:45:57 +01:00
onItemActivated(item) {
2023-03-29 21:39:12 +02:00
if (!item) return;
2023-03-29 10:17:36 +02:00
if (item.type === 'directory') this.cwd = sanitize(this.cwd + '/' + item.name);
2023-04-11 12:14:47 +02:00
else this.$router.push(`/viewer/${this.activeResource.type}/${this.activeResource.id}${sanitize(this.cwd + '/' + item.name)}`);
2023-02-26 01:45:57 +01:00
},
async deleteHandler(files) {
2023-04-02 10:06:14 +02:00
if (!files) return;
for (let i in files) {
2023-04-16 13:43:48 +02:00
await this.directoryModel.remove(buildFilePath(this.cwd, files[i].name));
2023-04-02 10:06:14 +02:00
}
await this.loadCwd();
},
2023-04-01 10:41:35 +02:00
async renameHandler(file, newName) {
2023-04-16 13:43:48 +02:00
await this.directoryModel.rename(buildFilePath(this.cwd, file.name), sanitize(this.cwd + '/' + newName));
2023-04-01 10:41:35 +02:00
await this.loadCwd();
},
2023-04-02 10:41:24 +02:00
async copyHandler(files) {
if (!files) return;
this.clipboard = {
action: 'copy',
files
};
},
async cutHandler(files) {
if (!files) return;
this.clipboard = {
action: 'cut',
files
};
},
2023-04-11 16:29:58 +02:00
async uploadHandler(targetDir, file, progressHandler) {
await this.directoryModel.upload(targetDir, file, progressHandler);
2023-04-16 13:43:48 +02:00
await this.loadCwd();
2023-04-11 16:29:58 +02:00
},
2023-03-29 17:44:52 +02:00
async loadCwd() {
2023-04-17 17:07:25 +02:00
this.items = await this.directoryModel.listFiles(this.cwd);
2023-03-29 10:17:36 +02:00
const tmp = this.cwd.split('/').slice(1);
2023-04-11 12:14:47 +02:00
let name = this.activeResource.fqdn;
if (tmp.length > 1) name = tmp[tmp.length-2];
this.activeDirectoryItem = {
id: name,
name: name,
type: 'directory',
mimeType: 'inode/directory',
2023-04-16 19:30:18 +02:00
icon: `${BASE_URL}mime-types/inode-directory.svg`
};
2023-02-26 02:55:19 +01:00
},
2023-04-11 12:14:47 +02:00
async loadResource(resource) {
this.activeResource = resource;
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, resource.type === 'volume' ? `volumes/${resource.id}` : `apps/${resource.id}`);
2023-03-29 17:44:52 +02:00
this.loadCwd();
2023-02-26 01:45:57 +01:00
}
},
async mounted() {
2023-03-29 09:46:07 +02:00
useConfirm();
2023-02-26 01:45:57 +01:00
// load all apps
2023-04-16 19:06:14 +02:00
let error, result;
try {
result = await superagent.get(`${this.apiOrigin}/api/v1/apps`).query({ access_token: this.accessToken });
} catch (e) {
error = e;
}
if (error || result.statusCode !== 200) {
console.error('Failed to list apps', error || result.statusCode);
2023-02-26 01:45:57 +01:00
this.apps = [];
} else {
this.apps = result.body ? result.body.apps.filter(a => !!a.manifest.addons.localstorage) : [];
}
2023-04-12 14:08:41 +02:00
this.apps.forEach(function (a) { a.type = 'app'; a.label = a.fqdn; });
2023-02-26 02:55:19 +01:00
2023-04-11 12:14:47 +02:00
// load all volumes
2023-04-16 19:06:14 +02:00
try {
result = await superagent.get(`${this.apiOrigin}/api/v1/volumes`).query({ access_token: this.accessToken });
} catch (e) {
error = e;
}
if (error || result.statusCode !== 200) {
console.error('Failed to list volumes', error || result.statusCode);
2023-04-11 12:14:47 +02:00
this.volumes = [];
} else {
this.volumes = result.body ? result.body.volumes : [];
}
2023-04-12 14:08:41 +02:00
this.volumes.forEach(function (a) { a.type = 'volume'; a.label = a.name; });
2023-02-26 23:34:31 +01:00
2023-04-11 12:14:47 +02:00
this.resources = this.apps.concat(this.volumes);
2023-02-26 23:34:31 +01:00
2023-04-12 14:08:41 +02:00
this.resourcesDropdownModel = [{
label: 'Apps',
items: this.apps
}, {
label: 'Volumes',
items: this.volumes
}];
2023-04-11 12:14:47 +02:00
const type = this.$route.params.type || 'app';
const resourceId = this.$route.params.resourceId;
if (type === 'volume') {
this.activeResource = this.volumes.find(a => a.id === resourceId);
if (!this.activeResource) this.activeResource = this.volumes[0];
if (!this.activeResource) return console.error('Unable to find volumes', resourceId);
} else if (type === 'app') {
this.activeResource = this.apps.find(a => a.id === resourceId);
if (!this.activeResource) this.activeResource = this.apps[0];
if (!this.activeResource) return console.error('Unable to find app', resourceId);
} else {
this.activeResource = this.apps[0];
}
2023-02-26 23:34:31 +01:00
2023-04-16 19:06:14 +02:00
if (!this.activeResource) {
console.error('Not able to load apps or volumes. Cannot continue');
return;
}
2023-03-29 10:17:36 +02:00
this.cwd = sanitize('/' + (this.$route.params.cwd ? this.$route.params.cwd.join('/') : '/'));
2023-02-26 23:34:31 +01:00
2023-04-11 12:14:47 +02:00
this.loadResource(this.activeResource);
2023-02-26 15:00:16 +01:00
this.$watch(() => this.$route.params, (toParams, previousParams) => {
2023-04-11 12:14:47 +02:00
if (toParams.type === 'volume') {
this.activeResource = this.volumes.find(a => a.id === toParams.resourceId);
} else if (toParams.type === 'app') {
this.activeResource = this.apps.find(a => a.id === toParams.resourceId);
} else {
console.error(`Unknown type ${toParams.type}`);
}
2023-03-29 10:17:36 +02:00
this.cwd = toParams.cwd ? `/${toParams.cwd.join('/')}` : '/';
2023-02-26 15:00:16 +01:00
});
}
};
</script>
<style scoped>
.main-view {
2023-02-23 13:16:21 +01:00
flex-grow: 1;
2023-02-25 03:18:07 +01:00
overflow: hidden;
height: 100%;
2023-02-24 13:39:31 +01:00
display: flex;
2023-02-25 03:18:07 +01:00
padding: 0 10px
}
.main-view-col {
overflow: auto;
flex-grow: 1;
2023-04-16 13:01:25 +02:00
}
.dialog-header {
font-weight: 600;
font-size: 1.25rem;
}
.dialog-single-input {
display: block;
width: 100%;
margin-top: 5px;
margin-bottom: 1.5rem;
}
2023-02-25 03:18:07 +01:00
2023-04-16 13:01:25 +02:00
.dialog-single-input-submit {
margin-top: 5px;
}
</style>