mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
520 lines
16 KiB
JavaScript
520 lines
16 KiB
JavaScript
import React from "react";
|
|
import {
|
|
clearCompletedDownloads,
|
|
deleteDownload,
|
|
deleteOnceDownloads,
|
|
killDownload,
|
|
restartDownload,
|
|
retryOnceDownloads
|
|
} from "../../api";
|
|
import {Link} from "react-router";
|
|
import {
|
|
APIButton,
|
|
DisableDownloadsToggle,
|
|
ErrorMessage,
|
|
formatFrequency,
|
|
isoDatetimeToElapsedPopup,
|
|
useTitle,
|
|
WROLModeMessage
|
|
} from "../Common";
|
|
import {
|
|
Button as SButton,
|
|
ButtonGroup,
|
|
Label,
|
|
Loader,
|
|
PlaceholderLine,
|
|
TableBody,
|
|
TableCell,
|
|
TableFooter,
|
|
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 {
|
|
EditArchiveDownloadForm,
|
|
EditChannelDownloadForm,
|
|
EditRSSDownloadForm,
|
|
EditScrapeFilesDownloadForm,
|
|
EditVideosDownloadForm,
|
|
EditZimDownloadForm
|
|
} from "../Download";
|
|
import {Downloaders} from "../Vars";
|
|
|
|
function ClearDownloadsButton({callback}) {
|
|
async function localClearDownloads() {
|
|
try {
|
|
await clearCompletedDownloads();
|
|
} finally {
|
|
if (callback) {
|
|
callback()
|
|
}
|
|
}
|
|
}
|
|
|
|
return <>
|
|
<APIButton
|
|
onClick={localClearDownloads}
|
|
color='violet'
|
|
obeyWROLMode={true}
|
|
>Clear</APIButton>
|
|
</>
|
|
}
|
|
|
|
function RetryDownloadsButton({callback}) {
|
|
async function localRetryOnce() {
|
|
try {
|
|
await retryOnceDownloads();
|
|
} finally {
|
|
if (callback) {
|
|
callback()
|
|
}
|
|
}
|
|
}
|
|
|
|
return <APIButton
|
|
color='green'
|
|
onClick={localRetryOnce}
|
|
obeyWROLMode={true}
|
|
>Retry</APIButton>
|
|
}
|
|
|
|
function DeleteOnceDownloadsButton({callback}) {
|
|
async function localDeleteOnce() {
|
|
try {
|
|
await deleteOnceDownloads();
|
|
} finally {
|
|
if (callback) {
|
|
callback()
|
|
}
|
|
}
|
|
}
|
|
|
|
return <APIButton
|
|
color='red'
|
|
onClick={localDeleteOnce}
|
|
confirmContent='Are you sure you want to delete all downloads? Some may be retried!'
|
|
confirmButton='Delete'
|
|
obeyWROLMode={true}
|
|
>Delete</APIButton>
|
|
}
|
|
|
|
|
|
function RecurringDownloadRow({download, fetchDownloads, onDelete}) {
|
|
const [errorModalOpen, setErrorModalOpen] = React.useState(false);
|
|
const [editModalOpen, setEditModalOpen] = React.useState(false);
|
|
|
|
const handleRestart = async () => {
|
|
const {id} = download;
|
|
try {
|
|
await restartDownload(id);
|
|
} finally {
|
|
if (fetchDownloads) {
|
|
await fetchDownloads();
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleEditOpen = () => setEditModalOpen(true);
|
|
const handleEditClose = () => setEditModalOpen(false);
|
|
const handleErrorOpen = () => setErrorModalOpen(true);
|
|
const handleErrorClose = () => setErrorModalOpen(false);
|
|
|
|
let {
|
|
url,
|
|
frequency,
|
|
last_successful_download,
|
|
status,
|
|
location,
|
|
next_download,
|
|
error,
|
|
downloader,
|
|
} = download;
|
|
|
|
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={handleErrorClose}
|
|
onOpen={handleErrorOpen}
|
|
open={errorModalOpen}
|
|
trigger={<Button icon='exclamation circle' color='orange'/>}
|
|
>
|
|
<ModalHeader>Download Error</ModalHeader>
|
|
<ModalContent>
|
|
<pre style={{overflowX: 'scroll'}}>{error}</pre>
|
|
</ModalContent>
|
|
<ModalActions>
|
|
<SButton onClick={handleEditClose}>Close</SButton>
|
|
</ModalActions>
|
|
</Modal>;
|
|
|
|
const editButton = <Button icon='edit' onClick={handleEditOpen}/>;
|
|
|
|
const restartButton = <APIButton
|
|
color='green'
|
|
icon='redo'
|
|
confirmContent='Are you sure you want to restart this download?'
|
|
confirmButton='Restart'
|
|
onClick={handleRestart}
|
|
obeyWROLMode={true}
|
|
/>;
|
|
|
|
// Show "now" if we have passed the next_download.
|
|
let next = 'now';
|
|
if (next_download && new Date() < new Date(next_download)) {
|
|
next = isoDatetimeToElapsedPopup(next_download);
|
|
}
|
|
|
|
const onSuccess = async () => {
|
|
if (fetchDownloads) {
|
|
await fetchDownloads();
|
|
}
|
|
handleEditClose();
|
|
}
|
|
|
|
const localOnDelete = async () => {
|
|
try {
|
|
await deleteDownload(download.id);
|
|
await onDelete();
|
|
} finally {
|
|
await fetchDownloads();
|
|
handleEditClose();
|
|
}
|
|
}
|
|
|
|
let editForm;
|
|
if (downloader === Downloaders.VideoChannel) {
|
|
editForm = <EditChannelDownloadForm
|
|
download={download}
|
|
onCancel={handleEditClose}
|
|
onSuccess={onSuccess}
|
|
onDelete={localOnDelete}
|
|
/>;
|
|
} else if (downloader === Downloaders.RSS) {
|
|
editForm = <EditRSSDownloadForm
|
|
download={download}
|
|
onDelete={localOnDelete}
|
|
onCancel={handleEditClose}
|
|
onSuccess={onSuccess}
|
|
/>;
|
|
} else if (downloader === Downloaders.KiwixCatalog) {
|
|
editForm = <EditZimDownloadForm
|
|
download={download}
|
|
onDelete={localOnDelete}
|
|
onCancel={handleEditClose}
|
|
onSuccess={onSuccess}
|
|
/>;
|
|
} else if (downloader === Downloaders.ScrapeHtml) {
|
|
editForm = <EditScrapeFilesDownloadForm
|
|
download={download}
|
|
onDelete={localOnDelete}
|
|
onCancel={handleEditClose}
|
|
onSuccess={onSuccess}
|
|
/>;
|
|
}
|
|
|
|
const editModal = <Modal closeIcon
|
|
open={editModalOpen}
|
|
onClose={handleEditClose}
|
|
>
|
|
<ModalHeader>Edit Download</ModalHeader>
|
|
<ModalContent>
|
|
{editForm}
|
|
</ModalContent>
|
|
</Modal>;
|
|
|
|
return <TableRow>
|
|
<TableCell className='column-ellipsis'>
|
|
{link(url)}
|
|
</TableCell>
|
|
<TableCell>{formatFrequency(frequency)}</TableCell>
|
|
<TableCell>
|
|
{last_successful_download && isoDatetimeToElapsedPopup(last_successful_download)}
|
|
{status === 'pending' && <Loader active inline size='tiny'/>}
|
|
</TableCell>
|
|
<TableCell>{next}</TableCell>
|
|
<TableCell textAlign='right'>
|
|
{error && errorModal}
|
|
{editButton}
|
|
{restartButton}
|
|
</TableCell>
|
|
{editModal}
|
|
</TableRow>
|
|
}
|
|
|
|
class OnceDownloadRow extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
editModalOpen: false
|
|
};
|
|
}
|
|
|
|
handleDelete = async () => {
|
|
try {
|
|
await deleteDownload(this.props.id);
|
|
} finally {
|
|
await this.props.fetchDownloads();
|
|
}
|
|
};
|
|
|
|
handleStop = async () => {
|
|
try {
|
|
await killDownload(this.props.id);
|
|
} finally {
|
|
await this.props.fetchDownloads();
|
|
}
|
|
};
|
|
|
|
handleRestart = async () => {
|
|
try {
|
|
await restartDownload(this.props.id);
|
|
} finally {
|
|
await this.props.fetchDownloads();
|
|
}
|
|
};
|
|
|
|
handleEditOpen = () => {
|
|
this.setState({editModalOpen: true});
|
|
};
|
|
|
|
handleEditClose = () => {
|
|
this.setState({editModalOpen: false});
|
|
};
|
|
|
|
handleEditSuccess = async () => {
|
|
await this.props.fetchDownloads();
|
|
this.handleEditClose();
|
|
};
|
|
|
|
render() {
|
|
let {url, last_successful_download, status, location, error, downloader, settings, id} = this.props;
|
|
|
|
// Open downloads (/download), or external links in an anchor.
|
|
const link = location && !location.startsWith('/download') ?
|
|
(text) => <Link to={location}>{text}</Link> :
|
|
(text) => <a href={location || 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>
|
|
<APIButton
|
|
color='red'
|
|
onClick={this.handleStop}
|
|
confirmContent='Are you sure you want to stop this download? It will not be retried.'
|
|
confirmButton='Stop'
|
|
obeyWROLMode={true}
|
|
>Stop</APIButton>
|
|
</TableCell>
|
|
);
|
|
} else if (status === 'failed' || status === 'deferred') {
|
|
buttonCell = (
|
|
<TableCell>
|
|
<ButtonGroup>
|
|
|
|
<APIButton
|
|
color='red'
|
|
icon='trash'
|
|
onClick={this.handleDelete}
|
|
confirmContent='Are you sure you want to delete this download?'
|
|
confirmButton='Delete'
|
|
obeyWROLMode={true}
|
|
/>
|
|
{(downloader === Downloaders.Video || downloader === Downloaders.Archive) && status !== 'pending' && (
|
|
<Button
|
|
icon='edit'
|
|
color='blue'
|
|
onClick={this.handleEditOpen}
|
|
/>
|
|
)}
|
|
<APIButton
|
|
color='green'
|
|
icon='redo'
|
|
confirmContent='Are you sure you want to restart this download?'
|
|
confirmButton='Start'
|
|
onClick={this.handleRestart}
|
|
obeyWROLMode={true}
|
|
/>
|
|
</ButtonGroup>
|
|
</TableCell>
|
|
);
|
|
} else if (status === 'complete' && location) {
|
|
buttonCell = (
|
|
<TableCell>
|
|
{link('View')}
|
|
</TableCell>
|
|
);
|
|
}
|
|
if (error) {
|
|
completedAtCell = (
|
|
<Modal
|
|
closeIcon
|
|
trigger={<Button icon='exclamation circle' color='red'/>}
|
|
>
|
|
<ModalHeader>Download Error</ModalHeader>
|
|
<ModalContent>
|
|
<pre style={{overflowX: 'scroll'}}>{error}</pre>
|
|
</ModalContent>
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
// Create edit modal for video or archive downloads
|
|
let editModal = null;
|
|
|
|
if (downloader === Downloaders.Video) {
|
|
editModal = (
|
|
<Modal
|
|
closeIcon
|
|
open={this.state.editModalOpen}
|
|
onClose={this.handleEditClose}
|
|
>
|
|
<ModalHeader>Edit Video Download</ModalHeader>
|
|
<ModalContent>
|
|
<EditVideosDownloadForm
|
|
download={{
|
|
urls: url,
|
|
settings: settings || {},
|
|
id: id
|
|
}}
|
|
onCancel={this.handleEditClose}
|
|
onSuccess={this.handleEditSuccess}
|
|
onDelete={this.handleDelete}
|
|
/>
|
|
</ModalContent>
|
|
</Modal>
|
|
);
|
|
} else if (downloader === Downloaders.Archive) {
|
|
editModal = (
|
|
<Modal
|
|
closeIcon
|
|
open={this.state.editModalOpen}
|
|
onClose={this.handleEditClose}
|
|
>
|
|
<ModalHeader>Edit Archive Download</ModalHeader>
|
|
<ModalContent>
|
|
<EditArchiveDownloadForm
|
|
download={{
|
|
urls: url,
|
|
tag_names: settings?.tag_names || [],
|
|
id: id
|
|
}}
|
|
onCancel={this.handleEditClose}
|
|
onSuccess={this.handleEditSuccess}
|
|
onDelete={this.handleDelete}
|
|
/>
|
|
</ModalContent>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
return <TableRow>
|
|
<TableCell className='column-ellipsis'>
|
|
{link(url)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{completedAtCell}
|
|
{status === 'pending' ? <Loader active inline size='tiny'/> : null}
|
|
</TableCell>
|
|
{buttonCell}
|
|
{editModal}
|
|
</TableRow>
|
|
}
|
|
}
|
|
|
|
export function OnceDownloadsTable({downloads, fetchDownloads}) {
|
|
if (downloads && downloads.length >= 1) {
|
|
return <>
|
|
<Table className='table-ellipsis'>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHeaderCell width={11}>URL</TableHeaderCell>
|
|
<TableHeaderCell width={2}>Completed At</TableHeaderCell>
|
|
<TableHeaderCell width={2}>Control</TableHeaderCell>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{downloads.map(i => <OnceDownloadRow fetchDownloads={fetchDownloads} key={i.id} {...i}/>)}
|
|
</TableBody>
|
|
<TableFooter>
|
|
<TableRow>
|
|
<TableHeaderCell colSpan={3}>
|
|
<ClearDownloadsButton callback={fetchDownloads}/>
|
|
<RetryDownloadsButton callback={fetchDownloads}/>
|
|
<DeleteOnceDownloadsButton callback={fetchDownloads}/>
|
|
</TableHeaderCell>
|
|
</TableRow>
|
|
</TableFooter>
|
|
</Table>
|
|
</>
|
|
} else if (downloads) {
|
|
return <Segment>No downloads are scheduled.</Segment>
|
|
} else if (downloads === undefined) {
|
|
return <ErrorMessage>Unable to fetch downloads</ErrorMessage>
|
|
}
|
|
return <Placeholder>
|
|
<PlaceholderLine/>
|
|
<PlaceholderLine/>
|
|
</Placeholder>
|
|
}
|
|
|
|
export function RecurringDownloadsTable({downloads, fetchDownloads, onDelete}) {
|
|
if (downloads && downloads.length >= 1) {
|
|
return <Table compact className='table-ellipsis'>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHeaderCell width={8}>URL</TableHeaderCell>
|
|
<TableHeaderCell width={2}>Download Frequency</TableHeaderCell>
|
|
<TableHeaderCell width={2}>Completed At</TableHeaderCell>
|
|
<TableHeaderCell width={1}>Next</TableHeaderCell>
|
|
<TableHeaderCell width={3} textAlign='right'>Control</TableHeaderCell>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{downloads.map(i => {
|
|
return <RecurringDownloadRow
|
|
key={i.id}
|
|
fetchDownloads={fetchDownloads}
|
|
download={i}
|
|
onDelete={onDelete}
|
|
/>
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
} else if (downloads) {
|
|
return <Segment>No downloads are scheduled.</Segment>
|
|
} else if (downloads === undefined) {
|
|
return <ErrorMessage>Unable to fetch downloads</ErrorMessage>
|
|
}
|
|
return <Placeholder>
|
|
<PlaceholderLine/>
|
|
<PlaceholderLine/>
|
|
</Placeholder>
|
|
}
|
|
|
|
export function DownloadsPage() {
|
|
useTitle('Downloads');
|
|
|
|
const {onceDownloads, recurringDownloads, pendingOnceDownloads, fetchDownloads} = useDownloads();
|
|
|
|
const pendingOnceDownloadsSpan = pendingOnceDownloads > 0 ?
|
|
<Label color='violet' size='large'>{pendingOnceDownloads}</Label>
|
|
: null;
|
|
|
|
return <>
|
|
<WROLModeMessage content='Downloads are disabled because WROL Mode is enabled.'/>
|
|
<DisableDownloadsToggle/>
|
|
|
|
<Header as='h1'>Downloads {pendingOnceDownloadsSpan}</Header>
|
|
<OnceDownloadsTable downloads={onceDownloads} fetchDownloads={fetchDownloads}/>
|
|
|
|
<Header as='h1'>Recurring Downloads</Header>
|
|
<RecurringDownloadsTable downloads={recurringDownloads} fetchDownloads={fetchDownloads}/>
|
|
</>
|
|
}
|