Add Progressbar to jobs

Add Cancel-Buttons to running jobs
Auto-update Queue
This commit is contained in:
glax 2024-10-20 21:20:18 +02:00
parent f3a091f09d
commit 7364a20d5d
7 changed files with 114 additions and 28 deletions

View File

@ -10,7 +10,7 @@ export default function Footer({connectedToBackend, apiUri} : {connectedToBacken
const [AllJobsCount, setAllJobsCount] = React.useState(0); const [AllJobsCount, setAllJobsCount] = React.useState(0);
const [RunningJobsCount, setRunningJobsCount] = React.useState(0); const [RunningJobsCount, setRunningJobsCount] = React.useState(0);
const [StandbyJobsCount, setStandbyJobsCount] = React.useState(0); const [StandbyJobsCount, setStandbyJobsCount] = React.useState(0);
const [countUpdateInterval, setcountUpdateInterval] = React.useState<number>(); const [countUpdateInterval, setCountUpdateInterval] = React.useState<number>();
function UpdateBackendState(){ function UpdateBackendState(){
Job.GetMonitoringJobs(apiUri).then((jobs) => setMonitoringJobsCount(jobs.length)); Job.GetMonitoringJobs(apiUri).then((jobs) => setMonitoringJobsCount(jobs.length));
@ -22,19 +22,19 @@ export default function Footer({connectedToBackend, apiUri} : {connectedToBacken
useEffect(() => { useEffect(() => {
if(connectedToBackend){ if(connectedToBackend){
UpdateBackendState(); UpdateBackendState();
setcountUpdateInterval(setInterval(() => { setCountUpdateInterval(setInterval(() => {
UpdateBackendState(); UpdateBackendState();
}, 2000)); }, 2000));
}else{ }else{
clearInterval(countUpdateInterval); clearInterval(countUpdateInterval);
setcountUpdateInterval(undefined); setCountUpdateInterval(undefined);
} }
}, [connectedToBackend]); }, [connectedToBackend]);
return ( return (
<footer> <footer>
<div className="statusBadge"><Icon path={mdiEyeCheck} size={1}/> <span>{MonitoringJobsCount}</span></div> <div className="statusBadge"><Icon path={mdiEyeCheck} size={1}/> <span>{MonitoringJobsCount}</span></div>
<QueuePopUp apiUri={apiUri}> <QueuePopUp connectedToBackend={connectedToBackend} apiUri={apiUri}>
<div className="statusBadge hoverHand"><Icon path={mdiTrayFull} size={1}/> <span>{StandbyJobsCount}</span></div> <div className="statusBadge hoverHand"><Icon path={mdiTrayFull} size={1}/> <span>{StandbyJobsCount}</span></div>
<div className="statusBadge hoverHand"><Icon path={mdiRun} size={1}/> <span>{RunningJobsCount}</span></div> <div className="statusBadge hoverHand"><Icon path={mdiRun} size={1}/> <span>{RunningJobsCount}</span></div>
</QueuePopUp> </QueuePopUp>

View File

@ -1,20 +1,39 @@
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import IJob, {JobTypeFromNumber} from "./interfaces/IJob"; import IJob from "./interfaces/IJob";
import '../styles/queuePopUp.css'; import '../styles/queuePopUp.css';
import '../styles/popup.css'; import '../styles/popup.css';
import {Job} from "./Job"; import {Job} from "./Job";
import IManga from "./interfaces/IManga"; import IManga, {QueueItem} from "./interfaces/IManga";
import {Manga} from "./Manga"; import {Manga} from "./Manga";
export default function QueuePopUp({children, apiUri} : {children: JSX.Element[], apiUri: string}) { export default function QueuePopUp({connectedToBackend, children, apiUri} : {connectedToBackend: boolean, children: JSX.Element[], apiUri: string}) {
const [StandbyJobs, setStandbyJobs] = React.useState<IJob[]>([]); const [StandbyJobs, setStandbyJobs] = React.useState<IJob[]>([]);
const [StandbyJobsManga, setStandbyJobsManga] = React.useState<IManga[]>([]); const [StandbyJobsManga, setStandbyJobsManga] = React.useState<IManga[]>([]);
const [RunningJobs, setRunningJobs] = React.useState<IJob[]>([]); const [RunningJobs, setRunningJobs] = React.useState<IJob[]>([]);
const [RunningJobsManga, setRunningJobsManga] = React.useState<IManga[]>([]); const [RunningJobsManga, setRunningJobsManga] = React.useState<IManga[]>([]);
const [showQueuePopup, setShowQueuePopup] = useState<boolean>(false); const [showQueuePopup, setShowQueuePopup] = useState<boolean>(false);
const [queueListInterval, setQueueListInterval] = React.useState<number>();
useEffect(() => { useEffect(() => {
if(!showQueuePopup)
return;
UpdateMonitoringJobsList();
}, [showQueuePopup]);
useEffect(() => {
if(connectedToBackend){
UpdateMonitoringJobsList();
setQueueListInterval(setInterval(() => {
UpdateMonitoringJobsList();
}, 2000));
}else{
clearInterval(queueListInterval);
setQueueListInterval(undefined);
}
}, [connectedToBackend]);
function UpdateMonitoringJobsList(){
Job.GetStandbyJobs(apiUri) Job.GetStandbyJobs(apiUri)
.then((jobs) => { .then((jobs) => {
if(jobs.length > 0) if(jobs.length > 0)
@ -37,7 +56,7 @@ export default function QueuePopUp({children, apiUri} : {children: JSX.Element[]
//console.debug("Removing Metadata Jobs"); //console.debug("Removing Metadata Jobs");
setRunningJobs(jobs.filter(job => job.jobType <= 1)); setRunningJobs(jobs.filter(job => job.jobType <= 1));
}); });
}, []); }
useEffect(() => { useEffect(() => {
if(StandbyJobs.length < 1) if(StandbyJobs.length < 1)
@ -65,7 +84,7 @@ export default function QueuePopUp({children, apiUri} : {children: JSX.Element[]
{showQueuePopup {showQueuePopup
? <div className="popup" id="QueuePopUp"> ? <div className="popup" id="QueuePopUp">
<div className="popupHeader"> <div className="popupHeader">
<h1>Queue Status {showQueuePopup ? "true" : "false"}</h1> <h1>Queue Status</h1>
<img alt="Close Search" className="close" src="../media/close-x.svg" <img alt="Close Search" className="close" src="../media/close-x.svg"
onClick={() => setShowQueuePopup(false)}/> onClick={() => setShowQueuePopup(false)}/>
</div> </div>
@ -76,12 +95,8 @@ export default function QueuePopUp({children, apiUri} : {children: JSX.Element[]
{RunningJobs.map((job: IJob) => { {RunningJobs.map((job: IJob) => {
const manga = RunningJobsManga.find(manga => manga.internalId == job.mangaInternalId || manga.internalId == job.chapter?.parentManga.internalId); const manga = RunningJobsManga.find(manga => manga.internalId == job.mangaInternalId || manga.internalId == job.chapter?.parentManga.internalId);
if (manga === undefined || manga === null) if (manga === undefined || manga === null)
return <div key={"QueueJob-" + job.id}>Error. Could not find matching manga return <div key={"QueueJob-" + job.id}>Error. Could not find matching manga for {job.id}</div>
for {job.id}</div> return QueueItem(apiUri, manga, job, UpdateMonitoringJobsList);
return <div className="QueueJob" key={"QueueJob-" + job.id}>
<img src={Manga.GetMangaCoverUrl(apiUri, manga.internalId)} alt="Manga Cover" />
<p>{JobTypeFromNumber(job.jobType)}</p>
</div>;
})} })}
</div> </div>
</div> </div>
@ -91,14 +106,8 @@ export default function QueuePopUp({children, apiUri} : {children: JSX.Element[]
{StandbyJobs.map((job: IJob) => { {StandbyJobs.map((job: IJob) => {
const manga = StandbyJobsManga.find(manga => manga.internalId == job.mangaInternalId || manga.internalId == job.chapter?.parentManga.internalId); const manga = StandbyJobsManga.find(manga => manga.internalId == job.mangaInternalId || manga.internalId == job.chapter?.parentManga.internalId);
if (manga === undefined || manga === null) if (manga === undefined || manga === null)
return <div key={"QueueJob-" + job.id}>Error. Could not find matching manga return <div key={"QueueJob-" + job.id}>Error. Could not find matching manga for {job.id}</div>
for {job.id}</div> return QueueItem(apiUri, manga, job, UpdateMonitoringJobsList);
return <div className="QueueJob" key={"QueueJob-" + job.id}>
<img src={Manga.GetMangaCoverUrl(apiUri, manga.internalId)} alt="Manga Cover" />
<p className="QueueJob-Name">{manga.sortName}</p>
<p className="QueueJob-JobType">{JobTypeFromNumber(job.jobType)}</p>
<p className="QueueJob-additional">{job.jobType == 0 ? `Vol.${job.chapter?.volumeNumber} Ch.${job.chapter?.chapterNumber}` : ""}</p>
</div>;
})} })}
</div> </div>
</div> </div>

View File

@ -5,6 +5,9 @@ import React, {ReactElement} from "react";
import Icon from '@mdi/react'; import Icon from '@mdi/react';
import { mdiTagTextOutline, mdiAccountEdit } from '@mdi/js'; import { mdiTagTextOutline, mdiAccountEdit } from '@mdi/js';
import MarkdownPreview from '@uiw/react-markdown-preview'; import MarkdownPreview from '@uiw/react-markdown-preview';
import IJob, {JobTypeFromNumber} from "./IJob";
import {Job} from "../Job";
import ProgressBar from "@ramonak/react-progress-bar";
export default interface IManga{ export default interface IManga{
"sortName": string, "sortName": string,
@ -68,4 +71,33 @@ export function SearchResult(apiUri: string, manga: IManga, createJob: (internal
}}>Monitor }}>Monitor
</button> </button>
</div>); </div>);
}
function ProgressbarStr(job: IJob): string {
return job.progressToken.timeRemaining.substring(0,job.progressToken.timeRemaining.indexOf(".")).concat(" ", ToPercentString(job.progressToken.progress));
}
function ToPercentString(n: number): string {
return n.toString().substring(2,4).concat("%");
}
export function QueueItem(apiUri: string, manga: IManga, job: IJob, triggerUpdate: () => void){
return (
<div className="QueueJob" key={"QueueJob-" + job.id}>
<img src={Manga.GetMangaCoverUrl(apiUri, manga.internalId)} alt="Manga Cover"/>
<p className="QueueJob-Name">{manga.sortName}</p>
<p className="QueueJob-JobType">{JobTypeFromNumber(job.jobType)}</p>
<p className="QueueJob-additional">{job.jobType == 0 ? `Vol.${job.chapter?.volumeNumber} Ch.${job.chapter?.chapterNumber}` : ""}</p>
{job.progressToken.state === 0
? <ProgressBar labelColor={"#000"} height={"10px"} labelAlignment={"outside"} className="QueueJob-Progressbar" completed={job.progressToken.progress} maxCompleted={1} customLabel={ProgressbarStr(job)}/>
: <div className="QueueJob-Progressbar"></div>}
<div className="QueueJob-actions">
<button className="QueueJob-Cancel" onClick={() => Job.CancelJob(apiUri, job.id).then(triggerUpdate)}>Cancel</button>
{job.parentJobId != null
? <button className="QueueJob-Cancel" onClick={() => Job.CancelJob(apiUri, job.parentJobId!).then(triggerUpdate)}>Cancel all related</button>
: <></>
}
</div>
</div>
);
} }

View File

@ -5,6 +5,17 @@ export default interface IProgressToken{
progress: number; progress: number;
lastUpdate: Date; lastUpdate: Date;
executionStarted: Date; executionStarted: Date;
timeRemaining: Date; timeRemaining: string;
state: number; state: number;
}
export function GetProgressStateFromNumber(n: number): string {
switch (n){
case 0: return "Running";
case 1: return "Complete";
case 2: return "Standby";
case 3: return "Cancelled";
case 4: return "Waiting";
}
return "";
} }

View File

@ -12,6 +12,7 @@
#QueuePopUp #QueuePopUpBody h1 { #QueuePopUp #QueuePopUpBody h1 {
padding: 0; padding: 0;
margin: 0 0 5px 0; margin: 0 0 5px 0;
color: var(--primary-color);
} }
#QueuePopUp #QueuePopUpBody > *:first-child { #QueuePopUp #QueuePopUpBody > *:first-child {
@ -30,12 +31,18 @@
height: 200px; height: 200px;
display: grid; display: grid;
grid-template-columns: 150px auto; grid-template-columns: 150px auto;
grid-template-rows: 30% 30% auto; grid-template-rows: 25% 20% auto 15% 12%;
column-gap: 10px; column-gap: 10px;
grid-template-areas: grid-template-areas:
"cover name" "cover name"
"cover jobType" "cover jobType"
"cover additional" "cover additionalInfo"
"cover progress"
"cover actions"
}
.QueueJob p {
margin: 2px 0;
} }
.QueueJob img{ .QueueJob img{
@ -54,6 +61,21 @@
grid-area: jobType; grid-area: jobType;
} }
.QueueJob .QueueJob-additional { .QueueJob .QueueJob-additionalInfo {
grid-area: additional; grid-area: additionalInfo;
}
.QueueJob .QueueJob-actions {
grid-area: actions;
display: flex;
justify-content: space-evenly;
}
.QueueJob .QueueJob-Cancel {
grid-area: actions;
width: 150px;
}
.QueueJob .QueueJob-Progressbar {
grid-area: progress;
} }

11
package-lock.json generated
View File

@ -5,6 +5,7 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@ramonak/react-progress-bar": "^5.3.0",
"@uiw/react-markdown-preview": "^5.1.3" "@uiw/react-markdown-preview": "^5.1.3"
}, },
"devDependencies": { "devDependencies": {
@ -438,6 +439,16 @@
"prop-types": "^15.7.2" "prop-types": "^15.7.2"
} }
}, },
"node_modules/@ramonak/react-progress-bar": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@ramonak/react-progress-bar/-/react-progress-bar-5.3.0.tgz",
"integrity": "sha512-PjpOcSBAVSQNyx2cvYyBCI14Tg2eFM0psC9m2ic33PYBIdOzO9/DieWndq9BUQTSjIIarhSpa/lqJ33W/mFJMw==",
"license": "MIT",
"peerDependencies": {
"react": "^16.0.0 || ^17 || ^18",
"react-dom": "^16.0.0 || ^17 || ^18"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.24.0", "version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",

View File

@ -15,6 +15,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@ramonak/react-progress-bar": "^5.3.0",
"@uiw/react-markdown-preview": "^5.1.3" "@uiw/react-markdown-preview": "^5.1.3"
} }
} }