Merge new filemanager
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
<template>
|
||||
<MainLayout>
|
||||
<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>
|
||||
<template #header>
|
||||
<TopBar class="navbar">
|
||||
<template #left>
|
||||
<Button icon="pi pi-chevron-left" @click="onGoUp()" text :disabled="cwd === '/'"/>
|
||||
<span style="margin-left: 20px;">{{ cwd }}</span>
|
||||
</template>
|
||||
<template #right>
|
||||
<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" />
|
||||
<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" />
|
||||
<Button label="Logout" @click="onLogout" severity="secondary"/>
|
||||
</template>
|
||||
</TopBar>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="main-view">
|
||||
<div class="main-view-col">
|
||||
<DirectoryView
|
||||
@selection-changed="onSelectionChanged"
|
||||
@item-activated="onItemActivated"
|
||||
:delete-handler="deleteHandler"
|
||||
:rename-handler="renameHandler"
|
||||
:copy-handler="copyHandler"
|
||||
:cut-handler="cutHandler"
|
||||
:items="items"
|
||||
/>
|
||||
</div>
|
||||
<div class="main-view-col" style="max-width: 300px;">
|
||||
<PreviewPanel :item="activeItem || activeDirectoryItem"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<FileUploader
|
||||
ref="fileUploader"
|
||||
:cwd="cwd"
|
||||
:upload-handler="uploadHandler"
|
||||
@finished="onUploadFinished"
|
||||
/>
|
||||
<BottomBar />
|
||||
</template>
|
||||
</MainLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import safe from 'safetydance';
|
||||
import superagent from 'superagent';
|
||||
|
||||
import Button from 'primevue/button';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Menu from 'primevue/menu';
|
||||
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
|
||||
import { DirectoryView, TopBar, BottomBar, MainLayout, FileUploader } from 'pankow';
|
||||
import { sanitize, buildFilePath } from 'pankow/utils';
|
||||
|
||||
import PreviewPanel from '../components/PreviewPanel.vue';
|
||||
import { createDirectoryModel } from '../models/DirectoryModel.js';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_ORIGIN ? 'https://' + import.meta.env.VITE_API_ORIGIN : '';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
BottomBar,
|
||||
Button,
|
||||
Dialog,
|
||||
DirectoryView,
|
||||
Dropdown,
|
||||
FileUploader,
|
||||
InputText,
|
||||
MainLayout,
|
||||
Menu,
|
||||
PreviewPanel,
|
||||
TopBar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
cwd: '/',
|
||||
activeItem: null,
|
||||
activeDirectoryItem: {},
|
||||
items: [],
|
||||
selectedItems: [],
|
||||
clipboard: {},
|
||||
accessToken: localStorage.accessToken,
|
||||
baseUrl: BASE_URL || '',
|
||||
apps: [],
|
||||
volumes: [],
|
||||
resources: [],
|
||||
resourcesDropdownModel: [],
|
||||
selectedAppId: '',
|
||||
activeResource: null,
|
||||
visible: true,
|
||||
newFileDialog: {
|
||||
visible: false,
|
||||
busy: false,
|
||||
name: ''
|
||||
},
|
||||
newFolderDialog: {
|
||||
visible: false,
|
||||
busy: false,
|
||||
name: ''
|
||||
},
|
||||
// contextMenuModel will have activeItem attached if any command() is called
|
||||
createMenuModel: [{
|
||||
label: 'File',
|
||||
icon: 'pi pi-file',
|
||||
command: this.onNewFile
|
||||
}, {
|
||||
label: 'Folder',
|
||||
icon: 'pi pi-folder',
|
||||
command: this.onNewFolder
|
||||
}],
|
||||
uploadMenuModel: [{
|
||||
label: 'File',
|
||||
icon: 'pi pi-file',
|
||||
command: () => {
|
||||
this.$refs.fileUploader.onUploadFile();
|
||||
}
|
||||
}, {
|
||||
label: 'Folder',
|
||||
icon: 'pi pi-folder',
|
||||
command: () => {
|
||||
this.$refs.fileUploader.onUploadFolder();
|
||||
}
|
||||
}]
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
cwd(newCwd, oldCwd) {
|
||||
if (this.activeResource) this.$router.push(`/home/${this.activeResource.type}/${this.activeResource.id}${this.cwd}`);
|
||||
this.loadCwd();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onCreateMenu(event) {
|
||||
this.$refs.createMenu.toggle(event);
|
||||
},
|
||||
onUploadMenu(event) {
|
||||
this.$refs.uploadMenu.toggle(event);
|
||||
},
|
||||
// generic dialog focus handler
|
||||
onDialogShow(focusElementId) {
|
||||
setTimeout(() => document.getElementById(focusElementId).focus(), 0);
|
||||
},
|
||||
onNewFile() {
|
||||
this.newFileDialog.busy = false;
|
||||
this.newFileDialog.name = '';
|
||||
this.newFileDialog.visible = true;
|
||||
},
|
||||
async onNewFileDialogSubmit() {
|
||||
this.newFileDialog.busy = true;
|
||||
await this.directoryModel.newFile(buildFilePath(this.cwd, this.newFileDialog.name), this.newFileDialog.name);
|
||||
await this.loadCwd();
|
||||
this.newFileDialog.visible = false;
|
||||
},
|
||||
onNewFolder() {
|
||||
this.newFolderDialog.busy = false;
|
||||
this.newFolderDialog.name = '';
|
||||
this.newFolderDialog.visible = true;
|
||||
},
|
||||
async onNewFolderDialogSubmit() {
|
||||
this.newFolderDialog.busy = true;
|
||||
await this.directoryModel.newFolder(buildFilePath(this.cwd, this.newFolderDialog.name));
|
||||
await this.loadCwd();
|
||||
this.newFolderDialog.visible = false;
|
||||
},
|
||||
onUploadFinished() {
|
||||
this.loadCwd();
|
||||
},
|
||||
onAppChange(event) {
|
||||
this.$router.push(`/home/${event.value.type}/${event.value.id}`);
|
||||
this.cwd = '/';
|
||||
this.loadResource(event.value);
|
||||
},
|
||||
onLogout() {
|
||||
delete localStorage.accessToken;
|
||||
this.$router.push('/login');
|
||||
},
|
||||
onSelectionChanged(items) {
|
||||
this.activeItem = items[0] || null;
|
||||
this.selectedItems = items;
|
||||
},
|
||||
onGoUp() {
|
||||
this.cwd = sanitize(this.cwd.split('/').slice(0, -1).join('/'));
|
||||
},
|
||||
onItemActivated(item) {
|
||||
if (!item) return;
|
||||
|
||||
if (item.type === 'directory') this.cwd = sanitize(this.cwd + '/' + item.name);
|
||||
else this.$router.push(`/viewer/${this.activeResource.type}/${this.activeResource.id}${sanitize(this.cwd + '/' + item.name)}`);
|
||||
},
|
||||
async deleteHandler(files) {
|
||||
if (!files) return;
|
||||
|
||||
for (let i in files) {
|
||||
await this.directoryModel.remove(buildFilePath(this.cwd, files[i].name));
|
||||
}
|
||||
|
||||
await this.loadCwd();
|
||||
},
|
||||
async renameHandler(file, newName) {
|
||||
await this.directoryModel.rename(buildFilePath(this.cwd, file.name), sanitize(this.cwd + '/' + newName));
|
||||
await this.loadCwd();
|
||||
},
|
||||
async copyHandler(files) {
|
||||
if (!files) return;
|
||||
|
||||
this.clipboard = {
|
||||
action: 'copy',
|
||||
files
|
||||
};
|
||||
},
|
||||
async cutHandler(files) {
|
||||
if (!files) return;
|
||||
|
||||
this.clipboard = {
|
||||
action: 'cut',
|
||||
files
|
||||
};
|
||||
},
|
||||
async uploadHandler(targetDir, file, progressHandler) {
|
||||
await this.directoryModel.upload(targetDir, file, progressHandler);
|
||||
await this.loadCwd();
|
||||
},
|
||||
async loadCwd() {
|
||||
const items = await this.directoryModel.listFiles(this.cwd);
|
||||
|
||||
// convert to format DirectoryView currently wants
|
||||
this.items = items.map(function (i) {
|
||||
return {
|
||||
id: i.fileName,
|
||||
name: i.fileName,
|
||||
size: i.size,
|
||||
modified: new Date(i.mtime),
|
||||
type: i.isDirectory ? 'directory' : 'file',
|
||||
mimeType: i.mimeType,
|
||||
previewUrl: i.previewUrl || null,
|
||||
icon: `/mime-types/${i.mimeType === 'inode/symlink' ? 'none' : i.mimeType.split('/').join('-')}.svg`
|
||||
};
|
||||
});
|
||||
|
||||
const tmp = this.cwd.split('/').slice(1);
|
||||
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',
|
||||
icon: '/mime-types/inode-directory.svg'
|
||||
};
|
||||
},
|
||||
async loadResource(resource) {
|
||||
this.activeResource = resource;
|
||||
this.directoryModel = createDirectoryModel(BASE_URL, localStorage.accessToken, resource.type === 'volume' ? `volumes/${resource.id}` : `apps/${resource.id}`);
|
||||
this.loadCwd();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
useConfirm();
|
||||
|
||||
// load all apps
|
||||
let [error, result] = await safe(superagent.get(`${BASE_URL}/api/v1/apps`).query({ access_token: localStorage.accessToken }));
|
||||
if (error) {
|
||||
console.error('Failed to list apps', error);
|
||||
this.apps = [];
|
||||
} else {
|
||||
this.apps = result.body ? result.body.apps.filter(a => !!a.manifest.addons.localstorage) : [];
|
||||
}
|
||||
this.apps.forEach(function (a) { a.type = 'app'; a.label = a.fqdn; });
|
||||
|
||||
// load all volumes
|
||||
[error, result] = await safe(superagent.get(`${BASE_URL}/api/v1/volumes`).query({ access_token: localStorage.accessToken }));
|
||||
if (error) {
|
||||
console.error('Failed to list volumes', error);
|
||||
this.volumes = [];
|
||||
} else {
|
||||
this.volumes = result.body ? result.body.volumes : [];
|
||||
}
|
||||
this.volumes.forEach(function (a) { a.type = 'volume'; a.label = a.name; });
|
||||
|
||||
this.resources = this.apps.concat(this.volumes);
|
||||
|
||||
this.resourcesDropdownModel = [{
|
||||
label: 'Apps',
|
||||
items: this.apps
|
||||
}, {
|
||||
label: 'Volumes',
|
||||
items: this.volumes
|
||||
}];
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
this.cwd = sanitize('/' + (this.$route.params.cwd ? this.$route.params.cwd.join('/') : '/'));
|
||||
|
||||
this.loadResource(this.activeResource);
|
||||
|
||||
this.$watch(() => this.$route.params, (toParams, previousParams) => {
|
||||
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}`);
|
||||
}
|
||||
|
||||
this.cwd = toParams.cwd ? `/${toParams.cwd.join('/')}` : '/';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.main-view {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 0 10px
|
||||
}
|
||||
|
||||
.main-view-col {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.dialog-single-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog-single-input-submit {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<LoginView :login-url="loginUrl" @error="onError" @success="onSuccess"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { LoginView } from 'pankow';
|
||||
|
||||
// can be exposed as env var, see develop.sh
|
||||
const BASE_URL = import.meta.env.VITE_API_ORIGIN ? 'https://' + import.meta.env.VITE_API_ORIGIN : '';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoginView
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
totpToken: '',
|
||||
error: '',
|
||||
busy: false,
|
||||
loginUrl: `${BASE_URL}/api/v1/cloudron/login`
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onError(error) {
|
||||
console.error('Error loggin in', error);
|
||||
},
|
||||
onSuccess(accessToken) {
|
||||
console.log('Success loggin in');
|
||||
|
||||
localStorage.accessToken = accessToken;
|
||||
this.$router.push('/home');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
h1 {
|
||||
color: #777;
|
||||
font-weight: normal;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h1 b {
|
||||
color: black;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 640px;
|
||||
margin: auto;
|
||||
margin-top: 100px;
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.field * {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.field label {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.action-bar * {
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="viewer">
|
||||
<TextEditor ref="textEditor"
|
||||
v-show="active === 'textEditor'"
|
||||
:save-handler="saveHandler"
|
||||
@close="onClose"
|
||||
/>
|
||||
<ImageViewer ref="imageViewer" v-show="active === 'imageViewer'" @close="onClose"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { TextEditor, ImageViewer } from 'pankow';
|
||||
import { createDirectoryModel } from '../models/DirectoryModel.js';
|
||||
import { sanitize } from 'pankow/utils';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_ORIGIN ? 'https://' + import.meta.env.VITE_API_ORIGIN : '';
|
||||
|
||||
export default {
|
||||
name: 'Viewer',
|
||||
components: {
|
||||
ImageViewer,
|
||||
TextEditor
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
resourceId: '',
|
||||
resourceType: '',
|
||||
item: null,
|
||||
active: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.$router.go(-1);
|
||||
},
|
||||
async saveHandler(item, content) {
|
||||
await this.directoryModel.save(this.filePath, content);
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.resourceId = this.$route.params.resourceId;
|
||||
this.resourceType = this.$route.params.type;
|
||||
|
||||
this.filePath = this.$route.params.filePath.join('/');
|
||||
const fileName = this.$route.params.filePath[this.$route.params.filePath.length-1];
|
||||
const parentDirectoryPath = sanitize(this.filePath.split('/').slice(0, -1).join('/'));
|
||||
|
||||
this.directoryModel = createDirectoryModel(BASE_URL, localStorage.accessToken, (this.resourceType === 'volume' ? 'volumes/' : 'apps/') + this.resourceId);
|
||||
const files = await this.directoryModel.listFiles(parentDirectoryPath);
|
||||
|
||||
this.item = files.find(i => i.fileName === fileName);
|
||||
|
||||
if (!this.item) {
|
||||
console.log('File not found', this.filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$refs.imageViewer.canHandle(this.item)) {
|
||||
this.$refs.imageViewer.open(this.item, this.directoryModel.getFileUrl(this.filePath));
|
||||
this.active = 'imageViewer';
|
||||
} else if (this.$refs.textEditor.canHandle(this.item)) {
|
||||
const content = await this.directoryModel.getFile(this.filePath);
|
||||
this.$refs.textEditor.open(this.item, content);
|
||||
this.active = 'textEditor';
|
||||
} else {
|
||||
console.warn(`no editor or viewer found for ${this.item.mimeType}`, this.item);
|
||||
this.active = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.viewer{
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.main-view-col {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user