Created standardized Popup-Window

Moved Update-functions for Queue-Status and Monitoring-list to their respective modules
This commit is contained in:
glax 2024-10-20 17:27:02 +02:00
parent 1304bc750a
commit fcc1ff392c
10 changed files with 166 additions and 135 deletions

View File

@ -1,10 +1,11 @@
import React, {EventHandler, useEffect} from 'react';
import React, {useEffect, useState} from 'react';
import Footer from "./modules/Footer";
import Search from "./modules/Search";
import Header from "./modules/Header";
import MonitorJobsList from "./modules/MonitorJobsList";
import './styles/index.css'
import QueuePopUp from "./modules/QueuePopUp";
import {Job} from "./modules/Job";
import IFrontendSettings from "./modules/interfaces/IFrontendSettings";
export default function App(){
const [connected, setConnected] = React.useState(false);
@ -15,35 +16,14 @@ export default function App(){
const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number>();
useEffect(() => {
checkConnection();
checkConnection().then(res => setConnected(res)).catch(() => setConnected(false));
setInterval(() => {
checkConnection();
checkConnection().then(res => setConnected(res)).catch(() => setConnected(false));
}, 500);
}, []);
const checkConnection = () =>{
getData('http://127.0.0.1:6531/v2/Ping').then((result) => {
setConnected(result != null);
}).catch(() => setConnected(false));
}
useEffect(() => {
if(connected){
setLastJobListUpdate(new Date());
setJoblistUpdateInterval(setInterval(() => {
setLastJobListUpdate(new Date());
}, 5000));
}else{
clearInterval(joblistUpdateInterval);
setJoblistUpdateInterval(undefined);
}
}, [connected]);
const JobsChanged : EventHandler<any> = () => {
setLastMangaListUpdate(new Date());
setLastJobListUpdate(new Date());
function CreateJob(internalId: string, jobType: string){
Job.CreateJobDateInterval(internalId, jobType, frontendSettings.jobInterval);
}
return(<div>
@ -52,18 +32,14 @@ export default function App(){
? <>
{showSearch
? <>
<Search onJobsChanged={JobsChanged} closeSearch={() => setShowSearch(false)} />
<Search createJob={CreateJob} closeSearch={() => setShowSearch(false)} />
<hr/>
</>
: <></>}
{showQueue
? <QueuePopUp closeQueue={() => setShowQueue(false)} />
: <></>
}
<MonitorJobsList onStartSearch={() => setShowSearch(true)} onJobsChanged={JobsChanged} key={lastMangaListUpdate.getTime()}/>
<MonitorJobsList onStartSearch={() => setShowSearch(true)} onJobsChanged={() => console.info("jobsChanged")} connectedToBackend={connected} />
</>
: <h1>No connection to backend</h1>}
<Footer key={lastJobListUpdate.getTime()} showQueue={() => setShowQueue(true)}/>
<Footer connectedToBackend={connected} />
</div>)
}
@ -134,3 +110,9 @@ export function isValidUri(uri: string) : boolean{
return false;
}
}
export const checkConnection = async (): Promise<boolean> =>{
return getData('http://127.0.0.1:6531/v2/Ping').then((result) => {
return result != null;
}).catch(() => Promise.reject());
}

View File

@ -3,12 +3,14 @@ import '../styles/footer.css';
import {Job} from './Job';
import Icon from '@mdi/react';
import { mdiRun, mdiCounter, mdiEyeCheck, mdiTrayFull } from '@mdi/js';
import QueuePopUp from "./QueuePopUp";
export default function Footer({showQueue} : {showQueue(): void}){
export default function Footer({connectedToBackend} : {connectedToBackend: boolean}) {
const [MonitoringJobsCount, setMonitoringJobsCount] = React.useState(0);
const [AllJobsCount, setAllJobsCount] = React.useState(0);
const [RunningJobsCount, setRunningJobsCount] = React.useState(0);
const [StandbyJobsCount, setStandbyJobsCount] = React.useState(0);
const [countUpdateInterval, setcountUpdateInterval] = React.useState<number>();
function UpdateBackendState(){
Job.GetMonitoringJobs().then((jobs) => setMonitoringJobsCount(jobs.length));
@ -18,15 +20,25 @@ export default function Footer({showQueue} : {showQueue(): void}){
}
useEffect(() => {
if(connectedToBackend){
UpdateBackendState();
}, []);
setcountUpdateInterval(setInterval(() => {
UpdateBackendState();
}, 2000));
}else{
clearInterval(countUpdateInterval);
setcountUpdateInterval(undefined);
}
}, [connectedToBackend]);
return (
<footer>
<div><Icon path={mdiEyeCheck} size={1}/> <span>{MonitoringJobsCount}</span></div>
<div onClick={showQueue} className="hoverHand"><Icon path={mdiTrayFull} size={1}/> <span>{StandbyJobsCount}</span></div>
<div onClick={showQueue} className="hoverHand"><Icon path={mdiRun} size={1}/> <span>{RunningJobsCount}</span></div>
<div><Icon path={mdiCounter} size={1}/> <span>{AllJobsCount}</span></div>
<div className="statusBadge"><Icon path={mdiEyeCheck} size={1}/> <span>{MonitoringJobsCount}</span></div>
<QueuePopUp>
<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>
</QueuePopUp>
<div className="statusBadge"><Icon path={mdiCounter} size={1}/> <span>{AllJobsCount}</span></div>
<p id="madeWith">Made with Blåhaj 🦈</p>
</footer>)
}

View File

@ -4,6 +4,10 @@ import IProgressToken from "./interfaces/IProgressToken";
export class Job
{
static IntervalStringFromDate(date: Date) : string {
return `${date.getDay()}.${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
}
static async GetAllJobs(): Promise<string[]> {
console.info("Getting all Jobs");
return getData("http://127.0.0.1:6531/v2/Jobs")
@ -101,9 +105,18 @@ export class Job
});
}
static async CreateJobDateInterval(internalId: string, jobType: string, interval: Date) : Promise<null> {
return this.CreateJob(internalId, jobType, this.IntervalStringFromDate(interval));
}
static async CreateJob(internalId: string, jobType: string, interval: string): Promise<null> {
const validate = /(?:[0-9]{1,2}\.)?[0-9]{1,2}:[0-9]{1,2}(?::[0-9]{1,2})?/
console.info(`Creating Job for Manga ${internalId} at ${interval} interval`);
let data = {
if(!validate.test(interval)){
console.error("Interval was in incorrect format.");
return Promise.reject();
}
const data = {
internalId: internalId,
interval: interval
};

View File

@ -8,9 +8,10 @@ import '../styles/MangaCoverCard.css'
import Icon from '@mdi/react';
import { mdiTrashCanOutline, mdiPlayBoxOutline } from '@mdi/js';
export default function MonitorJobsList({onStartSearch, onJobsChanged} : {onStartSearch() : void, onJobsChanged: EventHandler<any>}) {
export default function MonitorJobsList({onStartSearch, onJobsChanged, connectedToBackend} : {onStartSearch() : void, onJobsChanged: EventHandler<any>, connectedToBackend: boolean}) {
const [MonitoringJobs, setMonitoringJobs] = useState<IJob[]>([]);
const [AllManga, setAllManga] = useState<IManga[]>([]);
const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number>();
useEffect(() => {
console.debug("Updating display list.");
@ -28,8 +29,16 @@ export default function MonitorJobsList({onStartSearch, onJobsChanged} : {onStar
}, [MonitoringJobs]);
useEffect(() => {
if(connectedToBackend){
UpdateMonitoringJobsList();
}, []);
setJoblistUpdateInterval(setInterval(() => {
UpdateMonitoringJobsList();
}, 1000));
}else{
clearInterval(joblistUpdateInterval);
setJoblistUpdateInterval(undefined);
}
}, [connectedToBackend]);
function UpdateMonitoringJobsList(){
console.debug("Updating MonitoringJobsList");

View File

@ -1,16 +1,18 @@
import React, {ReactElement, useEffect} from 'react';
import React, {useEffect, useState} from 'react';
import IJob, {JobTypeFromNumber} from "./interfaces/IJob";
import '../styles/queuePopUp.css';
import '../styles/popup.css';
import {Job} from "./Job";
import IManga from "./interfaces/IManga";
import {Manga} from "./Manga";
export default function QueuePopUp({closeQueue} : {closeQueue(): void}){
export default function QueuePopUp({children} : {children: JSX.Element[]}) {
const [StandbyJobs, setStandbyJobs] = React.useState<IJob[]>([]);
const [StandbyJobsManga, setStandbyJobsManga] = React.useState<IManga[]>([]);
const [RunningJobs, setRunningJobs] = React.useState<IJob[]>([]);
const [RunningJobsManga, setRunningJobsManga] = React.useState<IManga[]>([]);
const [showQueuePopup, setShowQueuePopup] = useState<boolean>(false);
useEffect(() => {
Job.GetStandbyJobs()
@ -56,20 +58,26 @@ export default function QueuePopUp({closeQueue} : {closeQueue(): void}){
.then(setRunningJobsManga);
}, [RunningJobs]);
return (
<div id="QueuePopUp">
<div id="QueuePopUpHeader">
<h1>Queue Status</h1>
<img alt="Close Search" id="closeSearch" src="../media/close-x.svg" onClick={closeQueue}/>
return (<>
<div onClick={() => setShowQueuePopup(true)}>
{children}
</div>
<div id="QueuePopUpBody">
{showQueuePopup
? <div className="popup" id="QueuePopUp">
<div className="popupHeader">
<h1>Queue Status {showQueuePopup ? "true" : "false"}</h1>
<img alt="Close Search" className="close" src="../media/close-x.svg"
onClick={() => setShowQueuePopup(false)}/>
</div>
<div id="QueuePopUpBody" className="popupBody">
<div id="RunningJobQueue">
<h1>Running</h1>
<div className="JobQueue">
{RunningJobs.map((job: IJob) => {
const manga = RunningJobsManga.find(manga => manga.internalId == job.mangaInternalId || manga.internalId == job.chapter?.parentManga.internalId);
if (manga === undefined || manga === null)
return <div key={"QueueJob-" + job.id}>Error. Could not find matching manga for {job.id}</div>
return <div key={"QueueJob-" + job.id}>Error. Could not find matching manga
for {job.id}</div>
return <div className="QueueJob" key={"QueueJob-" + job.id}>
<img src={Manga.GetMangaCoverUrl(manga.internalId)}/>
<p>{JobTypeFromNumber(job.jobType)}</p>
@ -96,5 +104,8 @@ export default function QueuePopUp({closeQueue} : {closeQueue(): void}){
</div>
</div>
</div>
: <></>
}
</>
);
}

View File

@ -6,7 +6,7 @@ import IManga, {SearchResult} from "./interfaces/IManga";
import '../styles/search.css';
import '../styles/MangaSearchResult.css'
export default function Search({onJobsChanged, closeSearch} : {onJobsChanged: EventHandler<any>, closeSearch(): void}) {
export default function Search({createJob, closeSearch} : {createJob: (internalId: string, type: string) => void, closeSearch(): void}) {
const [mangaConnectors, setConnectors] = useState<IMangaConnector[]>();
const [selectedConnector, setSelectedConnector] = useState<IMangaConnector>();
const [selectedLanguage, setSelectedLanguage] = useState<string>();
@ -98,8 +98,7 @@ export default function Search({onJobsChanged, closeSearch} : {onJobsChanged: Ev
<select id="Searchbox-language" onChange={changeSelectedLanguage} value={selectedLanguage === null ? "" : selectedLanguage}>
{selectedConnector === undefined
? <option value="" disabled hidden>Select Connector</option>
: selectedConnector.SupportedLanguages.map(language => <option value={language}
key={language}>{language}</option>)}
: selectedConnector.SupportedLanguages.map(language => <option value={language} key={language}>{language}</option>)}
</select>
<button id="Searchbox-button" type="submit" onClick={ExecuteSearch}>Search</button>
</div>
@ -107,7 +106,7 @@ export default function Search({onJobsChanged, closeSearch} : {onJobsChanged: Ev
<div id="SearchResults">
{searchResults === undefined
? <p></p>
: searchResults.map(result => SearchResult(result, onJobsChanged))}
: searchResults.map(result => SearchResult(result, createJob))}
</div>
</div>)
}

View File

@ -52,7 +52,7 @@ export function CoverCard(manga: IManga) : ReactElement {
</div>);
}
export function SearchResult(manga: IManga, jobsChanged: EventHandler<any>) : ReactElement {
export function SearchResult(manga: IManga, createJob: (internalId: string, type: string) => void) : ReactElement {
return(
<div className="SearchResult" key={manga.internalId}>
<img src={Manga.GetMangaCoverUrl(manga.internalId)}></img>
@ -65,7 +65,7 @@ export function SearchResult(manga: IManga, jobsChanged: EventHandler<any>) : Re
</div>
<MarkdownPreview className="Manga-description" source={manga.description} style={{ backgroundColor: "transparent", color: "black" }} />
<button className="Manga-AddButton" onClick={(e) => {
Job.CreateJob(manga.internalId, "MonitorManga", "03:00:00").then(() => jobsChanged(manga.internalId));
createJob(manga.internalId, "MonitorManga")
}}>Monitor
</button>
</div>);

View File

@ -11,6 +11,7 @@ footer {
position: fixed;
bottom: 0;
color: white;
z-index: 10;
}
#madeWith {
@ -20,19 +21,20 @@ footer {
cursor: url("Website/media/blahaj.png"), grab;
}
footer div {
footer .statusBadge {
margin: 0 10px;
display: flex;
align-items: center;
justify-items: center;
background-color: rgba(255,255,255, 0.3);
border-radius: 10px;
padding: 0 5px;
padding: 2px 5px;
}
footer div > * {
margin: 0 2px;
padding: 3px 0;
footer > div {
display: flex;
align-items: center;
justify-items: center;
}
footer .hoverHand {

43
Website/styles/popup.css Normal file
View File

@ -0,0 +1,43 @@
.popup {
position: fixed;
left: 10%;
top: 7.5%;
width: 80%;
height: 80%;
margin: auto;
z-index: 100;
background-color: var(--second-background-color);
border-radius: 10px;
overflow: hidden;
}
.popup .popupHeader {
position: absolute;
top: 0;
left: 0;
height: 40px;
width: 100%;
background-color: var(--primary-color);
color: var(--accent-color);
}
.popup .popupHeader h1 {
margin: 4px 10px;
font-size: 20pt;
}
.popup .close {
position: absolute;
top: 0;
right: 0;
height: 100%;
cursor: pointer;
}
.popup .popupBody {
position: absolute;
top: 40px;
left: 0;
width: 100%;
height: calc(100% - 40px);
}

View File

@ -1,44 +1,4 @@
#QueuePopUp {
position: absolute;
left: 10%;
top: 7.5%;
width: 80%;
height: 80%;
margin: auto;
z-index: 100;
background-color: var(--second-background-color);
border-radius: 10px;
overflow: hidden;
}
#QueuePopUp #QueuePopUpHeader {
position: absolute;
top: 0;
left: 0;
height: 40px;
width: 100%;
background-color: var(--primary-color);
color: var(--accent-color);
}
#QueuePopUp #QueuePopUpHeader h1 {
margin: 4px 10px;
font-size: 20pt;
}
#QueuePopUp #closeSearch {
position: absolute;
top: 0;
right: 0;
height: 100%;
}
#QueuePopUp #QueuePopUpBody {
position: absolute;
top: 40px;
left: 0;
width: 100%;
height: calc(100% - 40px);
display: flex;
}