Import queue progress (#87)
* 🚥 Added service status panel scaffold * 🐂 Support for showing import progress * 🐂 Support for session-tracking * 🔧 Tooling for resumable socket.io sessions * 🧹 Minor change in socket.io connection code * 🔧 Refactoring the Import Page * 📝 Added more details to import statuses * 🐂 Queue pause/resume functionality * 🐂 Queue drain event reducer * 🐂 Queue controls * 🔧 Hardening the import UX * 🔀 Bumped deps * 🔧 Fixed the airdcpp-apisocket version * ⛑️ Removed useless deps * 🪡 Fixed margin on the comicinfo.xml panel on the library page * 🏗️ Scaffold for job results * 🔢 Removed the useless LS_IMPORT event * 🔧 Wired up jobStatistics call * 🧹 Cleaning up the tabulated job results * 🔧 More finishing touches to Import UX * 🔧 Added a console log for debugging purposes --------- Co-authored-by: Rishi Ghan <hghan@apple.com>
This commit was merged in pull request #87.
This commit is contained in:
@@ -15,14 +15,15 @@ export const AirDCPPSettingsForm = (): ReactElement => {
|
||||
try {
|
||||
if (!isUndefined(hostname)) {
|
||||
const matches = hostname.match(hostnameRegex);
|
||||
return (isNil(matches) && matches.length !== 0) ? hostname : "Invalid hostname; it should not contain special characters";
|
||||
return isNil(matches) && matches.length !== 0
|
||||
? hostname
|
||||
: "Invalid hostname; it should not contain special characters";
|
||||
}
|
||||
}
|
||||
catch {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const onSubmit = useCallback(async (values) => {
|
||||
try {
|
||||
airDCPPSettings.setSettings(values);
|
||||
@@ -39,7 +40,7 @@ export const AirDCPPSettingsForm = (): ReactElement => {
|
||||
airDCPPSettings.setSettings({});
|
||||
dispatch(deleteSettings());
|
||||
}, []);
|
||||
const validate = async () => { };
|
||||
const validate = async () => {};
|
||||
const initFormData = !isUndefined(
|
||||
airDCPPSettings.airDCPPState.settings.directConnect,
|
||||
)
|
||||
@@ -67,13 +68,20 @@ export const AirDCPPSettingsForm = (): ReactElement => {
|
||||
</span>
|
||||
</p>
|
||||
<div className="control is-expanded">
|
||||
<Field
|
||||
name="hostname"
|
||||
validate={hostValidator}>
|
||||
<Field name="hostname" validate={hostValidator}>
|
||||
{({ input, meta }) => (
|
||||
<div>
|
||||
<input {...input} type="text" placeholder="AirDC++ hostname" className="input" />
|
||||
{meta.error && meta.touched && <span className="is-size-7 has-text-danger">{meta.error}</span>}
|
||||
<input
|
||||
{...input}
|
||||
type="text"
|
||||
placeholder="AirDC++ hostname"
|
||||
className="input"
|
||||
/>
|
||||
{meta.error && meta.touched && (
|
||||
<span className="is-size-7 has-text-danger">
|
||||
{meta.error}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { ReactElement, useContext, useEffect } from "react";
|
||||
import Dashboard from "./Dashboard/Dashboard";
|
||||
|
||||
import Import from "./Import";
|
||||
import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer";
|
||||
import TabulatedContentContainer from "./Library/TabulatedContentContainer";
|
||||
@@ -17,7 +16,9 @@ import {
|
||||
AirDCPPSocketContextProvider,
|
||||
AirDCPPSocketContext,
|
||||
} from "../context/AirDCPPSocket";
|
||||
import { isEmpty, isUndefined } from "lodash";
|
||||
import { SocketIOProvider } from "../context/SocketIOContext";
|
||||
import socketIOConnectionInstance from "../shared/socket.io/instance";
|
||||
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||
import {
|
||||
AIRDCPP_DOWNLOAD_PROGRESS_TICK,
|
||||
LS_SINGLE_IMPORT,
|
||||
@@ -95,45 +96,65 @@ const AirDCPPSocketComponent = (): ReactElement => {
|
||||
return <></>;
|
||||
};
|
||||
export const App = (): ReactElement => {
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
// Check if there is a sessionId in localStorage
|
||||
const sessionId = localStorage.getItem("sessionId");
|
||||
if (!isNil(sessionId)) {
|
||||
// Resume the session
|
||||
dispatch({
|
||||
type: "RESUME_SESSION",
|
||||
meta: { remote: true },
|
||||
session: { sessionId },
|
||||
});
|
||||
} else {
|
||||
// Inititalize the session and persist the sessionId to localStorage
|
||||
socketIOConnectionInstance.on("sessionInitialized", (sessionId) => {
|
||||
localStorage.setItem("sessionId", sessionId);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<AirDCPPSocketContextProvider>
|
||||
<div>
|
||||
<AirDCPPSocketComponent />
|
||||
<Navbar />
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/import" element={<Import path={"./comics"} />} />
|
||||
<Route
|
||||
path="/library"
|
||||
element={<TabulatedContentContainer category="library" />}
|
||||
/>
|
||||
<Route path="/library-grid" element={<LibraryGrid />} />
|
||||
<Route path="/downloads" element={<Downloads data={{}} />} />
|
||||
<Route path="/search" element={<Search />} />
|
||||
<Route
|
||||
path={"/comic/details/:comicObjectId"}
|
||||
element={<ComicDetailContainer />}
|
||||
/>
|
||||
<Route
|
||||
path={"/volume/details/:comicObjectId"}
|
||||
element={<VolumeDetail />}
|
||||
/>
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route
|
||||
path="/pull-list/all"
|
||||
element={<TabulatedContentContainer category="pullList" />}
|
||||
/>
|
||||
<Route
|
||||
path="/wanted/all"
|
||||
element={<TabulatedContentContainer category="wanted" />}
|
||||
/>
|
||||
<Route
|
||||
path="/volumes/all"
|
||||
element={<TabulatedContentContainer category="volumes" />}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
</AirDCPPSocketContextProvider>
|
||||
<SocketIOProvider socket={socketIOConnectionInstance}>
|
||||
<AirDCPPSocketContextProvider>
|
||||
<div>
|
||||
<AirDCPPSocketComponent />
|
||||
<Navbar />
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/import" element={<Import path={"./comics"} />} />
|
||||
<Route
|
||||
path="/library"
|
||||
element={<TabulatedContentContainer category="library" />}
|
||||
/>
|
||||
<Route path="/library-grid" element={<LibraryGrid />} />
|
||||
<Route path="/downloads" element={<Downloads data={{}} />} />
|
||||
<Route path="/search" element={<Search />} />
|
||||
<Route
|
||||
path={"/comic/details/:comicObjectId"}
|
||||
element={<ComicDetailContainer />}
|
||||
/>
|
||||
<Route
|
||||
path={"/volume/details/:comicObjectId"}
|
||||
element={<VolumeDetail />}
|
||||
/>
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route
|
||||
path="/pull-list/all"
|
||||
element={<TabulatedContentContainer category="pullList" />}
|
||||
/>
|
||||
<Route
|
||||
path="/wanted/all"
|
||||
element={<TabulatedContentContainer category="wanted" />}
|
||||
/>
|
||||
<Route
|
||||
path="/volumes/all"
|
||||
element={<TabulatedContentContainer category="volumes" />}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
</AirDCPPSocketContextProvider>
|
||||
</SocketIOProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -48,7 +48,9 @@ export const ComicInfoXML = (data): ReactElement => {
|
||||
<dd>
|
||||
<span className="is-size-7">{json.notes[0]}</span>
|
||||
</dd>
|
||||
<dd className="mt-1 mb-1">{json.summary[0]}</dd>
|
||||
<dd className="mt-1 mb-1">
|
||||
{!isUndefined(json.summary) && json.summary[0]}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "../../actions/fileops.actions";
|
||||
import { getLibraryStatistics } from "../../actions/comicinfo.actions";
|
||||
import { isEmpty, isNil } from "lodash";
|
||||
import Header from "../Header";
|
||||
|
||||
export const Dashboard = (): ReactElement => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -43,7 +44,7 @@ export const Dashboard = (): ReactElement => {
|
||||
}, []);
|
||||
|
||||
const recentComics = useSelector(
|
||||
(state: RootState) => state.fileOps.recentComics
|
||||
(state: RootState) => state.fileOps.recentComics,
|
||||
);
|
||||
const wantedComics = useSelector(
|
||||
(state: RootState) => state.fileOps.wantedComics,
|
||||
@@ -64,6 +65,36 @@ export const Dashboard = (): ReactElement => {
|
||||
<>
|
||||
{/* Pull List */}
|
||||
<PullList issues={recentComics} />
|
||||
<>
|
||||
<div className="content mt-6">
|
||||
<Header
|
||||
headerContent="Import Activity"
|
||||
subHeaderContent="Results aggregated from the last import"
|
||||
iconClassNames="fa-solid fa-file-invoice mr-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<abbr title="Position">Pos</abbr>
|
||||
</th>
|
||||
<th>Team</th>
|
||||
<th>
|
||||
<abbr title="Played">Pld</abbr>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>1</th>
|
||||
<td>38</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
|
||||
{/* Stats */}
|
||||
{!isEmpty(libraryStatistics) && (
|
||||
@@ -74,8 +105,8 @@ export const Dashboard = (): ReactElement => {
|
||||
<WantedComicsList comics={wantedComics} />
|
||||
)}
|
||||
{/* Recent imports */}
|
||||
<RecentlyImported comicBookCovers={recentComics} />
|
||||
|
||||
<RecentlyImported comicBookCovers={recentComics} />
|
||||
|
||||
{/* Volumes */}
|
||||
{!isEmpty(volumeGroups) && (
|
||||
<VolumeGroups volumeGroups={volumeGroups} />
|
||||
@@ -94,4 +125,4 @@ export const Dashboard = (): ReactElement => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
export default Dashboard;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isNil, map } from "lodash";
|
||||
import React, { createRef, ReactElement, useCallback, useEffect } from "react";
|
||||
import Card from "../Carda";
|
||||
import Header from "../Header";
|
||||
import Masonry from "react-masonry-css";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getWeeklyPullList } from "../../actions/comicinfo.actions";
|
||||
@@ -20,7 +21,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
getWeeklyPullList({
|
||||
startDate: "2023-5-25",
|
||||
startDate: "2023-8-9",
|
||||
pageSize: "15",
|
||||
currentPage: "1",
|
||||
}),
|
||||
@@ -88,12 +89,9 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
|
||||
return (
|
||||
<>
|
||||
<div className="content">
|
||||
<h4 className="title is-4">
|
||||
<i className="fa-solid fa-splotch"></i> Discover
|
||||
</h4>
|
||||
<p className="subtitle is-7">
|
||||
Pull List aggregated for the week from League Of Comic Geeks
|
||||
</p>
|
||||
<Header headerContent="Discover"
|
||||
subHeaderContent="Pull List aggregated for the week from League Of Comic Geeks"
|
||||
iconClassNames="fa-solid fa-splotch mr-2"/>
|
||||
<div className="field is-grouped">
|
||||
{/* select week */}
|
||||
<div className="control">
|
||||
@@ -127,7 +125,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
|
||||
<Slider {...settings} ref={(c) => (sliderRef = c)}>
|
||||
{!isNil(pullList) &&
|
||||
pullList &&
|
||||
map(pullList, ({issue}, idx) => {
|
||||
map(pullList, ({ issue }, idx) => {
|
||||
return (
|
||||
<Card
|
||||
key={idx}
|
||||
@@ -161,4 +159,4 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PullList;
|
||||
export default PullList;
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import React, { ReactElement, useCallback, useContext, useEffect, useState } from "react";
|
||||
import React, {
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { getTransfers } from "../../actions/airdcpp.actions";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
|
||||
@@ -20,7 +26,9 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
|
||||
const airDCPPTransfers = useSelector(
|
||||
(state: RootState) => state.airdcpp.transfers,
|
||||
);
|
||||
const issueBundles = useSelector((state: RootState) => state.airdcpp.issue_bundles);
|
||||
const issueBundles = useSelector(
|
||||
(state: RootState) => state.airdcpp.issue_bundles,
|
||||
);
|
||||
const [bundles, setBundles] = useState([]);
|
||||
// Make the call to get all transfers from AirDC++
|
||||
useEffect(() => {
|
||||
@@ -37,18 +45,26 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
|
||||
useEffect(() => {
|
||||
if (!isUndefined(issueBundles)) {
|
||||
const foo = issueBundles.data.map((bundle) => {
|
||||
const { rawFileDetails, inferredMetadata, acquisition: { directconnect: { downloads } }, sourcedMetadata: { locg, comicvine } } = bundle;
|
||||
const {
|
||||
rawFileDetails,
|
||||
inferredMetadata,
|
||||
acquisition: {
|
||||
directconnect: { downloads },
|
||||
},
|
||||
sourcedMetadata: { locg, comicvine },
|
||||
} = bundle;
|
||||
const { issueName, url } = determineCoverFile({
|
||||
rawFileDetails, comicvine, locg,
|
||||
rawFileDetails,
|
||||
comicvine,
|
||||
locg,
|
||||
});
|
||||
return { ...bundle, issueName, url }
|
||||
|
||||
})
|
||||
return { ...bundle, issueName, url };
|
||||
});
|
||||
setBundles(foo);
|
||||
}
|
||||
}, [issueBundles])
|
||||
}, [issueBundles]);
|
||||
|
||||
return !isNil(bundles) ?
|
||||
return !isNil(bundles) ? (
|
||||
<div className="container">
|
||||
<section className="section">
|
||||
<h1 className="title">Downloads</h1>
|
||||
@@ -56,45 +72,59 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
|
||||
<div className="column is-half">
|
||||
{bundles.map((bundle, idx) => {
|
||||
console.log(bundle);
|
||||
return <div key={idx}>
|
||||
<MetadataPanel
|
||||
data={bundle}
|
||||
imageStyle={{ maxWidth: 80 }}
|
||||
titleStyle={{ fontSize: "0.8rem" }}
|
||||
tagsStyle={{ fontSize: "0.7rem" }}
|
||||
containerStyle={{
|
||||
maxWidth: 400,
|
||||
padding: 0,
|
||||
margin: "0 0 8px 0",
|
||||
}} />
|
||||
return (
|
||||
<div key={idx}>
|
||||
<MetadataPanel
|
||||
data={bundle}
|
||||
imageStyle={{ maxWidth: 80 }}
|
||||
titleStyle={{ fontSize: "0.8rem" }}
|
||||
tagsStyle={{ fontSize: "0.7rem" }}
|
||||
containerStyle={{
|
||||
maxWidth: 400,
|
||||
padding: 0,
|
||||
margin: "0 0 8px 0",
|
||||
}}
|
||||
/>
|
||||
|
||||
<table className="table is-size-7">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th>Type</th>
|
||||
<th>Bundle ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bundle.acquisition.directconnect.downloads.map((bundle, idx) => {
|
||||
return (<tr key={idx}>
|
||||
<td>{bundle.name}</td>
|
||||
<td>{bundle.size}</td>
|
||||
<td>{bundle.type.str}</td>
|
||||
<td><span className="tag is-warning">{bundle.bundleId}</span></td>
|
||||
</tr>)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* <pre>{JSON.stringify(bundle.acquisition.directconnect.downloads, null, 2)}</pre> */}
|
||||
</div>
|
||||
<table className="table is-size-7">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th>Type</th>
|
||||
<th>Bundle ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bundle.acquisition.directconnect.downloads.map(
|
||||
(bundle, idx) => {
|
||||
return (
|
||||
<tr key={idx}>
|
||||
<td>{bundle.name}</td>
|
||||
<td>{bundle.size}</td>
|
||||
<td>{bundle.type.str}</td>
|
||||
<td>
|
||||
<span className="tag is-warning">
|
||||
{bundle.bundleId}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* <pre>{JSON.stringify(bundle.acquisition.directconnect.downloads, null, 2)}</pre> */}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div> : <div>There are no downloads.</div>;
|
||||
</div>
|
||||
) : (
|
||||
<div>There are no downloads.</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Downloads;
|
||||
|
||||
20
src/client/components/Header.tsx
Normal file
20
src/client/components/Header.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React, { ReactElement } from "react";
|
||||
|
||||
type IHeaderProps = {
|
||||
headerContent: string;
|
||||
subHeaderContent: string;
|
||||
iconClassNames: string;
|
||||
};
|
||||
|
||||
export const Header = (props: IHeaderProps): ReactElement => {
|
||||
return (
|
||||
<>
|
||||
<h4 className="title is-4">
|
||||
<i className={props.iconClassNames}></i> {props.headerContent}
|
||||
</h4>
|
||||
<p className="subtitle is-7">{props.subHeaderContent}</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, { ReactElement, useCallback, useContext, useState } from "react";
|
||||
import React, { ReactElement, useCallback, useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import {
|
||||
fetchComicBookMetadata,
|
||||
toggleImportQueueStatus,
|
||||
getImportJobResultStatistics,
|
||||
setQueueControl,
|
||||
} from "../actions/fileops.actions";
|
||||
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
||||
import { format } from "date-fns";
|
||||
import Loader from "react-loader-spinner";
|
||||
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||
|
||||
interface IProps {
|
||||
matches?: unknown;
|
||||
@@ -29,102 +32,203 @@ interface IProps {
|
||||
|
||||
export const Import = (props: IProps): ReactElement => {
|
||||
const dispatch = useDispatch();
|
||||
const libraryQueueResults = useSelector(
|
||||
(state: RootState) => state.fileOps.librarySearchResultCount,
|
||||
const successfulImportJobCount = useSelector(
|
||||
(state: RootState) => state.fileOps.successfulJobCount,
|
||||
);
|
||||
const failedImportJobCount = useSelector(
|
||||
(state: RootState) => state.fileOps.failedJobCount,
|
||||
);
|
||||
|
||||
const lastQueueJob = useSelector(
|
||||
(state: RootState) => state.fileOps.lastQueueJob,
|
||||
);
|
||||
const libraryQueueImportStatus = useSelector(
|
||||
(state: RootState) => state.fileOps.IMSCallInProgress,
|
||||
(state: RootState) => state.fileOps.LSQueueImportStatus,
|
||||
);
|
||||
const [isImportQueuePaused, setImportQueueStatus] = useState(false);
|
||||
|
||||
const allImportJobResults = useSelector(
|
||||
(state: RootState) => state.fileOps.importJobStatistics,
|
||||
);
|
||||
|
||||
const initiateImport = useCallback(() => {
|
||||
if (typeof props.path !== "undefined") {
|
||||
dispatch(fetchComicBookMetadata(props.path));
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
const toggleImport = useCallback(() => {
|
||||
setImportQueueStatus(!isImportQueuePaused);
|
||||
if (isImportQueuePaused === false) {
|
||||
dispatch(toggleImportQueueStatus({ action: "resume" }));
|
||||
} else if (isImportQueuePaused === true) {
|
||||
dispatch(toggleImportQueueStatus({ action: "pause" }));
|
||||
const toggleQueue = useCallback(
|
||||
(queueAction: string, queueStatus: string) => {
|
||||
dispatch(setQueueControl(queueAction, queueStatus));
|
||||
},
|
||||
[],
|
||||
);
|
||||
useEffect(() => {
|
||||
dispatch(getImportJobResultStatistics());
|
||||
}, []);
|
||||
|
||||
const renderQueueControls = (status: string): ReactElement | null => {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return (
|
||||
<div className="control">
|
||||
<button
|
||||
className="button is-warning is-light"
|
||||
onClick={() => toggleQueue("pause", "paused")}
|
||||
>
|
||||
<i className="fa-solid fa-pause mr-2"></i> Pause
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
case "paused":
|
||||
return (
|
||||
<div className="control">
|
||||
<button
|
||||
className="button is-success is-light"
|
||||
onClick={() => toggleQueue("resume", "running")}
|
||||
>
|
||||
<i className="fa-solid fa-play mr-2"></i> Resume
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "drained":
|
||||
return null;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [isImportQueuePaused]);
|
||||
const pauseIconText = (
|
||||
<>
|
||||
<i className="fa-solid fa-pause mr-2"></i> Pause Import
|
||||
</>
|
||||
);
|
||||
const playIconText = (
|
||||
<>
|
||||
<i className="fa-solid fa-play mr-2"></i> Resume Import
|
||||
</>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className="container">
|
||||
<section className="section is-small">
|
||||
<h1 className="title">Import</h1>
|
||||
<h1 className="title">Import Comics</h1>
|
||||
<article className="message is-dark">
|
||||
<div className="message-body">
|
||||
<p className="mb-2">
|
||||
<span className="tag is-medium is-info is-light">
|
||||
Import Only
|
||||
Import Comics
|
||||
</span>
|
||||
will add comics identified from the mapped folder into the local
|
||||
db.
|
||||
will add comics identified from the mapped folder into ThreeTwo's
|
||||
database.
|
||||
</p>
|
||||
<p>
|
||||
<span className="tag is-medium is-info is-light">
|
||||
Import and Tag
|
||||
</span>
|
||||
will scan the ComicVine, shortboxed APIs and import comics from
|
||||
the mapped folder with the additional metadata.
|
||||
Metadata from ComicInfo.xml, if present, will also be extracted.
|
||||
</p>
|
||||
<p>
|
||||
This process could take a while, if you have a lot of comics, or
|
||||
are importing over a network connection.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
<p className="buttons">
|
||||
<button
|
||||
className={
|
||||
libraryQueueImportStatus
|
||||
? "button is-loading is-medium"
|
||||
: "button is-medium"
|
||||
libraryQueueImportStatus === "drained" ||
|
||||
libraryQueueImportStatus === undefined
|
||||
? "button is-medium"
|
||||
: "button is-loading is-medium"
|
||||
}
|
||||
onClick={initiateImport}
|
||||
>
|
||||
<span className="icon">
|
||||
<i className="fas fa-file-import"></i>
|
||||
</span>
|
||||
<span>Import Only</span>
|
||||
</button>
|
||||
|
||||
<button className="button is-medium">
|
||||
<span className="icon">
|
||||
<i className="fas fa-tag"></i>
|
||||
</span>
|
||||
<span>Import and Tag</span>
|
||||
<span>Start Import</span>
|
||||
</button>
|
||||
</p>
|
||||
<div className="columns is-multiline">
|
||||
<div className="column is-one-fifth">
|
||||
<div className="box control-palette">
|
||||
<span className="is-size-2 has-text-weight-bold">
|
||||
{JSON.stringify(libraryQueueResults, null, 2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="is-half">
|
||||
<div className="content">
|
||||
<div className="control">
|
||||
<button
|
||||
className="button is-warning is-light"
|
||||
onClick={toggleImport}
|
||||
>
|
||||
{!isImportQueuePaused ? pauseIconText : playIconText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{libraryQueueImportStatus !== "drained" &&
|
||||
!isUndefined(libraryQueueImportStatus) && (
|
||||
<>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Completed Jobs</th>
|
||||
<th>Failed Jobs</th>
|
||||
<th>Queue Controls</th>
|
||||
<th>Queue Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
{successfulImportJobCount > 0 && (
|
||||
<div className="box has-background-success-light has-text-centered">
|
||||
<span className="is-size-2 has-text-weight-bold">
|
||||
{successfulImportJobCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
<td>
|
||||
{failedImportJobCount > 0 && (
|
||||
<div className="box has-background-danger has-text-centered">
|
||||
<span className="is-size-2 has-text-weight-bold">
|
||||
{failedImportJobCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td>{renderQueueControls(libraryQueueImportStatus)}</td>
|
||||
<td>
|
||||
{libraryQueueImportStatus !== undefined ? (
|
||||
<span className="tag is-warning">
|
||||
{libraryQueueImportStatus}
|
||||
</span>
|
||||
) : null}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
Imported{" "}
|
||||
<span className="has-text-weight-bold">{lastQueueJob}</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Past imports */}
|
||||
|
||||
<h3 className="subtitle is-4 mt-5">Past Imports</h3>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time Started</th>
|
||||
<th>Session Id</th>
|
||||
<th>Imported</th>
|
||||
<th>Failed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{allImportJobResults.map((jobResult, id) => {
|
||||
return (
|
||||
<tr key={id}>
|
||||
<td>
|
||||
{format(
|
||||
new Date(jobResult.earliestTimestamp),
|
||||
"EEEE, hh:mma, do LLLL Y",
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<span className="tag is-warning">
|
||||
{jobResult.sessionId}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="tag is-success">
|
||||
{jobResult.completedJobs}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="tag is-danger">
|
||||
{jobResult.failedJobs}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -47,7 +47,7 @@ export const Library = (): ReactElement => {
|
||||
|
||||
const ComicInfoXML = (value) => {
|
||||
return value.data ? (
|
||||
<div className="comicvine-metadata">
|
||||
<div className="comicvine-metadata mt-3">
|
||||
<dl>
|
||||
<span className="tags has-addons is-size-7">
|
||||
<span className="tag">Series</span>
|
||||
|
||||
@@ -20,6 +20,25 @@ const Navbar: React.FunctionComponent = (props) => {
|
||||
const socketDisconnectionReason = useSelector(
|
||||
(state: RootState) => state.airdcpp.socketDisconnectionReason,
|
||||
);
|
||||
|
||||
// Import-related selector hooks
|
||||
const successfulImportJobCount = useSelector(
|
||||
(state: RootState) => state.fileOps.successfulJobCount,
|
||||
);
|
||||
const failedImportJobCount = useSelector(
|
||||
(state: RootState) => state.fileOps.failedJobCount,
|
||||
);
|
||||
|
||||
const lastQueueJob = useSelector(
|
||||
(state: RootState) => state.fileOps.lastQueueJob,
|
||||
);
|
||||
const libraryQueueImportStatus = useSelector(
|
||||
(state: RootState) => state.fileOps.LSQueueImportStatus,
|
||||
);
|
||||
|
||||
const allImportJobResults = useSelector(
|
||||
(state: RootState) => state.fileOps.importJobStatistics,
|
||||
);
|
||||
return (
|
||||
<nav className="navbar is-fixed-top">
|
||||
<div className="navbar-brand">
|
||||
@@ -88,9 +107,43 @@ const Navbar: React.FunctionComponent = (props) => {
|
||||
<div className="navbar-dropdown is-right is-boxed">
|
||||
<a className="navbar-item">
|
||||
<DownloadProgressTick data={downloadProgressTick} />
|
||||
</a> </div>
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!isUndefined(libraryQueueImportStatus) &&
|
||||
location.hash !== "#/import" ? (
|
||||
<div className="navbar-item has-dropdown is-hoverable">
|
||||
<a className="navbar-link is-arrowless">
|
||||
<i className="fa-solid fa-file-import has-text-warning-dark"></i>
|
||||
</a>
|
||||
|
||||
<div className="navbar-dropdown is-right is-boxed">
|
||||
<a className="navbar-item">
|
||||
<ul>
|
||||
{successfulImportJobCount > 0 ? (
|
||||
<li className="mb-2">
|
||||
<span className="tag is-success mr-2">
|
||||
{successfulImportJobCount}
|
||||
</span>
|
||||
imported.
|
||||
</li>
|
||||
) : null}
|
||||
{failedImportJobCount > 0 ? (
|
||||
<li>
|
||||
<span className="tag is-danger mr-2">
|
||||
{failedImportJobCount}
|
||||
</span>
|
||||
failed to import.
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* AirDC++ socket connection status */}
|
||||
<div className="navbar-item has-dropdown is-hoverable">
|
||||
{airDCPPSocketConnectionStatus ? (
|
||||
|
||||
@@ -15,7 +15,7 @@ export const PullList = (): ReactElement => {
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
getWeeklyPullList({
|
||||
startDate: "2022-11-15",
|
||||
startDate: "2023-7-28",
|
||||
pageSize: "15",
|
||||
currentPage: "1",
|
||||
}),
|
||||
@@ -109,15 +109,15 @@ export const PullList = (): ReactElement => {
|
||||
{!isNil(pullListComics) && (
|
||||
<div>
|
||||
<div className="library">
|
||||
<T2Table
|
||||
sourceData={pullListComics}
|
||||
columns={columnData}
|
||||
totalPages={pullListComics.length}
|
||||
paginationHandlers={{
|
||||
nextPage: nextPageHandler,
|
||||
previousPage: previousPageHandler,
|
||||
}}
|
||||
/>
|
||||
<T2Table
|
||||
sourceData={pullListComics}
|
||||
columns={columnData}
|
||||
totalPages={pullListComics.length}
|
||||
paginationHandlers={{
|
||||
nextPage: nextPageHandler,
|
||||
previousPage: previousPageHandler,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
25
src/client/components/ServiceStatuses/ServiceStatuses.tsx
Normal file
25
src/client/components/ServiceStatuses/ServiceStatuses.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, { ReactElement } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
import { getServiceStatus } from "../../actions/fileops.actions";
|
||||
|
||||
export const ServiceStatuses = (): ReactElement => {
|
||||
const serviceStatus = useSelector(
|
||||
(state: RootState) => state.fileOps.libraryServiceStatus,
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(getServiceStatus());
|
||||
}, []);
|
||||
return (
|
||||
<div className="is-clearfix">
|
||||
<div className="mt-4">
|
||||
<h3 className="title">Core Services</h3>
|
||||
<h6 className="subtitle has-text-grey-light">
|
||||
Statuses for core services
|
||||
</h6>
|
||||
</div>
|
||||
<pre>{JSON.stringify(serviceStatus, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React, { useState, ReactElement } from "react";
|
||||
import { AirDCPPSettingsForm } from "./AirDCPPSettings/AirDCPPSettingsForm";
|
||||
import { AirDCPPHubsForm } from "./AirDCPPSettings/AirDCPPHubsForm";
|
||||
import { SystemSettingsForm } from "./SystemSettings/SystemSettingsForm";
|
||||
import { ServiceStatuses } from "./ServiceStatuses/ServiceStatuses";
|
||||
import settingsObject from "../constants/settings/settingsMenu.json";
|
||||
import { isUndefined, map } from "lodash";
|
||||
|
||||
@@ -22,6 +23,10 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "core-service",
|
||||
content: <ServiceStatuses />,
|
||||
},
|
||||
{
|
||||
id: "flushdb",
|
||||
content: (
|
||||
|
||||
Reference in New Issue
Block a user