299 lines
12 KiB
TypeScript
299 lines
12 KiB
TypeScript
import React, { ReactElement, useCallback, useEffect } from "react";
|
|
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;
|
|
fetchComicMetadata?: any;
|
|
path: string;
|
|
covers?: any;
|
|
}
|
|
|
|
/**
|
|
* Component to facilitate the import of comics to the ThreeTwo library
|
|
*
|
|
* @param x - The first input number
|
|
* @param y - The second input number
|
|
* @returns The arithmetic mean of `x` and `y`
|
|
*
|
|
* @beta
|
|
*/
|
|
|
|
export const Import = (props: IProps): ReactElement => {
|
|
const queryClient = useQueryClient();
|
|
const { importJobQueue, socketIOInstance } = useStore(
|
|
useShallow((state) => ({
|
|
importJobQueue: state.importJobQueue,
|
|
socketIOInstance: state.socketIOInstance,
|
|
})),
|
|
);
|
|
|
|
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 { data, isError, isLoading } = useQuery({
|
|
queryKey: ["allImportJobResults"],
|
|
queryFn: async () =>
|
|
await axios({
|
|
method: "GET",
|
|
url: "http://localhost:3000/api/jobqueue/getJobResultStatistics",
|
|
}),
|
|
});
|
|
|
|
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":
|
|
return (
|
|
<div>
|
|
<button
|
|
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
|
onClick={() => {
|
|
toggleQueue("pause", "paused");
|
|
importJobQueue.setStatus("paused");
|
|
}}
|
|
>
|
|
<span className="text-md">Pause</span>
|
|
<span className="w-5 h-5">
|
|
<i className="h-5 w-5 icon-[solar--pause-bold]"></i>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
);
|
|
case "paused":
|
|
return (
|
|
<div>
|
|
<button
|
|
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
|
onClick={() => {
|
|
toggleQueue("resume", "running");
|
|
importJobQueue.setStatus("running");
|
|
}}
|
|
>
|
|
<span className="text-md">Resume</span>
|
|
<span className="w-5 h-5">
|
|
<i className="h-5 w-5 icon-[solar--play-bold]"></i>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
);
|
|
|
|
case "drained":
|
|
return null;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
return (
|
|
<div>
|
|
<section>
|
|
<header className="bg-slate-200 dark:bg-slate-500">
|
|
<div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4">
|
|
<div className="sm:flex sm:items-center sm:justify-between">
|
|
<div className="text-center sm:text-left">
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
|
|
Import
|
|
</h1>
|
|
|
|
<p className="mt-1.5 text-sm text-gray-500 dark:text-white">
|
|
Import comics into the ThreeTwo library.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="mx-auto max-w-screen-xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
|
|
<article
|
|
role="alert"
|
|
className="rounded-lg max-w-screen-md border-s-4 border-blue-500 bg-blue-50 p-4 dark:border-s-4 dark:border-blue-600 dark:bg-blue-300 dark:text-slate-600"
|
|
>
|
|
<div>
|
|
<p>
|
|
Importing will add comics identified from the mapped folder into
|
|
ThreeTwo's database.
|
|
</p>
|
|
<p>
|
|
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>
|
|
|
|
<div className="my-4">
|
|
{importJobQueue.status === "drained" ||
|
|
(importJobQueue.status === undefined && (
|
|
<button
|
|
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-5 py-3 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
|
onClick={() => {
|
|
initiateImport();
|
|
importJobQueue.setStatus("running");
|
|
}}
|
|
>
|
|
<span className="text-md">Start Import</span>
|
|
<span className="w-6 h-6">
|
|
<i className="h-6 w-6 icon-[solar--file-left-bold-duotone]"></i>
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Activity */}
|
|
{(importJobQueue.status === "running" ||
|
|
importJobQueue.status === "paused") && (
|
|
<>
|
|
<span className="flex items-center my-5 max-w-screen-lg">
|
|
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
|
|
Import Activity
|
|
</span>
|
|
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
|
|
</span>
|
|
<div className="mt-5 flex flex-col gap-4 sm:mt-0 sm:flex-row sm:items-center">
|
|
<dl className="grid grid-cols-2 gap-4 sm:grid-cols-2">
|
|
{/* Successful import counts */}
|
|
<div className="flex flex-col rounded-lg bg-green-100 dark:bg-green-200 px-4 py-6 text-center">
|
|
<dd className="text-3xl text-green-600 md:text-5xl">
|
|
{importJobQueue.successfulJobCount}
|
|
</dd>
|
|
<dt className="text-lg font-medium text-gray-500">
|
|
imported
|
|
</dt>
|
|
</div>
|
|
{/* Failed job counts */}
|
|
<div className="flex flex-col rounded-lg bg-red-100 dark:bg-red-200 px-4 py-6 text-center">
|
|
<dd className="text-3xl text-red-600 md:text-5xl">
|
|
{importJobQueue.failedJobCount}
|
|
</dd>
|
|
<dt className="text-lg font-medium text-gray-500">
|
|
failed
|
|
</dt>
|
|
</div>
|
|
|
|
<div className="flex flex-col dark:text-slate-200 text-slate-400">
|
|
<dd>{renderQueueControls(importJobQueue.status)}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
<div className="flex">
|
|
<span className="mt-2 dark:text-slate-200 text-slate-400">
|
|
Imported: <span>{importJobQueue.mostRecentImport}</span>
|
|
</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Past imports */}
|
|
{!isLoading && !isEmpty(data?.data) && (
|
|
<div className="max-w-screen-lg">
|
|
<span className="flex items-center mt-6">
|
|
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
|
|
Past Imports
|
|
</span>
|
|
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
|
|
</span>
|
|
|
|
<div className="overflow-x-auto mt-4 rounded-lg border border-gray-200">
|
|
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-200 text-md">
|
|
<thead className="ltr:text-left rtl:text-right">
|
|
<tr>
|
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
|
Time Started
|
|
</th>
|
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
|
Session Id
|
|
</th>
|
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
|
Imported
|
|
</th>
|
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
|
Failed
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody className="divide-y divide-gray-200">
|
|
{data?.data.map((jobResult, id) => {
|
|
return (
|
|
<tr key={id}>
|
|
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
|
{format(
|
|
new Date(jobResult.earliestTimestamp),
|
|
"EEEE, hh:mma, do LLLL Y",
|
|
)}
|
|
</td>
|
|
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
|
<span className="tag is-warning">
|
|
{jobResult.sessionId}
|
|
</span>
|
|
</td>
|
|
<td className="whitespace-nowrap px-4 py-2 text-gray-700 dark:text-slate-300">
|
|
<span className="inline-flex items-center justify-center rounded-full bg-emerald-100 px-2 py-0.5 text-emerald-700">
|
|
<span className="h-5 w-6">
|
|
<i className="icon-[solar--check-circle-line-duotone] h-5 w-5"></i>
|
|
</span>
|
|
<p className="whitespace-nowrap text-sm">
|
|
{jobResult.completedJobs}
|
|
</p>
|
|
</span>
|
|
</td>
|
|
<td className="whitespace-nowrap px-4 py-2 text-gray-700 dark:text-slate-300">
|
|
<span className="inline-flex items-center justify-center rounded-full bg-red-100 px-2 py-0.5 text-red-700">
|
|
<span className="h-5 w-6">
|
|
<i className="icon-[solar--close-circle-line-duotone] h-5 w-5"></i>
|
|
</span>
|
|
|
|
<p className="whitespace-nowrap text-sm">
|
|
{jobResult.failedJobs}
|
|
</p>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Import;
|