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:
2023-08-30 13:49:58 -04:00
committed by GitHub
parent c20f24b1a2
commit 32f4055daa
26 changed files with 5513 additions and 6136 deletions

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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 ? (

View File

@@ -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>
)}

View 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>
);
};

View File

@@ -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: (