File Control frontend
This commit is contained in:
parent
48a3932451
commit
9337faab97
@ -115,7 +115,7 @@ namespace Api {
|
|||||||
$files = array();
|
$files = array();
|
||||||
foreach ($res as $row) {
|
foreach ($res as $row) {
|
||||||
if ($row["uid"] === null) continue;
|
if ($row["uid"] === null) continue;
|
||||||
$fileId = (string)$row["uid"];
|
$fileId = $row["uid"];
|
||||||
$parentId = $row["parentId"];
|
$parentId = $row["parentId"];
|
||||||
$fileName = $row["name"];
|
$fileName = $row["name"];
|
||||||
$isDirectory = $row["directory"];
|
$isDirectory = $row["directory"];
|
||||||
@ -377,7 +377,6 @@ namespace Api\File {
|
|||||||
'parentId' => new Parameter('parentId', Parameter::TYPE_INT, true, null)
|
'parentId' => new Parameter('parentId', Parameter::TYPE_INT, true, null)
|
||||||
));
|
));
|
||||||
$this->loginRequired = true;
|
$this->loginRequired = true;
|
||||||
$this->csrfTokenRequired = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute($values = array()) {
|
public function execute($values = array()) {
|
||||||
|
@ -57,6 +57,18 @@ export default class API {
|
|||||||
return this.apiCall("file/revokeToken", { token: token });
|
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) {
|
async upload(files, token = null, parentId = null) {
|
||||||
const csrf_token = this.csrfToken();
|
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;
|
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) {
|
.file-table td:nth-child(n+3), .file-table th:nth-child(n+3) {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -2,22 +2,23 @@ import * as React from "react";
|
|||||||
import "./file-browser.css";
|
import "./file-browser.css";
|
||||||
import Dropzone from "react-dropzone";
|
import Dropzone from "react-dropzone";
|
||||||
import Icon from "./icon";
|
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) {
|
let files = props.files || { };
|
||||||
super(props);
|
let api = props.api;
|
||||||
|
let tokenObj = props.token || { valid: false };
|
||||||
|
let onSelectFile = props.onSelectFile || function() { };
|
||||||
|
let onFetchFiles = props.onFetchFiles || function() { };
|
||||||
|
|
||||||
this.state = {
|
let [popup, setPopup] = useState({ visible: false, directoryName: "" });
|
||||||
api: props.api,
|
let [alerts, setAlerts] = useState( []);
|
||||||
files: props.files,
|
let [filesToUpload, setFilesToUpload] = useState([]);
|
||||||
token: props.token,
|
|
||||||
filesToUpload: [],
|
|
||||||
alerts: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
svgMiddle(indentation, scale=1.0) {
|
function svgMiddle(indentation, scale=1.0) {
|
||||||
let width = 48 * scale;
|
let width = 48 * scale;
|
||||||
let height = 64 * scale;
|
let height = 64 * scale;
|
||||||
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {});
|
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {});
|
||||||
@ -33,7 +34,7 @@ export class FileBrowser extends React.Component {
|
|||||||
</svg>;
|
</svg>;
|
||||||
}
|
}
|
||||||
|
|
||||||
svgEnd(indentation, scale=1.0) {
|
function svgEnd(indentation, scale=1.0) {
|
||||||
let width = 48 * scale;
|
let width = 48 * scale;
|
||||||
let height = 64 * scale;
|
let height = 64 * scale;
|
||||||
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {});
|
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {});
|
||||||
@ -51,7 +52,7 @@ export class FileBrowser extends React.Component {
|
|||||||
</svg>;
|
</svg>;
|
||||||
}
|
}
|
||||||
|
|
||||||
createFileIcon(mimeType, size=2) {
|
function createFileIcon(mimeType, size=2) {
|
||||||
let icon = "";
|
let icon = "";
|
||||||
if (mimeType !== null) {
|
if (mimeType !== null) {
|
||||||
mimeType = mimeType.toLowerCase().trim();
|
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"} />
|
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"];
|
const suffixes = ["B","KiB","MiB","GiB","TiB"];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (; i < suffixes.length && size >= 1024; i++) {
|
for (; i < suffixes.length && size >= 1024; i++) {
|
||||||
@ -103,78 +104,32 @@ export class FileBrowser extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canUpload() {
|
function canUpload() {
|
||||||
return this.state.api.loggedIn || (this.state.token.valid && this.state.token.type === "upload");
|
return api.loggedIn || (tokenObj.valid && tokenObj.type === "upload");
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddUploadFiles(acceptedFiles) {
|
function onAddUploadFiles(acceptedFiles) {
|
||||||
let files = this.state.filesToUpload.slice();
|
let files = filesToUpload.slice();
|
||||||
files.push(...acceptedFiles);
|
files.push(...acceptedFiles);
|
||||||
this.setState({ ...this.state, filesToUpload: files });
|
setFilesToUpload(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectedIds(items = null, recursive = true) {
|
function getSelectedIds(items = null, recursive = true) {
|
||||||
let ids = [];
|
let ids = [];
|
||||||
items = items || this.state.files;
|
items = items || files;
|
||||||
for (const fileItem of Object.values(items)) {
|
for (const fileItem of Object.values(items)) {
|
||||||
if (fileItem.selected) {
|
if (fileItem.selected) {
|
||||||
ids.push(fileItem.uid);
|
ids.push(fileItem.uid);
|
||||||
}
|
}
|
||||||
if (recursive && fileItem.isDirectory) {
|
if (recursive && fileItem.isDirectory) {
|
||||||
ids.push(...this.getSelectedIds(fileItem.items));
|
ids.push(...getSelectedIds(fileItem.items));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectAll(selected, items) {
|
function createFileList(elements, indentation=0) {
|
||||||
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 = [];
|
let rows = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const values = Object.values(elements);
|
const values = Object.values(elements);
|
||||||
@ -182,15 +137,15 @@ export class FileBrowser extends React.Component {
|
|||||||
let name = fileElement.name;
|
let name = fileElement.name;
|
||||||
let uid = fileElement.uid;
|
let uid = fileElement.uid;
|
||||||
let type = (fileElement.isDirectory ? "Directory" : fileElement.mimeType);
|
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 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 = <></>;
|
let svg = <></>;
|
||||||
if (indentation > 0) {
|
if (indentation > 0) {
|
||||||
if (i === values.length - 1) {
|
if (i === values.length - 1) {
|
||||||
svg = this.svgEnd(indentation, 0.75);
|
svg = svgEnd(indentation, 0.75);
|
||||||
} else {
|
} 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"}>
|
<tr key={"file-" + uid} data-id={uid} className={"file-row"}>
|
||||||
<td>
|
<td>
|
||||||
{ svg }
|
{ svg }
|
||||||
{ this.createFileIcon(mimeType) }
|
{ createFileIcon(mimeType) }
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{fileElement.isDirectory ? name :
|
{fileElement.isDirectory ? name :
|
||||||
@ -209,53 +164,49 @@ export class FileBrowser extends React.Component {
|
|||||||
<td>{size}</td>
|
<td>{size}</td>
|
||||||
<td>
|
<td>
|
||||||
<input type={"checkbox"} checked={!!fileElement.selected}
|
<input type={"checkbox"} checked={!!fileElement.selected}
|
||||||
onChange={(e) => this.onSelectFile(e, uid)}
|
onChange={(e) => onSelectFile(e, uid)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fileElement.isDirectory) {
|
if (fileElement.isDirectory) {
|
||||||
rows.push(...this.createFileList(fileElement.items, indentation + 1));
|
rows.push(...createFileList(fileElement.items, indentation + 1));
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
let rows = createFileList(files);
|
||||||
|
let selectedIds = getSelectedIds();
|
||||||
let rows = this.createFileList(this.state.files);
|
|
||||||
let selectedIds = this.getSelectedIds();
|
|
||||||
let selectedCount = selectedIds.length;
|
let selectedCount = selectedIds.length;
|
||||||
let uploadZone = <></>;
|
let uploadZone = <></>;
|
||||||
let writePermissions = this.canUpload();
|
let writePermissions = canUpload();
|
||||||
let uploadedFiles = [];
|
let uploadedFiles = [];
|
||||||
let alerts = [];
|
let alertElements = [];
|
||||||
|
|
||||||
let i = 0;
|
for (let i = 0; i < alerts.length; i++) {
|
||||||
for (const alert of this.state.alerts) {
|
const alert = alerts[i];
|
||||||
alerts.push(
|
alertElements.push(
|
||||||
<div key={"alert-" + i++} className={"alert alert-" + alert.type}>
|
<Alert key={"alert-" + i} {...alert} onClose={() => removeAlert(i)} />
|
||||||
{ alert.text }
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (writePermissions) {
|
if (writePermissions) {
|
||||||
|
|
||||||
for(let i = 0; i < this.state.filesToUpload.length; i++) {
|
for(let i = 0; i < filesToUpload.length; i++) {
|
||||||
const file = this.state.filesToUpload[i];
|
const file = filesToUpload[i];
|
||||||
uploadedFiles.push(
|
uploadedFiles.push(
|
||||||
<span className={"uploaded-file"} key={i}>
|
<span className={"uploaded-file"} key={i}>
|
||||||
{ this.createFileIcon(file.type, 3) }
|
{ createFileIcon(file.type, 3) }
|
||||||
<span>{file.name}</span>
|
<span>{file.name}</span>
|
||||||
<Icon icon={"times"} onClick={(e) => this.onRemoveUploadedFile(e, i)}/>
|
<Icon icon={"times"} onClick={(e) => onRemoveUploadedFile(e, i)}/>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadZone = <><Dropzone onDrop={this.onAddUploadFiles.bind(this)}>
|
uploadZone = <><Dropzone onDrop={onAddUploadFiles}>
|
||||||
{({getRootProps, getInputProps}) => (
|
{({getRootProps, getInputProps}) => (
|
||||||
<section className={"file-upload-container"}>
|
<section className={"file-upload-container"}>
|
||||||
<div {...getRootProps()}>
|
<div {...getRootProps()}>
|
||||||
@ -273,7 +224,10 @@ export class FileBrowser extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
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"}>
|
<table className={"table data-table file-table"}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -285,17 +239,23 @@ export class FileBrowser extends React.Component {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{ rows }
|
{ rows.length > 0 ? rows :
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4} className={"text-center text-black-50"}>
|
||||||
|
No files uploaded yet
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div className={"file-control-buttons"}>
|
<div className={"file-control-buttons"}>
|
||||||
<button type={"button"} className={"btn btn-success"} disabled={selectedCount === 0}
|
<button type={"button"} className={"btn btn-success"} disabled={selectedCount === 0}
|
||||||
onClick={() => this.onDownload(selectedIds)}>
|
onClick={() => onDownload(selectedIds)}>
|
||||||
<Icon icon={"download"} className={"mr-1"}/>
|
<Icon icon={"download"} className={"mr-1"}/>
|
||||||
Download Selected Files ({selectedCount})
|
Download Selected Files ({selectedCount})
|
||||||
</button>
|
</button>
|
||||||
{ this.state.api.loggedIn ?
|
{ api.loggedIn ?
|
||||||
<button type={"button"} className={"btn btn-info"}>
|
<button type={"button"} className={"btn btn-info"} onClick={onPopupOpen}>
|
||||||
<Icon icon={"plus"} className={"mr-1"}/>
|
<Icon icon={"plus"} className={"mr-1"}/>
|
||||||
Create Directory
|
Create Directory
|
||||||
</button> :
|
</button> :
|
||||||
@ -305,12 +265,12 @@ export class FileBrowser extends React.Component {
|
|||||||
writePermissions ?
|
writePermissions ?
|
||||||
<>
|
<>
|
||||||
<button type={"button"} className={"btn btn-primary"} disabled={uploadedFiles.length === 0}
|
<button type={"button"} className={"btn btn-primary"} disabled={uploadedFiles.length === 0}
|
||||||
onClick={this.onUpload.bind(this)}>
|
onClick={onUpload}>
|
||||||
<Icon icon={"upload"} className={"mr-1"}/>
|
<Icon icon={"upload"} className={"mr-1"}/>
|
||||||
Upload
|
Upload
|
||||||
</button>
|
</button>
|
||||||
<button type={"button"} className={"btn btn-danger"} disabled={selectedCount === 0}
|
<button type={"button"} className={"btn btn-danger"} disabled={selectedCount === 0}
|
||||||
onClick={() => this.deleteFiles(selectedIds)}>
|
onClick={() => deleteFiles(selectedIds)}>
|
||||||
<Icon icon={"trash"} className={"mr-1"}/>
|
<Icon icon={"trash"} className={"mr-1"}/>
|
||||||
Delete Selected Files ({selectedCount})
|
Delete Selected Files ({selectedCount})
|
||||||
</button>
|
</button>
|
||||||
@ -320,72 +280,113 @@ export class FileBrowser extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
{ uploadZone }
|
{ uploadZone }
|
||||||
<div>
|
<div>
|
||||||
{ alerts }
|
{ alertElements }
|
||||||
</div>
|
</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() {
|
function onPopupClose() {
|
||||||
if (this.state.token.valid) {
|
setPopup({ ...popup, visible: false });
|
||||||
this.state.api.validateToken(this.state.token.value).then((res) => {
|
}
|
||||||
if (res) {
|
|
||||||
this.setState({ ...this.state, files: res.files });
|
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 {
|
} 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) {
|
} else if (api.loggedIn) {
|
||||||
this.state.api.listFiles().then((res) => {
|
api.listFiles().then((res) => {
|
||||||
if (res) {
|
if (res) {
|
||||||
this.setState({ ...this.state, files: res.files });
|
onFetchFiles(res.files);
|
||||||
} else {
|
} else {
|
||||||
this.pushAlert(res);
|
pushAlert(res);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemoveUploadedFile(e, i) {
|
function onRemoveUploadedFile(e, i) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
let files = this.state.filesToUpload.slice();
|
let files = filesToUpload.slice();
|
||||||
files.splice(i, 1);
|
files.splice(i, 1);
|
||||||
this.setState({ ...this.state, filesToUpload: files });
|
setFilesToUpload(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
pushAlert(res) {
|
function pushAlert(res, title) {
|
||||||
let newAlerts = this.state.alerts.slice();
|
let newAlerts = alerts.slice();
|
||||||
newAlerts.push({ type: "danger", text: res.msg });
|
newAlerts.push({ type: "danger", message: res.msg, title: title });
|
||||||
this.setState({ ...this.state, alerts: newAlerts });
|
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) {
|
if (selectedIds && selectedIds.length > 0) {
|
||||||
let token = (this.state.api.loggedIn ? null : this.state.token.value);
|
let token = (api.loggedIn ? null : tokenObj.value);
|
||||||
this.state.api.delete(selectedIds, token).then((res) => {
|
api.delete(selectedIds, token).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
this.fetchFiles();
|
fetchFiles();
|
||||||
} else {
|
} else {
|
||||||
this.pushAlert(res);
|
pushAlert(res);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpload() {
|
function onUpload() {
|
||||||
let token = (this.state.api.loggedIn ? null : this.state.token.value);
|
let token = (api.loggedIn ? null : tokenObj.value);
|
||||||
this.state.api.upload(this.state.filesToUpload, token).then((res) => {
|
api.upload(filesToUpload, token).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
this.setState({ ...this.state, filesToUpload: [] })
|
setFilesToUpload([]);
|
||||||
this.fetchFiles();
|
fetchFiles();
|
||||||
} else {
|
} else {
|
||||||
this.pushAlert(res);
|
pushAlert(res);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onDownload(selectedIds) {
|
function onDownload(selectedIds) {
|
||||||
if (selectedIds && selectedIds.length > 0) {
|
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 ids = selectedIds.map(id => "id[]=" + id).join("&");
|
||||||
let downloadUrl = "/api/file/download?" + ids + token;
|
let downloadUrl = "/api/file/download?" + ids + token;
|
||||||
fetch(downloadUrl)
|
fetch(downloadUrl)
|
||||||
|
@ -1,37 +1,46 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
class Popup extends React.Component {
|
export function Popup(props) {
|
||||||
|
|
||||||
constructor(props) {
|
let buttonNames = props.buttons || ["Ok", "Cancel"];
|
||||||
super(props);
|
let onClick = props.onClick || function () { };
|
||||||
this.state = {
|
let visible = !!props.visible;
|
||||||
title: props.title || "Title",
|
let title = props.title || "Popup Title";
|
||||||
content: props.content || "Content",
|
let onClose = props.onClose || function() { };
|
||||||
buttons: props.buttons || ["Ok", "Cancel"]
|
|
||||||
}
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
render() {
|
<div className={"modal fade" + (visible ? " show" : "")} tabIndex="-1" role="dialog" style={{display: (visible) ? "block" : "none"}}>
|
||||||
return <div className="modal" tabIndex="-1" role="dialog">
|
|
||||||
<div className="modal-dialog" role="document">
|
<div className="modal-dialog" role="document">
|
||||||
<div className="modal-content">
|
<div className="modal-content">
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h5 className="modal-title">{this.state.title}</h5>
|
<h5 className="modal-title">{title}</h5>
|
||||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" className="close" aria-label="Close" onClick={onClose}>
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<p>Modal body text goes here.</p>
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button type="button" className="btn btn-secondary" data-dismiss="modal">Close</button>
|
{buttons}
|
||||||
<button type="button" className="btn btn-primary">Save changes</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
}
|
{visible ? <div className={"modal-backdrop fade show"}/> : <></>}
|
||||||
|
</>;
|
||||||
|
|
||||||
}
|
}
|
@ -1,31 +1,49 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Icon from "./icon";
|
import Icon from "./icon";
|
||||||
import moment from "moment";
|
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) {
|
let api = props.api;
|
||||||
super(props);
|
let selectedFiles = props.selectedFiles || [];
|
||||||
|
|
||||||
this.state = {
|
let [tokens, setTokens] = useState(null);
|
||||||
api: props.api,
|
let [alerts, setAlerts] = useState([]);
|
||||||
tokens: null,
|
let [hideRevoked, setHideRevoked] = useState(true);
|
||||||
alerts: []
|
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 = [];
|
let rows = [];
|
||||||
if (this.state.tokens === null) {
|
if (tokens === null) {
|
||||||
this.state.api.listTokens().then((res) => {
|
fetchTokens();
|
||||||
this.setState({ ...this.state, tokens: res.tokens });
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
for (const token of this.state.tokens) {
|
for (const token of tokens) {
|
||||||
const validUntil = token.valid_until;
|
const validUntil = token.valid_until;
|
||||||
const revoked = validUntil !== null && moment(validUntil).isSameOrBefore(new Date());
|
const revoked = validUntil !== null && moment(validUntil).isSameOrBefore(new Date());
|
||||||
|
if (revoked && hideRevoked) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const timeStr = (validUntil === null ? "Forever" : moment(validUntil).format("Do MMM YYYY, HH:mm"));
|
const timeStr = (validUntil === null ? "Forever" : moment(validUntil).format("Do MMM YYYY, HH:mm"));
|
||||||
|
|
||||||
rows.push(
|
rows.push(
|
||||||
@ -35,7 +53,10 @@ export class TokenList extends React.Component {
|
|||||||
<td>{timeStr}</td>
|
<td>{timeStr}</td>
|
||||||
<td>
|
<td>
|
||||||
<Icon icon={"times"} className={"clickable text-" + (revoked ? "secondary" : "danger")}
|
<Icon icon={"times"} className={"clickable text-" + (revoked ? "secondary" : "danger")}
|
||||||
onClick={() => (revoked ? null : this.onRevokeToken(token.token) )}
|
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}/>
|
disabled={revoked}/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -43,18 +64,25 @@ export class TokenList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let alerts = [];
|
let alertElements = [];
|
||||||
let i = 0;
|
for (let i = 0; i < alerts.length; i++) {
|
||||||
for (const alert of this.state.alerts) {
|
const alert = alerts[i];
|
||||||
alerts.push(
|
alertElements.push(
|
||||||
<div key={"alert-" + i++} className={"alert alert-" + alert.type}>
|
<Alert key={"alert-" + i} {...alert} onClose={() => removeAlert(i)}/>
|
||||||
{ alert.text }
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<h4>Tokens</h4>
|
<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"}>
|
<table className={"table token-table"}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -65,41 +93,144 @@ export class TokenList extends React.Component {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{ rows }
|
{rows.length > 0 ? rows :
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4} className={"text-center text-black-50"}>
|
||||||
|
No active tokens connected with this account
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div>
|
<div>
|
||||||
<button type={"button"} className={"btn btn-success m-2"} onClick={this.onCreateToken.bind(this)}>
|
<button type={"button"} className={"btn btn-success m-2"} onClick={onPopupOpen}>
|
||||||
<Icon icon={"plus"} className={"mr-1"}/>
|
<Icon icon={"plus"} className={"mr-1"}/>
|
||||||
Create Token
|
Create Token
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{ alerts }
|
{alertElements}
|
||||||
</div>
|
</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);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRevokeToken(token) {
|
function removeAlert(i) {
|
||||||
this.state.api.revokeToken(token).then((res) => {
|
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) {
|
if (res.success) {
|
||||||
let newTokens = this.state.tokens.slice();
|
let newTokens = tokens.slice();
|
||||||
for (const tokenObj of newTokens) {
|
for (const tokenObj of newTokens) {
|
||||||
if (tokenObj.token === token) {
|
if (tokenObj.token === token) {
|
||||||
tokenObj.valid_until = moment();
|
tokenObj.valid_until = moment();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setState({ ...this.state, tokens: newTokens });
|
setTokens(newTokens);
|
||||||
} else {
|
} else {
|
||||||
let newAlerts = this.state.alerts.slice();
|
pushAlert(res, "Error revoking token");
|
||||||
newAlerts.push({ type: "danger", text: res.msg });
|
|
||||||
this.setState({ ...this.state, alerts: newAlerts });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onCreateToken() {
|
function onPopupOpen() {
|
||||||
Popup.alert('I am alert, nice to meet you');
|
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: "",
|
errorMessage: "",
|
||||||
user: { },
|
user: { },
|
||||||
token: { valid: false, value: "", validUntil: null, type: null },
|
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) {
|
onValidateToken(token = null) {
|
||||||
if (token === null) {
|
if (token === null) {
|
||||||
this.setState({ ...this.state, validatingToken: true, errorMessage: "" });
|
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 start = (pathName.startsWith("/files/") ? ("/files/").length : 1);
|
||||||
let token = pathName.substr(start, end);
|
let token = pathName.substr(start, end);
|
||||||
if (token) {
|
if (token) {
|
||||||
// this.setState({ ...this.state, loaded: true, token: { ...this.state.token, value: token } });
|
|
||||||
this.onValidateToken(token);
|
this.onValidateToken(token);
|
||||||
checkUser = false;
|
checkUser = false;
|
||||||
}
|
}
|
||||||
@ -78,10 +142,11 @@ class FileControlPanel extends React.Component {
|
|||||||
|
|
||||||
return <>Loading… <Icon icon={"spinner"} /></>;
|
return <>Loading… <Icon icon={"spinner"} /></>;
|
||||||
} else if (this.api.loggedIn || this.state.token.valid) {
|
} else if (this.api.loggedIn || this.state.token.valid) {
|
||||||
|
let selectedIds = this.getSelectedIds();
|
||||||
let tokenList = (this.api.loggedIn) ?
|
let tokenList = (this.api.loggedIn) ?
|
||||||
<div className={"row"}>
|
<div className={"row"}>
|
||||||
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
|
<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>
|
||||||
</div> :
|
</div> :
|
||||||
<></>;
|
<></>;
|
||||||
@ -91,7 +156,9 @@ class FileControlPanel extends React.Component {
|
|||||||
<div className={"row"}>
|
<div className={"row"}>
|
||||||
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
|
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
|
||||||
<h2>File Control Panel</h2>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{ tokenList }
|
{ 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