File Control frontend
This commit is contained in:
parent
48a3932451
commit
9337faab97
@ -115,7 +115,7 @@ namespace Api {
|
||||
$files = array();
|
||||
foreach ($res as $row) {
|
||||
if ($row["uid"] === null) continue;
|
||||
$fileId = (string)$row["uid"];
|
||||
$fileId = $row["uid"];
|
||||
$parentId = $row["parentId"];
|
||||
$fileName = $row["name"];
|
||||
$isDirectory = $row["directory"];
|
||||
@ -377,7 +377,6 @@ namespace Api\File {
|
||||
'parentId' => new Parameter('parentId', Parameter::TYPE_INT, true, null)
|
||||
));
|
||||
$this->loginRequired = true;
|
||||
$this->csrfTokenRequired = false;
|
||||
}
|
||||
|
||||
public function execute($values = array()) {
|
||||
|
@ -57,6 +57,18 @@ export default class API {
|
||||
return this.apiCall("file/revokeToken", { token: token });
|
||||
}
|
||||
|
||||
createDownloadToken(durability, files) {
|
||||
return this.apiCall("file/createDownloadToken", { files: files, durability: durability });
|
||||
}
|
||||
|
||||
createUploadToken(durability, parentId=null, maxFiles=0, maxSize=0, extensions = "") {
|
||||
return this.apiCall("file/createUploadToken", { parentId: parentId, durability: durability, maxFiles: maxFiles, maxSize: maxSize, extensions: extensions });
|
||||
}
|
||||
|
||||
createDirectory(name, parentId = null) {
|
||||
return this.apiCall("file/createDirectory", { name: name, parentId: parentId });
|
||||
}
|
||||
|
||||
async upload(files, token = null, parentId = null) {
|
||||
const csrf_token = this.csrfToken();
|
||||
|
||||
|
25
fileControlPanel/src/elements/alert.js
Normal file
25
fileControlPanel/src/elements/alert.js
Normal file
@ -0,0 +1,25 @@
|
||||
import Icon from "./icon";
|
||||
import React from "react";
|
||||
|
||||
export default function Alert(props) {
|
||||
|
||||
const onClose = props.onClose || null;
|
||||
const title = props.title || "Untitled Alert";
|
||||
const message = props.message || "Alert message";
|
||||
const type = props.type || "danger";
|
||||
|
||||
let icon = "ban";
|
||||
if (type === "warning") {
|
||||
icon = "exclamation-triangle";
|
||||
} else if(type === "success") {
|
||||
icon = "check";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"alert alert-" + type + " alert-dismissible"}>
|
||||
{onClose ? <button type="button" className={"close"} data-dismiss={"alert"} aria-hidden={"true"} onClick={onClose}>×</button> : null}
|
||||
<h5><Icon icon={icon} className={"icon"} /> {title}</h5>
|
||||
{message}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -60,6 +60,10 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.token-table td:nth-child(4) > i {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.file-table td:nth-child(n+3), .file-table th:nth-child(n+3) {
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -2,22 +2,23 @@ import * as React from "react";
|
||||
import "./file-browser.css";
|
||||
import Dropzone from "react-dropzone";
|
||||
import Icon from "./icon";
|
||||
import Alert from "./alert";
|
||||
import {Popup} from "./popup";
|
||||
import {useState} from "react";
|
||||
|
||||
export class FileBrowser extends React.Component {
|
||||
export function FileBrowser(props) {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
let files = props.files || { };
|
||||
let api = props.api;
|
||||
let tokenObj = props.token || { valid: false };
|
||||
let onSelectFile = props.onSelectFile || function() { };
|
||||
let onFetchFiles = props.onFetchFiles || function() { };
|
||||
|
||||
this.state = {
|
||||
api: props.api,
|
||||
files: props.files,
|
||||
token: props.token,
|
||||
filesToUpload: [],
|
||||
alerts: []
|
||||
}
|
||||
}
|
||||
let [popup, setPopup] = useState({ visible: false, directoryName: "" });
|
||||
let [alerts, setAlerts] = useState( []);
|
||||
let [filesToUpload, setFilesToUpload] = useState([]);
|
||||
|
||||
svgMiddle(indentation, scale=1.0) {
|
||||
function svgMiddle(indentation, scale=1.0) {
|
||||
let width = 48 * scale;
|
||||
let height = 64 * scale;
|
||||
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {});
|
||||
@ -33,7 +34,7 @@ export class FileBrowser extends React.Component {
|
||||
</svg>;
|
||||
}
|
||||
|
||||
svgEnd(indentation, scale=1.0) {
|
||||
function svgEnd(indentation, scale=1.0) {
|
||||
let width = 48 * scale;
|
||||
let height = 64 * scale;
|
||||
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {});
|
||||
@ -51,7 +52,7 @@ export class FileBrowser extends React.Component {
|
||||
</svg>;
|
||||
}
|
||||
|
||||
createFileIcon(mimeType, size=2) {
|
||||
function createFileIcon(mimeType, size=2) {
|
||||
let icon = "";
|
||||
if (mimeType !== null) {
|
||||
mimeType = mimeType.toLowerCase().trim();
|
||||
@ -89,7 +90,7 @@ export class FileBrowser extends React.Component {
|
||||
return <Icon icon={icon} type={"far"} className={"p-1 align-middle fa-" + size + "x"} />
|
||||
}
|
||||
|
||||
formatSize(size) {
|
||||
function formatSize(size) {
|
||||
const suffixes = ["B","KiB","MiB","GiB","TiB"];
|
||||
let i = 0;
|
||||
for (; i < suffixes.length && size >= 1024; i++) {
|
||||
@ -103,78 +104,32 @@ export class FileBrowser extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
canUpload() {
|
||||
return this.state.api.loggedIn || (this.state.token.valid && this.state.token.type === "upload");
|
||||
function canUpload() {
|
||||
return api.loggedIn || (tokenObj.valid && tokenObj.type === "upload");
|
||||
}
|
||||
|
||||
onAddUploadFiles(acceptedFiles) {
|
||||
let files = this.state.filesToUpload.slice();
|
||||
function onAddUploadFiles(acceptedFiles) {
|
||||
let files = filesToUpload.slice();
|
||||
files.push(...acceptedFiles);
|
||||
this.setState({ ...this.state, filesToUpload: files });
|
||||
setFilesToUpload(files);
|
||||
}
|
||||
|
||||
getSelectedIds(items = null, recursive = true) {
|
||||
function getSelectedIds(items = null, recursive = true) {
|
||||
let ids = [];
|
||||
items = items || this.state.files;
|
||||
items = items || 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));
|
||||
ids.push(...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) {
|
||||
function createFileList(elements, indentation=0) {
|
||||
let rows = [];
|
||||
let i = 0;
|
||||
const values = Object.values(elements);
|
||||
@ -182,15 +137,15 @@ export class FileBrowser extends React.Component {
|
||||
let name = fileElement.name;
|
||||
let uid = fileElement.uid;
|
||||
let type = (fileElement.isDirectory ? "Directory" : fileElement.mimeType);
|
||||
let size = (fileElement.isDirectory ? "" : this.formatSize(fileElement.size));
|
||||
let size = (fileElement.isDirectory ? "" : formatSize(fileElement.size));
|
||||
let mimeType = (fileElement.isDirectory ? "application/x-directory" : fileElement.mimeType);
|
||||
let token = (this.state.token && this.state.token.valid ? "&token=" + this.state.token.value : "");
|
||||
let token = (tokenObj && tokenObj.valid ? "&token=" + tokenObj.value : "");
|
||||
let svg = <></>;
|
||||
if (indentation > 0) {
|
||||
if (i === values.length - 1) {
|
||||
svg = this.svgEnd(indentation, 0.75);
|
||||
svg = svgEnd(indentation, 0.75);
|
||||
} else {
|
||||
svg = this.svgMiddle(indentation, 0.75);
|
||||
svg = svgMiddle(indentation, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,7 +153,7 @@ export class FileBrowser extends React.Component {
|
||||
<tr key={"file-" + uid} data-id={uid} className={"file-row"}>
|
||||
<td>
|
||||
{ svg }
|
||||
{ this.createFileIcon(mimeType) }
|
||||
{ createFileIcon(mimeType) }
|
||||
</td>
|
||||
<td>
|
||||
{fileElement.isDirectory ? name :
|
||||
@ -209,53 +164,49 @@ export class FileBrowser extends React.Component {
|
||||
<td>{size}</td>
|
||||
<td>
|
||||
<input type={"checkbox"} checked={!!fileElement.selected}
|
||||
onChange={(e) => this.onSelectFile(e, uid)}
|
||||
onChange={(e) => onSelectFile(e, uid)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
if (fileElement.isDirectory) {
|
||||
rows.push(...this.createFileList(fileElement.items, indentation + 1));
|
||||
rows.push(...createFileList(fileElement.items, indentation + 1));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let rows = this.createFileList(this.state.files);
|
||||
let selectedIds = this.getSelectedIds();
|
||||
let rows = createFileList(files);
|
||||
let selectedIds = getSelectedIds();
|
||||
let selectedCount = selectedIds.length;
|
||||
let uploadZone = <></>;
|
||||
let writePermissions = this.canUpload();
|
||||
let writePermissions = canUpload();
|
||||
let uploadedFiles = [];
|
||||
let alerts = [];
|
||||
let alertElements = [];
|
||||
|
||||
let i = 0;
|
||||
for (const alert of this.state.alerts) {
|
||||
alerts.push(
|
||||
<div key={"alert-" + i++} className={"alert alert-" + alert.type}>
|
||||
{ alert.text }
|
||||
</div>
|
||||
for (let i = 0; i < alerts.length; i++) {
|
||||
const alert = alerts[i];
|
||||
alertElements.push(
|
||||
<Alert key={"alert-" + i} {...alert} onClose={() => removeAlert(i)} />
|
||||
);
|
||||
}
|
||||
|
||||
if (writePermissions) {
|
||||
|
||||
for(let i = 0; i < this.state.filesToUpload.length; i++) {
|
||||
const file = this.state.filesToUpload[i];
|
||||
for(let i = 0; i < filesToUpload.length; i++) {
|
||||
const file = filesToUpload[i];
|
||||
uploadedFiles.push(
|
||||
<span className={"uploaded-file"} key={i}>
|
||||
{ this.createFileIcon(file.type, 3) }
|
||||
{ createFileIcon(file.type, 3) }
|
||||
<span>{file.name}</span>
|
||||
<Icon icon={"times"} onClick={(e) => this.onRemoveUploadedFile(e, i)}/>
|
||||
<Icon icon={"times"} onClick={(e) => onRemoveUploadedFile(e, i)}/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
uploadZone = <><Dropzone onDrop={this.onAddUploadFiles.bind(this)}>
|
||||
uploadZone = <><Dropzone onDrop={onAddUploadFiles}>
|
||||
{({getRootProps, getInputProps}) => (
|
||||
<section className={"file-upload-container"}>
|
||||
<div {...getRootProps()}>
|
||||
@ -273,7 +224,10 @@ export class FileBrowser extends React.Component {
|
||||
}
|
||||
|
||||
return <>
|
||||
<h4>File Browser</h4>
|
||||
<h4>
|
||||
<Icon icon={"sync"} className={"mx-3 clickable small"} onClick={fetchFiles}/>
|
||||
File Browser
|
||||
</h4>
|
||||
<table className={"table data-table file-table"}>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -285,17 +239,23 @@ export class FileBrowser extends React.Component {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ rows }
|
||||
{ rows.length > 0 ? rows :
|
||||
<tr>
|
||||
<td colSpan={4} className={"text-center text-black-50"}>
|
||||
No files uploaded yet
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={"file-control-buttons"}>
|
||||
<button type={"button"} className={"btn btn-success"} disabled={selectedCount === 0}
|
||||
onClick={() => this.onDownload(selectedIds)}>
|
||||
onClick={() => onDownload(selectedIds)}>
|
||||
<Icon icon={"download"} className={"mr-1"}/>
|
||||
Download Selected Files ({selectedCount})
|
||||
</button>
|
||||
{ this.state.api.loggedIn ?
|
||||
<button type={"button"} className={"btn btn-info"}>
|
||||
{ api.loggedIn ?
|
||||
<button type={"button"} className={"btn btn-info"} onClick={onPopupOpen}>
|
||||
<Icon icon={"plus"} className={"mr-1"}/>
|
||||
Create Directory
|
||||
</button> :
|
||||
@ -305,12 +265,12 @@ export class FileBrowser extends React.Component {
|
||||
writePermissions ?
|
||||
<>
|
||||
<button type={"button"} className={"btn btn-primary"} disabled={uploadedFiles.length === 0}
|
||||
onClick={this.onUpload.bind(this)}>
|
||||
onClick={onUpload}>
|
||||
<Icon icon={"upload"} className={"mr-1"}/>
|
||||
Upload
|
||||
</button>
|
||||
<button type={"button"} className={"btn btn-danger"} disabled={selectedCount === 0}
|
||||
onClick={() => this.deleteFiles(selectedIds)}>
|
||||
onClick={() => deleteFiles(selectedIds)}>
|
||||
<Icon icon={"trash"} className={"mr-1"}/>
|
||||
Delete Selected Files ({selectedCount})
|
||||
</button>
|
||||
@ -320,72 +280,113 @@ export class FileBrowser extends React.Component {
|
||||
</div>
|
||||
{ uploadZone }
|
||||
<div>
|
||||
{ alerts }
|
||||
{ alertElements }
|
||||
</div>
|
||||
<Popup title={"Create Directory"} visible={popup.visible} buttons={["Ok","Cancel"]} onClose={onPopupClose} onClick={onPopupButton}>
|
||||
<div className={"form-group"}>
|
||||
<label>Directory Name</label>
|
||||
<input type={"text"} className={"form-control"} value={popup.directoryName} maxLength={32} placeholder={"Enter name…"}
|
||||
onChange={(e) => onPopupChange(e, "directoryName")}/>
|
||||
</div>
|
||||
</Popup>
|
||||
</>;
|
||||
|
||||
function onPopupOpen() {
|
||||
setPopup({ ...popup, visible: true });
|
||||
}
|
||||
|
||||
fetchFiles() {
|
||||
if (this.state.token.valid) {
|
||||
this.state.api.validateToken(this.state.token.value).then((res) => {
|
||||
if (res) {
|
||||
this.setState({ ...this.state, files: res.files });
|
||||
function onPopupClose() {
|
||||
setPopup({ ...popup, visible: false });
|
||||
}
|
||||
|
||||
function onPopupChange(e, key) {
|
||||
setPopup({ ...popup, [key]: e.target.value });
|
||||
}
|
||||
|
||||
function onPopupButton(btn) {
|
||||
|
||||
if (btn === "Ok") {
|
||||
api.createDirectory(popup.directoryName, null).then((res) => {
|
||||
if (!res.success) {
|
||||
pushAlert(res, "Error creating directory");
|
||||
} else {
|
||||
this.pushAlert(res);
|
||||
fetchFiles();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onPopupClose();
|
||||
}
|
||||
|
||||
function fetchFiles() {
|
||||
if (tokenObj.valid) {
|
||||
api.validateToken(tokenObj.value).then((res) => {
|
||||
if (res) {
|
||||
onFetchFiles(res.files);
|
||||
} else {
|
||||
pushAlert(res);
|
||||
}
|
||||
});
|
||||
} else if (this.state.api.loggedIn) {
|
||||
this.state.api.listFiles().then((res) => {
|
||||
} else if (api.loggedIn) {
|
||||
api.listFiles().then((res) => {
|
||||
if (res) {
|
||||
this.setState({ ...this.state, files: res.files });
|
||||
onFetchFiles(res.files);
|
||||
} else {
|
||||
this.pushAlert(res);
|
||||
pushAlert(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onRemoveUploadedFile(e, i) {
|
||||
function onRemoveUploadedFile(e, i) {
|
||||
e.stopPropagation();
|
||||
let files = this.state.filesToUpload.slice();
|
||||
let files = filesToUpload.slice();
|
||||
files.splice(i, 1);
|
||||
this.setState({ ...this.state, filesToUpload: files });
|
||||
setFilesToUpload(files);
|
||||
}
|
||||
|
||||
pushAlert(res) {
|
||||
let newAlerts = this.state.alerts.slice();
|
||||
newAlerts.push({ type: "danger", text: res.msg });
|
||||
this.setState({ ...this.state, alerts: newAlerts });
|
||||
function pushAlert(res, title) {
|
||||
let newAlerts = alerts.slice();
|
||||
newAlerts.push({ type: "danger", message: res.msg, title: title });
|
||||
setAlerts(newAlerts);
|
||||
}
|
||||
|
||||
deleteFiles(selectedIds) {
|
||||
function removeAlert(i) {
|
||||
if (i >= 0 && i < alerts.length) {
|
||||
let newAlerts = alerts.slice();
|
||||
newAlerts.splice(i, 1);
|
||||
setAlerts(newAlerts);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteFiles(selectedIds) {
|
||||
if (selectedIds && selectedIds.length > 0) {
|
||||
let token = (this.state.api.loggedIn ? null : this.state.token.value);
|
||||
this.state.api.delete(selectedIds, token).then((res) => {
|
||||
let token = (api.loggedIn ? null : tokenObj.value);
|
||||
api.delete(selectedIds, token).then((res) => {
|
||||
if (res.success) {
|
||||
this.fetchFiles();
|
||||
fetchFiles();
|
||||
} else {
|
||||
this.pushAlert(res);
|
||||
pushAlert(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onUpload() {
|
||||
let token = (this.state.api.loggedIn ? null : this.state.token.value);
|
||||
this.state.api.upload(this.state.filesToUpload, token).then((res) => {
|
||||
function onUpload() {
|
||||
let token = (api.loggedIn ? null : tokenObj.value);
|
||||
api.upload(filesToUpload, token).then((res) => {
|
||||
if (res.success) {
|
||||
this.setState({ ...this.state, filesToUpload: [] })
|
||||
this.fetchFiles();
|
||||
setFilesToUpload([]);
|
||||
fetchFiles();
|
||||
} else {
|
||||
this.pushAlert(res);
|
||||
pushAlert(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDownload(selectedIds) {
|
||||
function onDownload(selectedIds) {
|
||||
if (selectedIds && selectedIds.length > 0) {
|
||||
let token = (this.state.api.loggedIn ? "" : "&token=" + this.state.token.value);
|
||||
let token = (api.loggedIn ? "" : "&token=" + tokenObj.value);
|
||||
let ids = selectedIds.map(id => "id[]=" + id).join("&");
|
||||
let downloadUrl = "/api/file/download?" + ids + token;
|
||||
fetch(downloadUrl)
|
||||
|
@ -1,37 +1,46 @@
|
||||
import React from 'react';
|
||||
|
||||
class Popup extends React.Component {
|
||||
export function Popup(props) {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
title: props.title || "Title",
|
||||
content: props.content || "Content",
|
||||
buttons: props.buttons || ["Ok", "Cancel"]
|
||||
}
|
||||
let buttonNames = props.buttons || ["Ok", "Cancel"];
|
||||
let onClick = props.onClick || function () { };
|
||||
let visible = !!props.visible;
|
||||
let title = props.title || "Popup Title";
|
||||
let onClose = props.onClose || function() { };
|
||||
|
||||
let buttons = [];
|
||||
const colors = ["primary", "secondary", "success", "warning", "danger"];
|
||||
for (let i = 0; i < buttonNames.length; i++) {
|
||||
let name = buttonNames[i];
|
||||
let color = colors[i % colors.length];
|
||||
buttons.push(
|
||||
<button key={"btn-" + i} type={"button"} className={"btn btn-" + color} data-dismiss={"modal"}
|
||||
onClick={() => onClick(name)}>
|
||||
{name}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return <div className="modal" tabIndex="-1" role="dialog">
|
||||
return <>
|
||||
<div className={"modal fade" + (visible ? " show" : "")} tabIndex="-1" role="dialog" style={{display: (visible) ? "block" : "none"}}>
|
||||
<div className="modal-dialog" role="document">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">{this.state.title}</h5>
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
|
||||
<h5 className="modal-title">{title}</h5>
|
||||
<button type="button" className="close" aria-label="Close" onClick={onClose}>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>Modal body text goes here.</p>
|
||||
{props.children}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" className="btn btn-primary">Save changes</button>
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
</div>
|
||||
{visible ? <div className={"modal-backdrop fade show"}/> : <></>}
|
||||
</>;
|
||||
|
||||
}
|
@ -1,105 +1,236 @@
|
||||
import * as React from "react";
|
||||
import Icon from "./icon";
|
||||
import moment from "moment";
|
||||
import Popup from "react-popup";
|
||||
import {Popup} from "./popup";
|
||||
import Alert from "./alert";
|
||||
import {useState} from "react";
|
||||
|
||||
export class TokenList extends React.Component {
|
||||
export function TokenList(props) {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
let api = props.api;
|
||||
let selectedFiles = props.selectedFiles || [];
|
||||
|
||||
this.state = {
|
||||
api: props.api,
|
||||
tokens: null,
|
||||
alerts: []
|
||||
}
|
||||
let [tokens, setTokens] = useState(null);
|
||||
let [alerts, setAlerts] = useState([]);
|
||||
let [hideRevoked, setHideRevoked] = useState(true);
|
||||
let [popup, setPopup] = useState({
|
||||
tokenType: "download",
|
||||
maxFiles: 0,
|
||||
maxSize: 0,
|
||||
extensions: "",
|
||||
durability: 24 * 60 * 2,
|
||||
visible: false
|
||||
});
|
||||
|
||||
function fetchTokens() {
|
||||
api.listTokens().then((res) => {
|
||||
if (res) {
|
||||
setTokens(res.tokens);
|
||||
} else {
|
||||
pushAlert(res, "Error fetching tokens");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let rows = [];
|
||||
if (this.state.tokens === null) {
|
||||
this.state.api.listTokens().then((res) => {
|
||||
this.setState({ ...this.state, tokens: res.tokens });
|
||||
});
|
||||
} else {
|
||||
for (const token of this.state.tokens) {
|
||||
const validUntil = token.valid_until;
|
||||
const revoked = validUntil !== null && moment(validUntil).isSameOrBefore(new Date());
|
||||
const timeStr = (validUntil === null ? "Forever" : moment(validUntil).format("Do MMM YYYY, HH:mm"));
|
||||
|
||||
rows.push(
|
||||
<tr key={"token-" + token.uid} className={revoked ? "token-revoked" : ""}>
|
||||
<td>{token.token}</td>
|
||||
<td>{token.type}</td>
|
||||
<td>{timeStr}</td>
|
||||
<td>
|
||||
<Icon icon={"times"} className={"clickable text-" + (revoked ? "secondary" : "danger")}
|
||||
onClick={() => (revoked ? null : this.onRevokeToken(token.token) )}
|
||||
disabled={revoked} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
let rows = [];
|
||||
if (tokens === null) {
|
||||
fetchTokens();
|
||||
} else {
|
||||
for (const token of tokens) {
|
||||
const validUntil = token.valid_until;
|
||||
const revoked = validUntil !== null && moment(validUntil).isSameOrBefore(new Date());
|
||||
if (revoked && hideRevoked) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let alerts = [];
|
||||
let i = 0;
|
||||
for (const alert of this.state.alerts) {
|
||||
alerts.push(
|
||||
<div key={"alert-" + i++} className={"alert alert-" + alert.type}>
|
||||
{ alert.text }
|
||||
</div>
|
||||
const timeStr = (validUntil === null ? "Forever" : moment(validUntil).format("Do MMM YYYY, HH:mm"));
|
||||
|
||||
rows.push(
|
||||
<tr key={"token-" + token.uid} className={revoked ? "token-revoked" : ""}>
|
||||
<td>{token.token}</td>
|
||||
<td>{token.type}</td>
|
||||
<td>{timeStr}</td>
|
||||
<td>
|
||||
<Icon icon={"times"} className={"clickable text-" + (revoked ? "secondary" : "danger")}
|
||||
onClick={() => (revoked ? null : onRevokeToken(token.token))}
|
||||
disabled={revoked}/>
|
||||
<Icon icon={"save"} className={"clickable text-" + (revoked ? "secondary" : "info")}
|
||||
onClick={() => (revoked ? null : onCopyToken(token.token))}
|
||||
disabled={revoked}/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return <>
|
||||
<h4>Tokens</h4>
|
||||
<table className={"table token-table"}>
|
||||
<thead>
|
||||
<tr>
|
||||
<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"} onClick={this.onCreateToken.bind(this)}>
|
||||
<Icon icon={"plus"} className={"mr-1"}/>
|
||||
Create Token
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
{ alerts }
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
onRevokeToken(token) {
|
||||
this.state.api.revokeToken(token).then((res) => {
|
||||
let alertElements = [];
|
||||
for (let i = 0; i < alerts.length; i++) {
|
||||
const alert = alerts[i];
|
||||
alertElements.push(
|
||||
<Alert key={"alert-" + i} {...alert} onClose={() => removeAlert(i)}/>
|
||||
);
|
||||
}
|
||||
|
||||
return <>
|
||||
<h4>
|
||||
<Icon icon={"sync"} className={"mx-3 clickable small"} onClick={fetchTokens}/>
|
||||
Tokens
|
||||
</h4>
|
||||
<div className={"form-check p-3 ml-3"}>
|
||||
<input type={"checkbox"} checked={hideRevoked} name={"hide-revoked"}
|
||||
className={"form-check-input"} style={{marginTop: "0.2rem"}}
|
||||
onChange={(e) => setHideRevoked(e.target.checked)}/>
|
||||
<label htmlFor={"hide-revoked"} className={"form-check-label pl-2"}>Hide revoked</label>
|
||||
</div>
|
||||
<table className={"table token-table"}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Token</th>
|
||||
<th>Type</th>
|
||||
<th>Valid Until</th>
|
||||
<th/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length > 0 ? rows :
|
||||
<tr>
|
||||
<td colSpan={4} className={"text-center text-black-50"}>
|
||||
No active tokens connected with this account
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button type={"button"} className={"btn btn-success m-2"} onClick={onPopupOpen}>
|
||||
<Icon icon={"plus"} className={"mr-1"}/>
|
||||
Create Token
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
{alertElements}
|
||||
</div>
|
||||
<Popup title={"Create Token"} visible={popup.visible} buttons={["Ok", "Cancel"]}
|
||||
onClose={onPopupClose} onClick={onPopupButton}>
|
||||
<div className={"form-group"}>
|
||||
<label>Token Durability in minutes (0 = forever):</label>
|
||||
<input type={"number"} min={0} className={"form-control"}
|
||||
value={popup.durability} onChange={(e) => onPopupChange(e, "durability")}/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Token Type:</label>
|
||||
<select value={popup.tokenType} className={"form-control"}
|
||||
onChange={(e) => onPopupChange(e, "tokenType")}>
|
||||
<option value={"upload"}>Upload</option>
|
||||
<option value={"download"}>Download</option>
|
||||
</select>
|
||||
</div>
|
||||
{popup.tokenType === "upload" ?
|
||||
<>
|
||||
<b>Upload Restrictions:</b>
|
||||
<div className={"form-group"}>
|
||||
<label>Max. Files (0 = unlimited):</label>
|
||||
<input type={"number"} min={0} max={25} className={"form-control"}
|
||||
value={popup.maxFiles}
|
||||
onChange={(e) => onPopupChange(e, "maxFiles")}/>
|
||||
</div>
|
||||
<div className={"form-group"}>
|
||||
<label>Max. Size per file in MB (0 = unlimited):</label>
|
||||
<input type={"number"} min={0} max={10} className={"form-control"}
|
||||
value={popup.maxSize} onChange={(e) => onPopupChange(e, "maxSize")}/>
|
||||
</div>
|
||||
<div className={"form-group"}>
|
||||
<label>Allowed Extensions:</label>
|
||||
<input type={"text"} placeholder={"(no restrictions)"} maxLength={256}
|
||||
className={"form-control"}
|
||||
value={popup.extensions}
|
||||
onChange={(e) => onPopupChange(e, "extensions")}/>
|
||||
</div>
|
||||
</> :
|
||||
<></>
|
||||
}
|
||||
</Popup>
|
||||
</>;
|
||||
|
||||
function pushAlert(res, title) {
|
||||
let newAlerts = alerts.slice();
|
||||
newAlerts.push({type: "danger", message: res.msg, title: title});
|
||||
setAlerts(newAlerts);
|
||||
}
|
||||
|
||||
function removeAlert(i) {
|
||||
if (i >= 0 && i < alerts.length) {
|
||||
let newAlerts = alerts.slice();
|
||||
newAlerts.splice(i, 1);
|
||||
setAlerts(newAlerts);
|
||||
}
|
||||
}
|
||||
|
||||
function onRevokeToken(token) {
|
||||
api.revokeToken(token).then((res) => {
|
||||
if (res.success) {
|
||||
let newTokens = this.state.tokens.slice();
|
||||
let newTokens = tokens.slice();
|
||||
for (const tokenObj of newTokens) {
|
||||
if (tokenObj.token === token) {
|
||||
tokenObj.valid_until = moment();
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.setState({ ...this.state, tokens: newTokens });
|
||||
setTokens(newTokens);
|
||||
} else {
|
||||
let newAlerts = this.state.alerts.slice();
|
||||
newAlerts.push({ type: "danger", text: res.msg });
|
||||
this.setState({ ...this.state, alerts: newAlerts });
|
||||
pushAlert(res, "Error revoking token");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCreateToken() {
|
||||
Popup.alert('I am alert, nice to meet you');
|
||||
function onPopupOpen() {
|
||||
setPopup({...popup, visible: true});
|
||||
}
|
||||
|
||||
function onPopupClose() {
|
||||
setPopup({...popup, visible: false});
|
||||
}
|
||||
|
||||
function onPopupChange(e, key) {
|
||||
setPopup({...popup, [key]: e.target.value});
|
||||
}
|
||||
|
||||
function onPopupButton(btn) {
|
||||
|
||||
if (btn === "Ok") {
|
||||
let durability = popup.durability;
|
||||
let validUntil = (durability === 0 ? null : moment().add(durability, "hours").format("YYYY-MM-DD HH:mm:ss"));
|
||||
if (popup.tokenType === "download") {
|
||||
api.createDownloadToken(durability, selectedFiles).then((res) => {
|
||||
if (!res.success) {
|
||||
pushAlert(res, "Error creating token");
|
||||
} else {
|
||||
let newTokens = tokens.slice();
|
||||
newTokens.push({token: res.token, valid_until: validUntil, type: "download"});
|
||||
setTokens(newTokens);
|
||||
}
|
||||
});
|
||||
} else if (popup.tokenType === "upload") {
|
||||
api.createUploadToken(durability, null, popup.maxFiles, popup.maxSize, popup.extensions).then((res) => {
|
||||
if (!res.success) {
|
||||
pushAlert(res, "Error creating token");
|
||||
} else {
|
||||
let newTokens = tokens.slice();
|
||||
newTokens.push({uid: res.tokenId, token: res.token, valid_until: validUntil, type: "upload"});
|
||||
setTokens(newTokens);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onPopupClose();
|
||||
}
|
||||
|
||||
function onCopyToken(token) {
|
||||
let url = window.location.href;
|
||||
if (!url.endsWith("/")) url += "/";
|
||||
url += token;
|
||||
navigator.clipboard.writeText(url);
|
||||
}
|
||||
}
|
@ -16,10 +16,75 @@ class FileControlPanel extends React.Component {
|
||||
errorMessage: "",
|
||||
user: { },
|
||||
token: { valid: false, value: "", validUntil: null, type: null },
|
||||
files: [],
|
||||
files: {}
|
||||
};
|
||||
}
|
||||
|
||||
onFetchFiles(files) {
|
||||
this.setState({ ...this.state, files: 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;
|
||||
}
|
||||
|
||||
onValidateToken(token = null) {
|
||||
if (token === null) {
|
||||
this.setState({ ...this.state, validatingToken: true, errorMessage: "" });
|
||||
@ -58,7 +123,6 @@ class FileControlPanel extends React.Component {
|
||||
let start = (pathName.startsWith("/files/") ? ("/files/").length : 1);
|
||||
let token = pathName.substr(start, end);
|
||||
if (token) {
|
||||
// this.setState({ ...this.state, loaded: true, token: { ...this.state.token, value: token } });
|
||||
this.onValidateToken(token);
|
||||
checkUser = false;
|
||||
}
|
||||
@ -78,10 +142,11 @@ class FileControlPanel extends React.Component {
|
||||
|
||||
return <>Loading… <Icon icon={"spinner"} /></>;
|
||||
} else if (this.api.loggedIn || this.state.token.valid) {
|
||||
let selectedIds = this.getSelectedIds();
|
||||
let tokenList = (this.api.loggedIn) ?
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
|
||||
<TokenList api={this.api} />
|
||||
<TokenList api={this.api} selectedFiles={selectedIds} />
|
||||
</div>
|
||||
</div> :
|
||||
<></>;
|
||||
@ -91,7 +156,9 @@ class FileControlPanel extends React.Component {
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
|
||||
<h2>File Control Panel</h2>
|
||||
<FileBrowser files={this.state.files} token={this.state.token} api={this.api} />
|
||||
<FileBrowser files={this.state.files} token={this.state.token} api={this.api}
|
||||
onSelectFile={this.onSelectFile.bind(this)}
|
||||
onFetchFiles={this.onFetchFiles.bind(this)}/>
|
||||
</div>
|
||||
</div>
|
||||
{ tokenList }
|
||||
|
14
js/files.min.js
vendored
14
js/files.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user