File Control Panel
This commit is contained in:
@@ -48,4 +48,8 @@ export default class API {
|
||||
listTokens() {
|
||||
return this.apiCall("file/listTokens");
|
||||
}
|
||||
|
||||
delete(id) {
|
||||
return this.apiCall("file/delete", { id: id })
|
||||
}
|
||||
};
|
||||
53
fileControlPanel/src/elements/file-browser.css
Normal file
53
fileControlPanel/src/elements/file-browser.css
Normal file
@@ -0,0 +1,53 @@
|
||||
.file-row td {
|
||||
padding: 0;
|
||||
border: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.file-control-buttons {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
.file-control-buttons > button {
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.file-upload-container {
|
||||
border: dotted;
|
||||
margin: 18px;
|
||||
padding: 15px;
|
||||
min-height: 150px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-upload-container > div > div {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto auto;
|
||||
}
|
||||
|
||||
.uploaded-file {
|
||||
max-width: 120px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.uploaded-file > img {
|
||||
width: 50px;
|
||||
height: 56px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.uploaded-file > span {
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.uploaded-file > i {
|
||||
color: red;
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
right: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import * as React from "react";
|
||||
import "./file-browser.css";
|
||||
import Dropzone from "react-dropzone";
|
||||
import Icon from "./icon";
|
||||
|
||||
export class FileBrowser extends React.Component {
|
||||
|
||||
@@ -6,10 +9,41 @@ export class FileBrowser extends React.Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
api: props.api,
|
||||
files: props.files,
|
||||
token: props.token,
|
||||
filesToUpload: [],
|
||||
}
|
||||
}
|
||||
|
||||
svgMiddle(indentation, size=64) {
|
||||
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*size) + "px" } : {});
|
||||
return <svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" style={style}>
|
||||
<g>
|
||||
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2="0" x2={size/2}
|
||||
y1={size} x1={size/2} strokeWidth="1.5" stroke="#000" fill="none"/>
|
||||
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2={size/2} x2={size}
|
||||
y1={size/2} x1={size/2} fillOpacity="null" strokeOpacity="null" strokeWidth="1.5"
|
||||
stroke="#000" fill="none"/>
|
||||
</g>
|
||||
</svg>;
|
||||
}
|
||||
|
||||
svgEnd(indentation, size=64) {
|
||||
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*size) + "px" } : {});
|
||||
return <svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" style={style}>
|
||||
<g>
|
||||
{ /* vertical line */}
|
||||
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2="0" x2={size/2}
|
||||
y1={size/2} x1={size/2} strokeWidth="1.5" stroke="#000" fill="none"/>
|
||||
{ /* horizontal line */}
|
||||
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2={size/2} x2={size}
|
||||
y1={size/2} x1={size/2} fillOpacity="null" strokeOpacity="null" strokeWidth="1.5"
|
||||
stroke="#000" fill="none"/>
|
||||
</g>
|
||||
</svg>;
|
||||
}
|
||||
|
||||
formatSize(size) {
|
||||
const suffixes = ["B","KiB","MiB","GiB","TiB"];
|
||||
let i = 0;
|
||||
@@ -20,24 +54,161 @@ export class FileBrowser extends React.Component {
|
||||
return size.toFixed(1) + " " + suffixes[i];
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
canUpload() {
|
||||
return this.state.api.loggedIn || (this.state.token.valid && this.state.token.type === "upload");
|
||||
}
|
||||
|
||||
onAddUploadFiles(acceptedFiles) {
|
||||
let files = this.state.filesToUpload.slice();
|
||||
files.push(...acceptedFiles);
|
||||
this.setState({ ...this.state, filesToUpload: files });
|
||||
}
|
||||
|
||||
getSelectedIds(items = null, recursive = true) {
|
||||
let ids = [];
|
||||
items = items || this.state.files;
|
||||
for (const fileItem of Object.values(items)) {
|
||||
if (fileItem.selected) {
|
||||
ids.push(fileItem.uid);
|
||||
}
|
||||
if (recursive && fileItem.isDirectory) {
|
||||
ids.push(...this.getSelectedIds(fileItem.items));
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
onSelectAll(selected, items) {
|
||||
for (const fileElement of Object.values(items)) {
|
||||
fileElement.selected = selected;
|
||||
if (fileElement.isDirectory) {
|
||||
this.onSelectAll(selected, fileElement.items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSelectFile(e, uid, items=null) {
|
||||
|
||||
let found = false;
|
||||
let updatedFiles = (items === null) ? {...this.state.files} : items;
|
||||
if (updatedFiles.hasOwnProperty(uid)) {
|
||||
let fileElement = updatedFiles[uid];
|
||||
found = true;
|
||||
fileElement.selected = e.target.checked;
|
||||
if (fileElement.isDirectory) {
|
||||
this.onSelectAll(fileElement.selected, fileElement.items);
|
||||
}
|
||||
} else {
|
||||
for (const fileElement of Object.values(updatedFiles)) {
|
||||
if (fileElement.isDirectory) {
|
||||
if (this.onSelectFile(e, uid, fileElement.items)) {
|
||||
if (!e.target.checked) {
|
||||
fileElement.selected = false;
|
||||
} else if (this.getSelectedIds(fileElement.items, false).length === Object.values(fileElement.items).length) {
|
||||
fileElement.selected = true;
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (items === null) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
files: updatedFiles
|
||||
});
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
createFileList(elements, indentation=0) {
|
||||
let rows = [];
|
||||
for (const [uid, fileElement] of Object.entries(this.state.files)) {
|
||||
let i = 0;
|
||||
const values = Object.values(elements);
|
||||
for (const fileElement of values) {
|
||||
let name = fileElement.name;
|
||||
let type = (fileElement.directory ? "Directory" : fileElement.mimeType);
|
||||
let size = (fileElement.directory ? "" : fileElement.size)
|
||||
let uid = fileElement.uid;
|
||||
let type = (fileElement.isDirectory ? "Directory" : fileElement.mimeType);
|
||||
let size = (fileElement.isDirectory ? "" : this.formatSize(fileElement.size));
|
||||
// let iconUrl = (fileElement.directory ? "/img/icon/")
|
||||
let iconUrl = "";
|
||||
let token = (this.state.token && this.state.token.valid ? "&token=" + this.token.state.value : "");
|
||||
let svg = <></>;
|
||||
if (indentation > 0) {
|
||||
if (i === values.length - 1) {
|
||||
svg = this.svgEnd(indentation, 48);
|
||||
} else {
|
||||
svg = this.svgMiddle(indentation, 48);
|
||||
}
|
||||
}
|
||||
|
||||
rows.push(
|
||||
<tr key={"file-" + uid}>
|
||||
<td><img src={iconUrl} alt={"[Icon]"} /></td>
|
||||
<td>{name}</td>
|
||||
<tr key={"file-" + uid} data-id={uid} className={"file-row"}>
|
||||
<td>
|
||||
{ svg }
|
||||
<img src={iconUrl} alt={"[Icon]"} />
|
||||
</td>
|
||||
<td>
|
||||
{fileElement.isDirectory ? name :
|
||||
<a href={"/api/file/download?id=" + uid + token} download={true}>{name}</a>
|
||||
}
|
||||
</td>
|
||||
<td>{type}</td>
|
||||
<td>{this.formatSize(size)}</td>
|
||||
<td>{size}</td>
|
||||
<td>
|
||||
<input type={"checkbox"} checked={!!fileElement.selected}
|
||||
onChange={(e) => this.onSelectFile(e, uid)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
if (fileElement.isDirectory) {
|
||||
rows.push(...this.createFileList(fileElement.items, indentation + 1));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let rows = this.createFileList(this.state.files);
|
||||
let selectedIds = this.getSelectedIds();
|
||||
let selectedCount = selectedIds.length;
|
||||
let uploadZone = <></>;
|
||||
let writePermissions = this.canUpload();
|
||||
let uploadedFiles = [];
|
||||
|
||||
if (writePermissions) {
|
||||
|
||||
for(let i = 0; i < this.state.filesToUpload.length; i++) {
|
||||
const file = this.state.filesToUpload[i];
|
||||
uploadedFiles.push(
|
||||
<span className={"uploaded-file"} key={i}>
|
||||
<img />
|
||||
<span>{file.name}</span>
|
||||
<Icon icon={"times"} onClick={(e) => this.onRemoveUploadedFile(e, i)}/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
uploadZone = <><Dropzone onDrop={this.onAddUploadFiles.bind(this)}>
|
||||
{({getRootProps, getInputProps}) => (
|
||||
<section className={"file-upload-container"}>
|
||||
<div {...getRootProps()}>
|
||||
<input {...getInputProps()} />
|
||||
<p>Drag 'n' drop some files here, or click to select files</p>
|
||||
{ uploadedFiles.length === 0 ? <Icon className={"mx-auto fa-3x text-black-50"} icon={"upload"}/> : <div>{uploadedFiles}</div> }
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</Dropzone>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <>
|
||||
@@ -49,12 +220,57 @@ export class FileBrowser extends React.Component {
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Size</th>
|
||||
<th/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ rows }
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={"file-control-buttons"}>
|
||||
<button type={"button"} className={"btn btn-success"} disabled={selectedCount === 0}>
|
||||
<Icon icon={"download"} className={"mr-1"}/>
|
||||
Download Selected Files ({selectedCount})
|
||||
</button>
|
||||
{ this.state.api.loggedIn ?
|
||||
<button type={"button"} className={"btn btn-info"}>
|
||||
<Icon icon={"plus"} className={"mr-1"}/>
|
||||
Create Directory
|
||||
</button> :
|
||||
<></>
|
||||
}
|
||||
{
|
||||
writePermissions ?
|
||||
<>
|
||||
<button type={"button"} className={"btn btn-primary"} disabled={uploadedFiles.length === 0}>
|
||||
<Icon icon={"upload"} className={"mr-1"}/>
|
||||
Upload
|
||||
</button>
|
||||
<button type={"button"} className={"btn btn-danger"} disabled={selectedCount === 0} onClick={(e) => this.deleteFiles(selectedIds)}>
|
||||
<Icon icon={"trash"} className={"mr-1"}/>
|
||||
Delete Selected Files ({selectedCount})
|
||||
</button>
|
||||
</>
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
|
||||
{ uploadZone }
|
||||
</>;
|
||||
}
|
||||
|
||||
onRemoveUploadedFile(e, i) {
|
||||
e.stopPropagation();
|
||||
let files = this.state.filesToUpload.slice();
|
||||
files.splice(i, 1);
|
||||
this.setState({ ...this.state, filesToUpload: files });
|
||||
}
|
||||
|
||||
deleteFiles(selectedIds) {
|
||||
// TODO: delete files
|
||||
this.state.api.delete(selectedIds).then((res) => {
|
||||
if (res.success) {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from "react";
|
||||
import Icon from "./icon";
|
||||
|
||||
export class TokenList extends React.Component {
|
||||
|
||||
@@ -25,6 +26,7 @@ export class TokenList extends React.Component {
|
||||
<td>{token.token}</td>
|
||||
<td>{token.type}</td>
|
||||
<td>{token.valid_until}</td>
|
||||
<td><Icon icon={"times"} className={"text-danger"}/></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@@ -38,12 +40,19 @@ export class TokenList extends React.Component {
|
||||
<th>Token</th>
|
||||
<th>Type</th>
|
||||
<th>Valid Until</th>
|
||||
<th/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ rows }
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button type={"button"} className={"btn btn-success m-2"}>
|
||||
<Icon icon={"plus"} className={"mr-1"}/>
|
||||
Create Token
|
||||
</button>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
@@ -49,10 +49,8 @@ class FileControlPanel extends React.Component {
|
||||
|
||||
if (!this.state.loaded) {
|
||||
this.api.fetchUser().then((isLoggedIn) => {
|
||||
console.log(`api.fetchUser => ${isLoggedIn}`);
|
||||
if (isLoggedIn) {
|
||||
this.api.listFiles().then((res) => {
|
||||
console.log(`api.listFiles => ${res.success}`);
|
||||
this.setState({ ...this.state, loaded: true, user: this.api.user, files: res.files });
|
||||
});
|
||||
} else {
|
||||
@@ -63,7 +61,7 @@ class FileControlPanel extends React.Component {
|
||||
} else if (this.api.loggedIn || this.state.token.valid) {
|
||||
let tokenList = (this.api.loggedIn) ?
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-6 col-md-8 col-sm-10 col-xs-12 mx-auto"}>
|
||||
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
|
||||
<TokenList api={this.api} />
|
||||
</div>
|
||||
</div> :
|
||||
@@ -71,9 +69,9 @@ class FileControlPanel extends React.Component {
|
||||
|
||||
return <div className={"container mt-4"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-6 col-md-8 col-sm-10 col-xs-12 mx-auto"}>
|
||||
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
|
||||
<h2>File Control Panel</h2>
|
||||
<FileBrowser files={this.state.files}/>
|
||||
<FileBrowser files={this.state.files} token={this.state.token} api={this.api} />
|
||||
</div>
|
||||
</div>
|
||||
{ tokenList }
|
||||
@@ -81,7 +79,7 @@ class FileControlPanel extends React.Component {
|
||||
} else {
|
||||
return <div className={"container mt-4"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-6 col-md-8 col-sm-10 col-xs-12 mx-auto"}>
|
||||
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
|
||||
<h2>File Control Panel</h2>
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<label htmlFor={"token"}>Enter a file token to download or upload files</label>
|
||||
|
||||
Reference in New Issue
Block a user