wrolpi/app/src/components/admin/Downloads.js
Roland 387282b445
Adding my public gpg key.
Moving upload destination input out of drop Segment.

Disabling uploads when downloads are disabled.

Inverting labels for checkboxes in inverted modal.

Deleting file_groups which no longer contain the exclusive primary path.

Adding debug logging for all API errors.

Checking that upload output is not a directory.

Expanding nginx request size.

Adding docs to upload endpoint.

Using dot-circle icon for more image mimetypes.

Using Form.Field to make upload destination required.

Adding border to inverted Segments.

Many files can be uploaded in chunks.

Files can be uploaded, progress is displayed.

Showing white border around share QR code.

Displaying WROLPi colors for download icons.

Creating button to clear DirectorySearch.

Excluding channel/domain directories so other directories can be returned.

Ordering search results.

Ignoring when no channel is found only if we have a destination.

Limiting directory search results.

Displaying categories when searching destination directories.

Handling when live_chat.json file exists.

Allowing refresh of directory directly.

Logging which download was successful.

Removing timeout.

Creating index for directory names.

Sleeping to catch cancel.

Arbitrary files can be downloaded using the new FileDownloader.
2023-04-28 13:56:32 -06:00

397 lines
12 KiB
JavaScript

import React from "react";
import {clearCompletedDownloads, clearFailedDownloads, deleteDownload, killDownload, postDownload} from "../../api";
import {Link} from "react-router-dom";
import {
DisableDownloadsToggle,
isoDatetimeToElapsedPopup,
secondsToFrequency,
textEllipsis,
useTitle,
WROLModeMessage
} from "../Common";
import {
Button as SButton,
Confirm,
Loader,
PlaceholderLine,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow
} from "semantic-ui-react";
import {Button, Header, Modal, ModalActions, ModalContent, ModalHeader, Placeholder, Segment, Table} from "../Theme";
import {useDownloads} from "../../hooks/customHooks";
import _ from "lodash";
function ClearCompleteDownloads({callback}) {
const [disabled, setDisabled] = React.useState(false);
async function localClearDownloads() {
setDisabled(true);
try {
await clearCompletedDownloads();
} finally {
setDisabled(false);
if (callback) {
callback()
}
}
}
return <>
<Button
onClick={localClearDownloads}
disabled={disabled}
color='yellow'
>Clear Completed</Button>
</>
}
function ClearFailedDownloads({callback}) {
const [open, setOpen] = React.useState(false);
const [disabled, setDisabled] = React.useState(false);
async function localDeleteFailed() {
setDisabled(true);
try {
await clearFailedDownloads();
} finally {
setDisabled(false);
setOpen(false);
if (callback) {
callback()
}
}
}
return <>
<Button
onClick={() => setOpen(true)}
disabled={disabled}
color='red'
>Clear Failed</Button>
<Confirm
open={open}
content='Are you sure you want to delete failed downloads? They will not be retried.'
confirmButton='Delete'
onCancel={() => setOpen(false)}
onConfirm={localDeleteFailed}
/>
</>
}
class DownloadRow extends React.Component {
constructor(props) {
super(props);
this.state = {
errorModalOpen: false,
deleteOpen: false,
}
}
openDelete = (e) => {
if (e) {
e.preventDefault();
}
this.setState({deleteOpen: true});
}
closeDelete = (e) => {
if (e) {
e.preventDefault();
}
this.setState({deleteOpen: false});
}
handleDelete = async (e) => {
if (e) {
e.preventDefault();
}
const {id} = this.props;
try {
await deleteDownload(id);
} finally {
this.closeDelete();
if (this.props.fetchDownloads) {
this.props.fetchDownloads();
}
}
}
render() {
let {url, frequency, last_successful_download, status, location, next_download, error} = this.props;
const {errorModalOpen, deleteOpen} = this.state;
const link = location ?
(text) => <Link to={location}>{text}</Link> :
(text) => <a href={url} target='_blank' rel='noopener noreferrer'>{text}</a>;
const errorModal = <Modal
closeIcon
onClose={() => this.setState({errorModalOpen: false})}
onOpen={() => this.setState({errorModalOpen: true})}
open={errorModalOpen}
trigger={<Button icon='exclamation circle' color='orange'/>}
>
<ModalHeader>Download Error</ModalHeader>
<ModalContent>
<pre style={{overflowX: 'scroll'}}>{error}</pre>
</ModalContent>
<ModalActions>
<SButton onClick={() => this.setState({errorModalOpen: false})}>Close</SButton>
</ModalActions>
</Modal>;
const deleteButton = <>
<Button icon='trash' onClick={this.openDelete} color='red'/>
<Confirm
open={deleteOpen}
content='Are you sure you want to delete this download?'
confirmButton='Delete'
onCancel={this.closeDelete}
onConfirm={this.handleDelete}
/>
</>;
// Show "now" if we have passed the next_download.
let next = 'now';
if (next_download) {
if (new Date() < new Date(next_download)) {
next = isoDatetimeToElapsedPopup(next_download);
}
}
return <TableRow>
<TableCell>
{link(textEllipsis(url, 50))}
</TableCell>
<TableCell>{secondsToFrequency(frequency)}</TableCell>
<TableCell>
{last_successful_download ? isoDatetimeToElapsedPopup(last_successful_download) : null}
{status === 'pending' ? <Loader active inline size='tiny'/> : null}
</TableCell>
<TableCell>{next}</TableCell>
<TableCell>
{error && errorModal}
{deleteButton}
</TableCell>
</TableRow>
}
}
class StoppableRow extends React.Component {
constructor(props) {
super(props);
this.state = {
stopOpen: false,
startOpen: false,
errorModalOpen: false,
deleteOpen: false,
};
}
openDelete = () => {
this.setState({deleteOpen: true});
}
closeDelete = () => {
this.setState({deleteOpen: false});
}
openStop = () => {
this.setState({stopOpen: true});
}
closeStop = () => {
this.setState({stopOpen: false});
}
openStart = () => {
this.setState({startOpen: true});
}
closeStart = () => {
this.setState({startOpen: false});
}
handleDelete = async (e) => {
e.preventDefault();
await deleteDownload(this.props.id);
this.closeDelete();
await this.props.fetchDownloads();
};
handleStop = async (e) => {
e.preventDefault();
await killDownload(this.props.id);
this.closeStop();
await this.props.fetchDownloads();
};
handleStart = async (e) => {
e.preventDefault();
let downloader = this.props.downloader;
await postDownload(`${this.props.url}`, downloader);
this.closeStart();
await this.props.fetchDownloads();
};
render() {
let {url, last_successful_download, status, location, error} = this.props;
let {stopOpen, startOpen, errorModalOpen, deleteOpen} = this.state;
// Open downloads (/download), or external links in an anchor.
const link = location && !location.startsWith('/download') ?
(text) => <Link to={location}>{text}</Link> :
(text) => <a href={url} target='_blank' rel='noopener noreferrer'>{text}</a>;
let completedAtCell = last_successful_download ? isoDatetimeToElapsedPopup(last_successful_download) : null;
let buttonCell = <TableCell/>;
if (status === 'pending' || status === 'new') {
buttonCell = (
<TableCell>
<Button
onClick={this.openStop}
color='red'
>Stop</Button>
<Confirm
open={stopOpen}
content='Are you sure you want to stop this download? It will not be retried.'
confirmButton='Stop'
onCancel={this.closeStop}
onConfirm={this.handleStop}
/>
</TableCell>
);
} else if (status === 'failed' || status === 'deferred') {
buttonCell = (
<TableCell>
<Button icon='trash' onClick={this.openDelete} color='red'/>
<Confirm
open={deleteOpen}
content='Are you sure you want to delete this download?'
confirmButton='Delete'
onCancel={this.closeDelete}
onConfirm={this.handleDelete}
/>
<Button onClick={this.openStart} color='green'>Retry</Button>
<Confirm
open={startOpen}
content='Are you sure you want to restart this download?'
confirmButton='Start'
onCancel={this.closeStart}
onConfirm={this.handleStart}
/>
</TableCell>
);
} else if (status === 'complete' && location) {
buttonCell = (
<TableCell>
{link('View')}
</TableCell>
);
}
if (error) {
completedAtCell = (
<Modal
closeIcon
onClose={() => this.setState({errorModalOpen: false})}
onOpen={() => this.setState({errorModalOpen: true})}
open={errorModalOpen}
trigger={<Button icon='exclamation circle' color='red'/>}
>
<ModalHeader>Download Error</ModalHeader>
<ModalContent>
<pre style={{overflowX: 'scroll'}}>{error}</pre>
</ModalContent>
<ModalActions>
<SButton onClick={() => this.setState({errorModalOpen: false})}>Close</SButton>
</ModalActions>
</Modal>
)
}
return <TableRow>
<TableCell>
{link(textEllipsis(url, 50))}
</TableCell>
<TableCell>
{completedAtCell}
{status === 'pending' ? <Loader active inline size='tiny'/> : null}
</TableCell>
{buttonCell}
</TableRow>
}
}
export function OnceDownloadsTable({downloads, fetchDownloads}) {
if (downloads && _.isEmpty(downloads)) {
return <Segment>No downloads are scheduled.</Segment>;
} else if (downloads) {
return <>
<ClearCompleteDownloads callback={fetchDownloads}/>
<ClearFailedDownloads callback={fetchDownloads}/>
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>URL</TableHeaderCell>
<TableHeaderCell>Completed At</TableHeaderCell>
<TableHeaderCell>Control</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{downloads.map(i => <StoppableRow fetchDownloads={fetchDownloads} key={i.id} {...i}/>)}
</TableBody>
</Table>
</>;
} else {
return <Placeholder>
<PlaceholderLine/>
<PlaceholderLine/>
</Placeholder>;
}
}
export function RecurringDownloadsTable({downloads, fetchDownloads}) {
if (downloads && _.isEmpty(downloads)) {
return <Segment>No downloads are scheduled.</Segment>;
} else if (downloads) {
return <Table>
<TableHeader>
<TableRow>
<TableHeaderCell>URL</TableHeaderCell>
<TableHeaderCell>Download Frequency</TableHeaderCell>
<TableHeaderCell>Completed At</TableHeaderCell>
<TableHeaderCell>Next</TableHeaderCell>
<TableHeaderCell>Control</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{downloads.map(i => <DownloadRow key={i.id} fetchDownloads={fetchDownloads} {...i}/>)}
</TableBody>
</Table>;
} else {
return <Placeholder>
<PlaceholderLine/>
<PlaceholderLine/>
</Placeholder>;
}
}
export function Downloads() {
useTitle('Downloads');
const {onceDownloads, recurringDownloads, fetchDownloads} = useDownloads();
return <>
<WROLModeMessage content='Downloads are disabled because WROL Mode is enabled.'/>
<DisableDownloadsToggle/>
<Header as='h1'>Downloads</Header>
<OnceDownloadsTable downloads={onceDownloads} fetchDownloads={fetchDownloads}/>
<Header as='h1'>Recurring Downloads</Header>
<RecurringDownloadsTable downloads={recurringDownloads} fetchDownloads={fetchDownloads}/>
</>
}