🏗️ Trying out react-query

This commit is contained in:
2023-10-30 13:47:35 -04:00
parent 2df0fce792
commit d45c53dff9
13 changed files with 117 additions and 393 deletions

View File

@@ -20,8 +20,8 @@
"@dnd-kit/utilities": "^3.2.1", "@dnd-kit/utilities": "^3.2.1",
"@fortawesome/fontawesome-free": "^6.3.0", "@fortawesome/fontawesome-free": "^6.3.0",
"@redux-devtools/extension": "^3.2.5", "@redux-devtools/extension": "^3.2.5",
"@reduxjs/toolkit": "^1.9.7",
"@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-node-resolve": "^15.0.1",
"@tanstack/react-query": "^5.0.5",
"@tanstack/react-table": "^8.9.3", "@tanstack/react-table": "^8.9.3",
"@types/mime-types": "^2.1.0", "@types/mime-types": "^2.1.0",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
@@ -56,7 +56,6 @@
"react-loader-spinner": "^4.0.0", "react-loader-spinner": "^4.0.0",
"react-masonry-css": "^1.0.16", "react-masonry-css": "^1.0.16",
"react-modal": "^3.15.1", "react-modal": "^3.15.1",
"react-redux": "^8.0.5",
"react-router": "^6.9.0", "react-router": "^6.9.0",
"react-router-dom": "^6.9.0", "react-router-dom": "^6.9.0",
"react-select": "^5.3.2", "react-select": "^5.3.2",
@@ -66,9 +65,6 @@
"react-stickynode": "^4.1.0", "react-stickynode": "^4.1.0",
"react-textarea-autosize": "^8.3.4", "react-textarea-autosize": "^8.3.4",
"reapop": "^4.2.1", "reapop": "^4.2.1",
"redux-first-history": "^5.1.1",
"redux-socket.io-middleware": "^1.0.4",
"redux-thunk": "^2.4.2",
"slick-carousel": "^1.8.1", "slick-carousel": "^1.8.1",
"socket.io-client": "^4.3.2", "socket.io-client": "^4.3.2",
"styled-components": "^6.0.7", "styled-components": "^6.0.7",
@@ -86,6 +82,8 @@
"@storybook/react": "^7.4.1", "@storybook/react": "^7.4.1",
"@storybook/react-vite": "^7.4.1", "@storybook/react-vite": "^7.4.1",
"@storybook/testing-library": "^0.2.0", "@storybook/testing-library": "^0.2.0",
"@tanstack/eslint-plugin-query": "^5.0.5",
"@tanstack/react-query-devtools": "^5.1.0",
"@tsconfig/node14": "^1.0.0", "@tsconfig/node14": "^1.0.0",
"@types/ellipsize": "^0.1.1", "@types/ellipsize": "^0.1.1",
"@types/express": "^4.17.8", "@types/express": "^4.17.8",

View File

@@ -23,7 +23,11 @@ import {
AIRDCPP_DOWNLOAD_PROGRESS_TICK, AIRDCPP_DOWNLOAD_PROGRESS_TICK,
LS_SINGLE_IMPORT, LS_SINGLE_IMPORT,
} from "../constants/action-types"; } from "../constants/action-types";
import { useDispatch, useSelector } from "react-redux";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient({});
/** /**
* Method that initializes an AirDC++ socket connection * Method that initializes an AirDC++ socket connection
@@ -32,128 +36,93 @@ import { useDispatch, useSelector } from "react-redux";
* @returns void * @returns void
*/ */
const AirDCPPSocketComponent = (): ReactElement => { const AirDCPPSocketComponent = (): ReactElement => {
const airDCPPConfiguration = useContext(AirDCPPSocketContext); // const airDCPPConfiguration = useContext(AirDCPPSocketContext);
const dispatch = useDispatch(); // const dispatch = useDispatch();
//
useEffect(() => { // useEffect(() => {
const initializeAirDCPPEventListeners = async () => { // const initializeAirDCPPEventListeners = async () => {
if ( // if (
!isUndefined(airDCPPConfiguration.airDCPPState) && // !isUndefined(airDCPPConfiguration.airDCPPState) &&
!isEmpty(airDCPPConfiguration.airDCPPState.settings) && // !isEmpty(airDCPPConfiguration.airDCPPState.settings) &&
!isEmpty(airDCPPConfiguration.airDCPPState.socket) // !isEmpty(airDCPPConfiguration.airDCPPState.socket)
) { // ) {
await airDCPPConfiguration.airDCPPState.socket.addListener( // await airDCPPConfiguration.airDCPPState.socket.addListener(
"queue", // "queue",
"queue_bundle_added", // "queue_bundle_added",
async (data) => { // async (data) => {
console.log("JEMEN:", data); // console.log("JEMEN:", data);
}, // },
); // );
// download tick listener // // download tick listener
await airDCPPConfiguration.airDCPPState.socket.addListener( // await airDCPPConfiguration.airDCPPState.socket.addListener(
`queue`, // `queue`,
"queue_bundle_tick", // "queue_bundle_tick",
async (downloadProgressData) => { // async (downloadProgressData) => {
dispatch({ // dispatch({
type: AIRDCPP_DOWNLOAD_PROGRESS_TICK, // type: AIRDCPP_DOWNLOAD_PROGRESS_TICK,
downloadProgressData, // downloadProgressData,
}); // });
}, // },
); // );
// download complete listener // // download complete listener
await airDCPPConfiguration.airDCPPState.socket.addListener( // await airDCPPConfiguration.airDCPPState.socket.addListener(
`queue`, // `queue`,
"queue_bundle_status", // "queue_bundle_status",
async (bundleData) => { // async (bundleData) => {
let count = 0; // let count = 0;
if (bundleData.status.completed && bundleData.status.downloaded) { // if (bundleData.status.completed && bundleData.status.downloaded) {
// dispatch the action for raw import, with the metadata // // dispatch the action for raw import, with the metadata
if (count < 1) { // if (count < 1) {
console.log(`[AirDCPP]: Download complete.`); // console.log(`[AirDCPP]: Download complete.`);
dispatch({ // dispatch({
type: LS_SINGLE_IMPORT, // type: LS_SINGLE_IMPORT,
meta: { remote: true }, // meta: { remote: true },
data: bundleData, // data: bundleData,
}); // });
count += 1; // count += 1;
} // }
} // }
}, // },
); // );
console.log( // console.log(
"[AirDCPP]: Listener registered - listening to queue bundle download ticks", // "[AirDCPP]: Listener registered - listening to queue bundle download ticks",
); // );
console.log( // console.log(
"[AirDCPP]: Listener registered - listening to queue bundle changes", // "[AirDCPP]: Listener registered - listening to queue bundle changes",
); // );
console.log( // console.log(
"[AirDCPP]: Listener registered - listening to transfer completion", // "[AirDCPP]: Listener registered - listening to transfer completion",
); // );
} // }
}; // };
initializeAirDCPPEventListeners(); // initializeAirDCPPEventListeners();
}, [airDCPPConfiguration]); // }, [airDCPPConfiguration]);
return <></>; return <></>;
}; };
export const App = (): ReactElement => { export const App = (): ReactElement => {
const dispatch = useDispatch(); // useEffect(() => {
useEffect(() => { // // Check if there is a sessionId in localStorage
// Check if there is a sessionId in localStorage // const sessionId = localStorage.getItem("sessionId");
const sessionId = localStorage.getItem("sessionId"); // if (!isNil(sessionId)) {
if (!isNil(sessionId)) { // // Resume the session
// Resume the session // dispatch({
dispatch({ // type: "RESUME_SESSION",
type: "RESUME_SESSION", // meta: { remote: true },
meta: { remote: true }, // session: { sessionId },
session: { sessionId }, // });
}); // } else {
} else { // // Inititalize the session and persist the sessionId to localStorage
// Inititalize the session and persist the sessionId to localStorage // socketIOConnectionInstance.on("sessionInitialized", (sessionId) => {
socketIOConnectionInstance.on("sessionInitialized", (sessionId) => { // localStorage.setItem("sessionId", sessionId);
localStorage.setItem("sessionId", sessionId); // });
}); // }
} // }, []);
}, []);
return ( return (
<SocketIOProvider socket={socketIOConnectionInstance}> <QueryClientProvider client={queryClient}>
<AirDCPPSocketContextProvider> {/* The rest of your application */}
<div> <ReactQueryDevtools initialIsOpen={true} />
<AirDCPPSocketComponent /> {/* <AirDCPPSocketComponent /> */};
<Routes> </QueryClientProvider>
<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

@@ -15,7 +15,6 @@ import { isEmpty, isNil } from "lodash";
import Header from "../shared/Header"; import Header from "../shared/Header";
export const Dashboard = (): ReactElement => { export const Dashboard = (): ReactElement => {
const dispatch = useDispatch();
// useEffect(() => { // useEffect(() => {
// dispatch(fetchVolumeGroups()); // dispatch(fetchVolumeGroups());
// dispatch( // dispatch(

View File

@@ -1,13 +1,10 @@
import React, { ReactElement, useCallback } from "react"; import React, { ReactElement, useCallback } from "react";
import { saveSettings } from "../../../actions/settings.actions"; import { saveSettings } from "../../../actions/settings.actions";
import { ConnectionForm } from "../../shared/ConnectionForm/ConnectionForm"; import { ConnectionForm } from "../../shared/ConnectionForm/ConnectionForm";
import { useConnectToQBittorrentClientQuery } from "../../../services/torrents.api";
export const QbittorrentConnectionForm = (): ReactElement => { export const QbittorrentConnectionForm = (): ReactElement => {
const { data, isLoading } = useConnectToQBittorrentClientQuery({});
const onSubmit = useCallback(async (values) => { const onSubmit = useCallback(async (values) => {
try { try {
dispatch(saveSettings(values, "bittorrent"));
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
@@ -15,13 +12,11 @@ export const QbittorrentConnectionForm = (): ReactElement => {
return ( return (
<> <>
{!isLoading && ( <ConnectionForm
<ConnectionForm initialData={data?.bittorrent.client.host}
initialData={data?.bittorrent.client.host} submitHandler={onSubmit}
submitHandler={onSubmit} formHeading={"Qbittorrent Configuration"}
formHeading={"Qbittorrent Configuration"} />
/>
)}
<pre>{JSON.stringify(data?.qbittorrentClientInfo, null, 2)}</pre> <pre>{JSON.stringify(data?.qbittorrentClientInfo, null, 2)}</pre>
</> </>
); );

View File

@@ -7,22 +7,18 @@ import { ServiceStatuses } from "../ServiceStatuses/ServiceStatuses";
import settingsObject from "../../constants/settings/settingsMenu.json"; import settingsObject from "../../constants/settings/settingsMenu.json";
import { isUndefined, map } from "lodash"; import { isUndefined, map } from "lodash";
interface ISettingsProps { } interface ISettingsProps {}
export const Settings = (props: ISettingsProps): ReactElement => { export const Settings = (props: ISettingsProps): ReactElement => {
const [active, setActive] = useState("gen-db"); const [active, setActive] = useState("gen-db");
const settingsContent = [ const settingsContent = [
{ {
id: "adc-hubs", id: "adc-hubs",
content: <div key="adc-hubs">{<AirDCPPHubsForm />}</div>, content: <div key="adc-hubs">{/* <AirDCPPHubsForm /> */}</div>,
}, },
{ {
id: "adc-connection", id: "adc-connection",
content: ( content: <div key="adc-connection">{/* <AirDCPPSettingsForm /> */}</div>,
<div key="adc-connection">
<AirDCPPSettingsForm />
</div>
),
}, },
{ {
id: "qbt-connection", id: "qbt-connection",
@@ -34,15 +30,11 @@ export const Settings = (props: ISettingsProps): ReactElement => {
}, },
{ {
id: "core-service", id: "core-service",
content: <ServiceStatuses />, content: <>a</>,
}, },
{ {
id: "flushdb", id: "flushdb",
content: ( content: <div key="flushdb">{/* <SystemSettingsForm /> */}</div>,
<div key="flushdb">
<SystemSettingsForm />
</div>
),
}, },
]; ];
return ( return (

View File

@@ -2,46 +2,10 @@ import React, { useContext } from "react";
import { SearchBar } from "../GlobalSearchBar/SearchBar"; import { SearchBar } from "../GlobalSearchBar/SearchBar";
import { DownloadProgressTick } from "../ComicDetail/DownloadProgressTick"; import { DownloadProgressTick } from "../ComicDetail/DownloadProgressTick";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
import { isUndefined } from "lodash"; import { isUndefined } from "lodash";
import { format, fromUnixTime } from "date-fns"; import { format, fromUnixTime } from "date-fns";
const Navbar: React.FunctionComponent = (props) => { const Navbar: React.FunctionComponent = (props) => {
const downloadProgressTick = useSelector(
(state: RootState) => state.airdcpp.downloadProgressData,
);
const airDCPPSocketConnectionStatus = useSelector(
(state: RootState) => state.airdcpp.isAirDCPPSocketConnected,
);
const airDCPPSessionInfo = useSelector(
(state: RootState) => state.airdcpp.airDCPPSessionInfo,
);
const qBittorrentConnectionInfo = useSelector(
(state: RootState) => state.settings.data,
);
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 ( return (
<nav className="navbar is-fixed-top"> <nav className="navbar is-fixed-top">
<div className="navbar-brand"> <div className="navbar-brand">
@@ -91,8 +55,6 @@ const Navbar: React.FunctionComponent = (props) => {
Downloads Downloads
</Link> </Link>
<SearchBar />
<Link to="/search" className="navbar-item"> <Link to="/search" className="navbar-item">
Search ComicVine Search ComicVine
</Link> </Link>
@@ -101,102 +63,7 @@ const Navbar: React.FunctionComponent = (props) => {
<div className="navbar-end"> <div className="navbar-end">
<a className="navbar-item is-hidden-desktop-only"></a> <a className="navbar-item is-hidden-desktop-only"></a>
<div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link is-arrowless">
<i className="fa-solid fa-download"></i>
{downloadProgressTick && <div className="pulsating-circle"></div>}
</a>
{!isUndefined(downloadProgressTick) ? (
<div className="navbar-dropdown is-right is-boxed">
<a className="navbar-item">
<DownloadProgressTick data={downloadProgressTick} />
</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 */} {/* AirDC++ socket connection status */}
<div className="navbar-item has-dropdown is-hoverable">
{airDCPPSocketConnectionStatus ? (
<>
<a className="navbar-link is-arrowless">
<i className="fa-solid fa-tower-cell"></i>
</a>
<div className="navbar-dropdown pr-2 pl-2 is-right airdcpp-status is-boxed">
{/* AirDC++ Session Information */}
<p>
Last login was{" "}
<span className="tag">
{format(
fromUnixTime(airDCPPSessionInfo.user.last_login),
"dd MMMM, yyyy",
)}
</span>
</p>
<hr className="navbar-divider" />
<p>
<span className="tag has-text-success">
{airDCPPSessionInfo.user.username}
</span>
connected to{" "}
<span className="tag has-text-success">
{airDCPPSessionInfo.system_info.client_version}
</span>{" "}
with session ID{" "}
<span className="tag has-text-success">
{airDCPPSessionInfo.session_id}
</span>
</p>
{/* <pre>{JSON.stringify(airDCPPSessionInfo, null, 2)}</pre> */}
</div>
</>
) : (
<>
<a className="navbar-link is-arrowless has-text-danger">
<i className="fa-solid fa-bolt"></i>
</a>
<div className="navbar-dropdown pr-2 pl-2 is-right is-boxed">
<pre>
{JSON.stringify(socketDisconnectionReason, null, 2)}
</pre>
</div>
</>
)}
</div>
<div className="navbar-item has-dropdown is-hoverable is-mega"> <div className="navbar-item has-dropdown is-hoverable is-mega">
<div className="navbar-link flex">Blog</div> <div className="navbar-link flex">Blog</div>

View File

@@ -1,6 +0,0 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "../store";
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@@ -1,18 +1,25 @@
import React from "react"; import React from "react";
import { render } from "react-dom"; import { render } from "react-dom";
import { Provider, connect } from "react-redux";
import { HistoryRouter as Router } from "redux-first-history/rr6";
import { store, history } from "./store/index";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import App from "./components/App"; import App from "./components/App";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Settings from "./components/Settings/Settings";
const rootEl = document.getElementById("root"); const rootEl = document.getElementById("root");
const root = createRoot(rootEl); const root = createRoot(rootEl);
const router = createBrowserRouter([
{
path: "/",
element: <App />,
},
{
path: "/settings",
element: <Settings />,
},
]);
root.render( root.render(
<Provider store={store}> <React.StrictMode>
<Router history={history}> <RouterProvider router={router} />
<App /> </React.StrictMode>,
</Router>
</Provider>,
); );

View File

@@ -1,7 +0,0 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
// initialize an empty api service that we'll inject endpoints into later as needed
export const emptySplitApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: "http://" }),
endpoints: () => ({}),
});

View File

@@ -1,13 +0,0 @@
import { emptySplitApi } from "./empty.api";
import { useConnectToQBittorrentClientQuery } from "./torrents.api";
export const settingsApi = emptySplitApi.injectEndpoints({
endpoints: (builder) => ({
getAllSettings: builder.query({
query: () => "localhost:3000/api/settings/getAllSettings",
}),
}),
overrideExisting: false,
});
export const { useGetAllSettingsQuery } = settingsApi;

View File

@@ -1,33 +0,0 @@
import { emptySplitApi } from "./empty.api";
export const torrentsApi = emptySplitApi.injectEndpoints({
endpoints: (builder) => ({
connectToQBittorrentClient: builder.query({
queryFn: async (_arg, _queryApi, _extraOptions, fetchWithBQ) => {
try {
const {
data: { bittorrent },
} = await fetchWithBQ("localhost:3000/api/settings/getAllSettings");
await fetchWithBQ({
url: "localhost:3060/api/qbittorrent/connect",
method: "POST",
body: bittorrent?.client?.host,
});
const { data } = await fetchWithBQ({
url: "localhost:3060/api/qbittorrent/getClientInfo",
method: "GET",
});
return {
data: { bittorrent, qbittorrentClientInfo: data },
};
} catch (err) {
throw err;
}
},
}),
}),
overrideExisting: false,
});
export const { useConnectToQBittorrentClientQuery } = torrentsApi;

View File

@@ -1,44 +0,0 @@
import { createHashHistory } from "history";
import thunk from "redux-thunk";
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import { createReduxHistoryContext } from "redux-first-history";
import socketIoMiddleware from "redux-socket.io-middleware";
import socketIOMiddleware from "../shared/middleware/SocketIOMiddleware";
import socketIOConnectionInstance from "../shared/socket.io/instance";
import settingsReducer from "../reducers/settings.reducer";
import { settingsApi } from "../services/settings.api";
const customSocketIOMiddleware = socketIOMiddleware(socketIOConnectionInstance);
const { createReduxHistory, routerMiddleware, routerReducer } =
createReduxHistoryContext({
history: createHashHistory(),
});
const rootReducer = (history) => ({
settings: settingsReducer,
[settingsApi.reducerPath]: settingsApi.reducer,
router: routerReducer,
});
const preloadedState = {};
export const store = configureStore({
middleware: [
socketIoMiddleware(socketIOConnectionInstance),
customSocketIOMiddleware,
thunk,
routerMiddleware,
settingsApi.middleware,
],
reducer: rootReducer(createHashHistory()),
preloadedState,
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
export const history = createReduxHistory(store);