🧸 Zustand and Tanstack Query (#96)
* ↪️ Removed node-sass, added sass * 🏗️ Refactoring Navbar to read from zustand store * ⬆️ Bumped deps * 🏗️ Refactored AirDC++ session status indicator * 🏗️ Refactored Import page to read from global state * 🏗 Wired up the event emit correctly * 🏗️ Added import queue related state * 🏗 Implemented setQueueAction * 🏗️ Wired up job queue control methods * 🏗️ Added null check and removed useless deps * 🏗️ Refactored the Import page * ↪️ Added cache invalidation to job statistics query * 🏗️ Refactoring the Library page * 🏗️ Fixed pagination and disabled states * ✏️ Changed page to offset To better reflect what we are doing with the pagination controls * 🏗️ Refactoring ComicDetail page and its children * 🏗️ Refactored ComicDetailContainer with useQuery * 🔧 Fixed the error check on Library page * 🏗️ Refactoring AcquisitionPanel * 🏗️ Refactoring the AirDC++ Forms * 🦃 Thanksgiving Day bug fixes * ⬆️ Bumped up Vite to 5.0 * 🔧 Refactoring AcquisitionPanel * 🏗️ Wiring up the DC++ search method * 🏗️ Refactoring AirDC++ search method * 🔎 Added some validation to ADC++ Hubs settings form * 🏗️ Fixed the ADC++ search results * 🏗️ Cleanup of the search results pane
This commit was merged in pull request #96.
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
import React, { ReactElement, useCallback, useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import {
|
||||
fetchComicBookMetadata,
|
||||
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";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useStore } from "../../store";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import axios from "axios";
|
||||
|
||||
interface IProps {
|
||||
matches?: unknown;
|
||||
@@ -18,10 +16,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the average of two numbers.
|
||||
*
|
||||
* @remarks
|
||||
* This method is part of the {@link core-library#Statistics | Statistics subsystem}.
|
||||
* Component to facilitate the import of comics to the ThreeTwo library
|
||||
*
|
||||
* @param x - The first input number
|
||||
* @param y - The second input number
|
||||
@@ -31,41 +26,70 @@ interface IProps {
|
||||
*/
|
||||
|
||||
export const Import = (props: IProps): ReactElement => {
|
||||
const dispatch = useDispatch();
|
||||
const successfulImportJobCount = useSelector(
|
||||
(state: RootState) => state.fileOps.successfulJobCount,
|
||||
);
|
||||
const failedImportJobCount = useSelector(
|
||||
(state: RootState) => state.fileOps.failedJobCount,
|
||||
const queryClient = useQueryClient();
|
||||
const { importJobQueue, socketIOInstance } = useStore(
|
||||
useShallow((state) => ({
|
||||
importJobQueue: state.importJobQueue,
|
||||
socketIOInstance: state.socketIOInstance,
|
||||
})),
|
||||
);
|
||||
|
||||
const lastQueueJob = useSelector(
|
||||
(state: RootState) => state.fileOps.lastQueueJob,
|
||||
);
|
||||
const libraryQueueImportStatus = useSelector(
|
||||
(state: RootState) => state.fileOps.LSQueueImportStatus,
|
||||
);
|
||||
const sessionId = localStorage.getItem("sessionId");
|
||||
const { mutate: initiateImport } = useMutation({
|
||||
mutationFn: async () =>
|
||||
await axios.request({
|
||||
url: `http://localhost:3000/api/library/newImport`,
|
||||
method: "POST",
|
||||
data: { sessionId },
|
||||
}),
|
||||
});
|
||||
|
||||
const allImportJobResults = useSelector(
|
||||
(state: RootState) => state.fileOps.importJobStatistics,
|
||||
);
|
||||
const { data, isError, isLoading } = useQuery({
|
||||
queryKey: ["allImportJobResults"],
|
||||
queryFn: async () =>
|
||||
await axios({
|
||||
method: "GET",
|
||||
url: "http://localhost:3000/api/jobqueue/getJobResultStatistics",
|
||||
}),
|
||||
});
|
||||
|
||||
const initiateImport = useCallback(() => {
|
||||
if (typeof props.path !== "undefined") {
|
||||
dispatch(fetchComicBookMetadata(props.path));
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
const toggleQueue = useCallback(
|
||||
(queueAction: string, queueStatus: string) => {
|
||||
dispatch(setQueueControl(queueAction, queueStatus));
|
||||
},
|
||||
[],
|
||||
);
|
||||
useEffect(() => {
|
||||
dispatch(getImportJobResultStatistics());
|
||||
}, []);
|
||||
// 1a. Act on each comic issue successfully imported/failed, as indicated
|
||||
// by the LS_COVER_EXTRACTED/LS_COVER_EXTRACTION_FAILED events
|
||||
socketIOInstance.on("LS_COVER_EXTRACTED", (data) => {
|
||||
const { completedJobCount, importResult } = data;
|
||||
importJobQueue.setJobCount("successful", completedJobCount);
|
||||
importJobQueue.setMostRecentImport(importResult.rawFileDetails.name);
|
||||
});
|
||||
socketIOInstance.on("LS_COVER_EXTRACTION_FAILED", (data) => {
|
||||
const { failedJobCount } = data;
|
||||
importJobQueue.setJobCount("failed", failedJobCount);
|
||||
});
|
||||
|
||||
// 1b. Clear the localStorage sessionId upon receiving the
|
||||
// LS_IMPORT_QUEUE_DRAINED event
|
||||
socketIOInstance.on("LS_IMPORT_QUEUE_DRAINED", (data) => {
|
||||
localStorage.removeItem("sessionId");
|
||||
importJobQueue.setStatus("drained");
|
||||
queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] });
|
||||
});
|
||||
const toggleQueue = (queueAction: string, queueStatus: string) => {
|
||||
socketIOInstance.emit(
|
||||
"call",
|
||||
"socket.setQueueStatus",
|
||||
{
|
||||
queueAction,
|
||||
queueStatus,
|
||||
},
|
||||
(data) => console.log(data),
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Method to render import job queue pause/resume controls on the UI
|
||||
*
|
||||
* @param status The `string` status (either `"pause"` or `"resume"`)
|
||||
* @returns ReactElement A `<button/>` that toggles queue status
|
||||
* @remarks Sets the global `importJobQueue.status` state upon toggling
|
||||
*/
|
||||
const renderQueueControls = (status: string): ReactElement | null => {
|
||||
switch (status) {
|
||||
case "running":
|
||||
@@ -73,7 +97,10 @@ export const Import = (props: IProps): ReactElement => {
|
||||
<div className="control">
|
||||
<button
|
||||
className="button is-warning is-light"
|
||||
onClick={() => toggleQueue("pause", "paused")}
|
||||
onClick={() => {
|
||||
toggleQueue("pause", "paused");
|
||||
importJobQueue.setStatus("paused");
|
||||
}}
|
||||
>
|
||||
<i className="fa-solid fa-pause mr-2"></i> Pause
|
||||
</button>
|
||||
@@ -84,7 +111,10 @@ export const Import = (props: IProps): ReactElement => {
|
||||
<div className="control">
|
||||
<button
|
||||
className="button is-success is-light"
|
||||
onClick={() => toggleQueue("resume", "running")}
|
||||
onClick={() => {
|
||||
toggleQueue("resume", "running");
|
||||
importJobQueue.setStatus("running");
|
||||
}}
|
||||
>
|
||||
<i className="fa-solid fa-play mr-2"></i> Resume
|
||||
</button>
|
||||
@@ -123,12 +153,15 @@ export const Import = (props: IProps): ReactElement => {
|
||||
<p className="buttons">
|
||||
<button
|
||||
className={
|
||||
libraryQueueImportStatus === "drained" ||
|
||||
libraryQueueImportStatus === undefined
|
||||
importJobQueue.status === "drained" ||
|
||||
importJobQueue.status === undefined
|
||||
? "button is-medium"
|
||||
: "button is-loading is-medium"
|
||||
}
|
||||
onClick={initiateImport}
|
||||
onClick={() => {
|
||||
initiateImport();
|
||||
importJobQueue.setStatus("running");
|
||||
}}
|
||||
>
|
||||
<span className="icon">
|
||||
<i className="fas fa-file-import"></i>
|
||||
@@ -136,8 +169,9 @@ export const Import = (props: IProps): ReactElement => {
|
||||
<span>Start Import</span>
|
||||
</button>
|
||||
</p>
|
||||
{libraryQueueImportStatus !== "drained" &&
|
||||
!isUndefined(libraryQueueImportStatus) && (
|
||||
|
||||
{importJobQueue.status !== "drained" &&
|
||||
!isUndefined(importJobQueue.status) && (
|
||||
<>
|
||||
<table className="table">
|
||||
<thead>
|
||||
@@ -152,29 +186,29 @@ export const Import = (props: IProps): ReactElement => {
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
{successfulImportJobCount > 0 && (
|
||||
{importJobQueue.successfulJobCount > 0 && (
|
||||
<div className="box has-background-success-light has-text-centered">
|
||||
<span className="is-size-2 has-text-weight-bold">
|
||||
{successfulImportJobCount}
|
||||
{importJobQueue.successfulJobCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
<td>
|
||||
{failedImportJobCount > 0 && (
|
||||
{importJobQueue.failedJobCount > 0 && (
|
||||
<div className="box has-background-danger has-text-centered">
|
||||
<span className="is-size-2 has-text-weight-bold">
|
||||
{failedImportJobCount}
|
||||
{importJobQueue.failedJobCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td>{renderQueueControls(libraryQueueImportStatus)}</td>
|
||||
<td>{renderQueueControls(importJobQueue.status)}</td>
|
||||
<td>
|
||||
{libraryQueueImportStatus !== undefined ? (
|
||||
{importJobQueue.status !== undefined ? (
|
||||
<span className="tag is-warning">
|
||||
{libraryQueueImportStatus}
|
||||
{importJobQueue.status}
|
||||
</span>
|
||||
) : null}
|
||||
</td>
|
||||
@@ -182,53 +216,58 @@ export const Import = (props: IProps): ReactElement => {
|
||||
</tbody>
|
||||
</table>
|
||||
Imported{" "}
|
||||
<span className="has-text-weight-bold">{lastQueueJob}</span>
|
||||
<span className="has-text-weight-bold">
|
||||
{importJobQueue.mostRecentImport}
|
||||
</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>
|
||||
{!isLoading && !isEmpty(data?.data) && (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{data?.data.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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user