🏗️ Refactored Import page to read from global state

This commit is contained in:
2023-11-09 10:21:20 -06:00
parent 18d2624d6c
commit 214f29f9a8
9 changed files with 163 additions and 200 deletions

View File

@@ -40,6 +40,7 @@
"final-form-arrays": "^3.0.2",
"history": "^5.3.0",
"html-to-text": "^8.1.0",
"immer": "^10.0.3",
"jsdoc": "^3.6.10",
"lodash": "^4.17.21",
"pretty-bytes": "^5.6.0",

View File

@@ -1,5 +1,4 @@
import React, { ReactElement, useCallback, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import {
fetchComicBookMetadata,
getImportJobResultStatistics,
@@ -9,6 +8,10 @@ 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 } from "@tanstack/react-query";
import { useStore } from "../../store";
import { useShallow } from "zustand/react/shallow";
import axios from "axios";
interface IProps {
matches?: unknown;
@@ -31,41 +34,62 @@ 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 { importJobQueue, socketIOInstance } = useStore(
useShallow((state) => ({
importJobQueue: state.importJobQueue,
socketIOInstance: state.socketIOInstance,
})),
);
// 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,
// );
const lastQueueJob = useSelector(
(state: RootState) => state.fileOps.lastQueueJob,
);
const libraryQueueImportStatus = useSelector(
(state: RootState) => state.fileOps.LSQueueImportStatus,
);
const allImportJobResults = useSelector(
(state: RootState) => state.fileOps.importJobStatistics,
);
const initiateImport = useCallback(() => {
if (typeof props.path !== "undefined") {
dispatch(fetchComicBookMetadata(props.path));
}
}, [dispatch]);
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 },
}),
});
// Act on each comic issue successfully imported, as indicated
// by the LS_COVER_EXTRACTED event
socketIOInstance.on("LS_COVER_EXTRACTED", (data) => {
const { completedJobCount } = data;
importJobQueue.setJobCount("successful", completedJobCount);
});
socketIOInstance.on("LS_COVER_EXTRACTION_FAILED", (data) => {
console.log(data);
const { failedJobCount } = data;
importJobQueue.setJobCount("failed", failedJobCount);
});
const toggleQueue = useCallback(
(queueAction: string, queueStatus: string) => {
dispatch(setQueueControl(queueAction, queueStatus));
// dispatch(setQueueControl(queueAction, queueStatus));
},
[],
);
useEffect(() => {
dispatch(getImportJobResultStatistics());
// dispatch(getImportJobResultStatistics());
}, []);
const libraryQueueImportStatus = undefined;
const renderQueueControls = (status: string): ReactElement | null => {
switch (status) {
case "running":
@@ -128,7 +152,7 @@ export const Import = (props: IProps): ReactElement => {
? "button is-medium"
: "button is-loading is-medium"
}
onClick={initiateImport}
onClick={() => initiateImport()}
>
<span className="icon">
<i className="fas fa-file-import"></i>
@@ -136,60 +160,58 @@ export const Import = (props: IProps): ReactElement => {
<span>Start Import</span>
</button>
</p>
{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>
<>
<table className="table">
<thead>
<tr>
<th>Completed Jobs</th>
<th>Failed Jobs</th>
<th>Queue Controls</th>
<th>Queue Status</th>
</tr>
</thead>
<td>{renderQueueControls(libraryQueueImportStatus)}</td>
<tbody>
<tr>
<th>
{importJobQueue.successfulJobCount > 0 && (
<div className="box has-background-success-light has-text-centered">
<span className="is-size-2 has-text-weight-bold">
{importJobQueue.successfulJobCount}
</span>
</div>
)}
</th>
<td>
{importJobQueue.failedJobCount > 0 && (
<div className="box has-background-danger has-text-centered">
<span className="is-size-2 has-text-weight-bold">
{importJobQueue.failedJobCount}
</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>
</>
)}
</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">
{/* <table className="table">
<thead>
<tr>
<th>Time Started</th>
@@ -228,7 +250,7 @@ export const Import = (props: IProps): ReactElement => {
);
})}
</tbody>
</table>
</table> */}
</section>
</div>
);

View File

@@ -38,7 +38,11 @@ export const Settings = (props: ISettingsProps): ReactElement => {
},
{
id: "flushdb",
content: <div key="flushdb">{/* <SystemSettingsForm /> */}</div>,
content: (
<div key="flushdb">
<SystemSettingsForm />
</div>
),
},
];
return (

View File

@@ -1,15 +1,16 @@
import React, { ReactElement, useCallback } from "react";
import { flushDb } from "../../../actions/settings.actions";
import { useDispatch, useSelector } from "react-redux";
import React, { ReactElement } from "react";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
export const SystemSettingsForm = (): ReactElement => {
const dispatch = useDispatch();
const isSettingsCallInProgress = useSelector(
(state: RootState) => state.settings.inProgress,
);
const flushDatabase = useCallback(() => {
dispatch(flushDb());
}, []);
const { mutate: flushDb, isLoading } = useMutation({
mutationFn: async () => {
await axios({
url: `http://localhost:3000/api/library/flushDb`,
method: "POST",
});
},
});
return (
<div className="is-clearfix">
@@ -48,11 +49,9 @@ export const SystemSettingsForm = (): ReactElement => {
<button
className={
isSettingsCallInProgress
? "button is-danger is-loading"
: "button is-danger"
isLoading ? "button is-danger is-loading" : "button is-danger"
}
onClick={flushDatabase}
onClick={() => flushDb()}
>
<span className="icon">
<i className="fas fa-eraser"></i>

View File

@@ -13,15 +13,11 @@ const Navbar: React.FunctionComponent = (props) => {
airDCPPDisconnectionInfo,
airDCPPSessionInformation,
airDCPPDownloadTick,
airDCPPClientConfiguration,
airDCPPSocketInstance,
} = useStore(
useShallow((state) => ({
airDCPPSocketConnected: state.airDCPPSocketConnected,
airDCPPDisconnectionInfo: state.airDCPPDisconnectionInfo,
airDCPPClientConfiguration: state.airDCPPClientConfiguration,
airDCPPSessionInformation: state.airDCPPSessionInformation,
airDCPPSocketInstance: state.airDCPPSocketInstance,
airDCPPDownloadTick: state.airDCPPDownloadTick,
})),
);

View File

@@ -1,108 +0,0 @@
import { isEmpty, isUndefined } from "lodash";
import React, { createContext, useEffect, useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { useStore } from "../store/index";
import AirDCPPSocket from "../services/DcppSearchService";
import axios from "axios";
const AirDCPPSocketContextProvider = ({ children }) => {
const { getState, setState } = useStore;
// setter for settings for use in the context consumer
const setSettings = (settingsObject) => {
persistSettings({
...airDCPPState,
airDCPPState: {
settings: settingsObject,
socket: {},
socketConnectionInformation: {},
},
});
};
// Initial state for AirDC++ configuration
const initState = {
airDCPPState: {
settings: {},
socket: {},
socketConnectionInformation: {},
},
setSettings: setSettings,
};
const [airDCPPState, persistSettings] = useState(initState);
// 1. get settings from mongo
const { data, isLoading, isError } = useQuery({
queryKey: ["settings"],
queryFn: async () =>
await axios({
url: "http://localhost:3000/api/settings/getAllSettings",
method: "GET",
}),
});
const directConnectConfiguration = data?.data.directConnect.client.host;
// 2. If available, init AirDC++ Socket with those settings
useEffect(() => {
if (!isEmpty(directConnectConfiguration)) {
initializeAirDCPPSocket(directConnectConfiguration);
}
}, [directConnectConfiguration]);
// Method to init AirDC++ Socket with supplied settings
const initializeAirDCPPSocket = async (configuration) => {
console.log("[AirDCPP]: Initializing socket...");
const initializedAirDCPPSocket = new AirDCPPSocket({
protocol: `${configuration.protocol}`,
hostname: `${configuration.hostname}:${configuration.port}`,
username: `${configuration.username}`,
password: `${configuration.password}`,
});
// Set up connect and disconnect handlers
initializedAirDCPPSocket.onConnected = (sessionInfo) => {
// update global state with socket connection status
setState({
airDCPPSocketConnected: true,
});
};
initializedAirDCPPSocket.onDisconnected = async (
reason,
code,
wasClean,
) => {
// update global state with socket connection status
setState({
disconnectionInfo: { reason, code, wasClean },
airDCPPSocketConnected: false,
});
};
// Attempt connection
const socketConnectionInformation =
await initializedAirDCPPSocket.connect();
// update the state with the new socket connection information
persistSettings({
...airDCPPState,
airDCPPState: {
settings: configuration,
socket: initializedAirDCPPSocket,
socketConnectionInformation,
},
});
};
console.log("connected?", getState());
// the Provider gives access to the context to its children
return (
<AirDCPPSocketContext.Provider value={airDCPPState}>
{children}
</AirDCPPSocketContext.Provider>
);
};
const AirDCPPSocketContext = createContext({
airDCPPState: {},
saveSettings: () => {},
});
export { AirDCPPSocketContext, AirDCPPSocketContextProvider };

View File

@@ -9,6 +9,7 @@ const rootEl = document.getElementById("root");
const root = createRoot(rootEl);
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import Import from "./components/Import/Import";
const queryClient = new QueryClient();
@@ -17,7 +18,10 @@ const router = createBrowserRouter([
path: "/",
element: <App />,
errorElement: <ErrorPage />,
children: [{ path: "settings", element: <Settings /> }],
children: [
{ path: "settings", element: <Settings /> },
{ path: "import", element: <Import path={"./comics"} /> },
],
},
]);

View File

@@ -1,5 +1,8 @@
import { create } from "zustand";
import { isEmpty } from "lodash";
import io from "socket.io-client";
import { SOCKET_BASE_URI } from "../constants/endpoints";
import { produce } from "immer";
import AirDCPPSocket from "../services/DcppSearchService";
import axios from "axios";
@@ -14,15 +17,49 @@ export const useStore = create((set, get) => ({
set((value) => ({
airDCPPSocketConnected: value,
})),
getAirDCPPConnectionStatus: () => {
const airDCPPSocketConnectionStatus = get().airDCPPSocketConnected;
},
airDCPPDownloadTick: {},
// Socket.io state
socketIOInstance: {},
// Import job results
importJobQueue: {
successfulJobCount: 0,
failedJobCount: 0,
setJobCount: (jobType: string, count: Number) => {
switch (jobType) {
case "successful":
set(
produce((state) => {
state.importJobQueue.successfulJobCount = count;
}),
);
break;
case "failed":
set(
produce((state) => {
state.importJobQueue.failedJobCount = count;
}),
);
break;
}
},
},
}));
const { getState, setState } = useStore;
// socket.io instantiation
const sessionId = localStorage.getItem("sessionId");
const socketIOInstance = io(SOCKET_BASE_URI, {
transports: ["websocket"],
withCredentials: true,
query: { sessionId },
});
setState({
socketIOInstance,
});
/**
* Method to init AirDC++ Socket with supplied settings
* @param configuration - credentials, and hostname details to init AirDC++ connection

View File

@@ -6345,6 +6345,11 @@ ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
immer@^10.0.3:
version "10.0.3"
resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.3.tgz#a8de42065e964aa3edf6afc282dfc7f7f34ae3c9"
integrity sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==
immutable@^4.0.0, immutable@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f"
@@ -9501,6 +9506,7 @@ string-length@^4.0.1:
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -9575,6 +9581,7 @@ string_decoder@~1.1.1:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
name strip-ansi-cjs
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -10388,6 +10395,7 @@ wordwrap@^1.0.0:
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
name wrap-ansi-cjs
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==