🛠 changed import to account for graphql

This commit is contained in:
2026-02-24 13:39:50 -05:00
parent dc014a08ce
commit ea66419f33
5 changed files with 457 additions and 191 deletions

View File

@@ -5,8 +5,7 @@ import React, {
useRef,
useState,
} from "react";
import { SearchQuery, PriorityEnum, SearchResponse } from "threetwo-ui-typings";
import { RootState, SearchInstance } from "threetwo-ui-typings";
import { SearchQuery, PriorityEnum, SearchResponse, SearchInstance } from "threetwo-ui-typings";
import ellipsize from "ellipsize";
import { Form, Field } from "react-final-form";
import { difference } from "../../shared/utils/object.utils";
@@ -15,44 +14,104 @@ import { useStore } from "../../store";
import { useShallow } from "zustand/react/shallow";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
import { AIRDCPP_SERVICE_BASE_URI } from "../../constants/endpoints";
import { AIRDCPP_SERVICE_BASE_URI, SETTINGS_SERVICE_BASE_URI } from "../../constants/endpoints";
import type { Socket } from "socket.io-client";
interface IAcquisitionPanelProps {
query: any;
comicObjectId: any;
comicObjectId: string;
comicObject: any;
settings: any;
}
interface AirDCPPConfig {
protocol: string;
hostname: string;
username: string;
password: string;
}
interface SearchResult {
id: string;
name: string;
type: {
id: string;
str: string;
};
size: number;
slots: {
total: number;
free: number;
};
users: {
user: {
nicks: string;
flags: string[];
};
};
dupe?: any;
}
interface SearchInstanceData {
id: number;
owner: string;
expires_in: number;
}
interface SearchInfo {
query: {
pattern: string;
extensions: string[];
file_type: string;
};
}
interface Hub {
hub_url: string;
identity: {
name: string;
};
value: string;
}
interface SearchFormValues {
issueName: string;
}
export const AcquisitionPanel = (
props: IAcquisitionPanelProps,
): ReactElement => {
const socketRef = useRef<Socket>();
const queryClient = useQueryClient();
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [dcppQuery, setDcppQuery] = useState({});
const [airDCPPSearchResults, setAirDCPPSearchResults] = useState<any[]>([]);
const [dcppQuery, setDcppQuery] = useState<SearchQuery | null>(null);
const [airDCPPSearchResults, setAirDCPPSearchResults] = useState<SearchResult[]>([]);
const [airDCPPSearchStatus, setAirDCPPSearchStatus] = useState(false);
const [airDCPPSearchInstance, setAirDCPPSearchInstance] = useState<any>({});
const [airDCPPSearchInfo, setAirDCPPSearchInfo] = useState<any>({});
const [isSearching, setIsSearching] = useState(false);
const [airDCPPSearchInstance, setAirDCPPSearchInstance] = useState<SearchInstanceData | null>(null);
const [airDCPPSearchInfo, setAirDCPPSearchInfo] = useState<SearchInfo | null>(null);
const [searchError, setSearchError] = useState<string | null>(null);
const { comicObjectId } = props;
const issueName = props.query.issue.name || "";
const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " ");
// Search timeout duration in milliseconds (30 seconds)
const SEARCH_TIMEOUT_MS = 30000;
useEffect(() => {
const socket = useStore.getState().getSocket("manual");
socketRef.current = socket;
// --- Handlers ---
const handleResultAdded = ({ result }: any) => {
const handleResultAdded = ({ result }: { result: SearchResult }) => {
setAirDCPPSearchResults((prev) =>
prev.some((r) => r.id === result.id) ? prev : [...prev, result],
);
};
const handleResultUpdated = ({ result }: any) => {
const handleResultUpdated = ({ result }: { result: SearchResult }) => {
setAirDCPPSearchResults((prev) => {
const idx = prev.findIndex((r) => r.id === result.id);
if (idx === -1) return prev;
@@ -63,45 +122,86 @@ export const AcquisitionPanel = (
});
};
const handleSearchInitiated = (data: any) => {
const handleSearchInitiated = (data: { instance: SearchInstanceData }) => {
setAirDCPPSearchInstance(data.instance);
setIsSearching(true);
setSearchError(null);
// Clear any existing timeout
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
// Set a timeout to stop searching after SEARCH_TIMEOUT_MS
searchTimeoutRef.current = setTimeout(() => {
setIsSearching(false);
console.log(`Search timeout reached after ${SEARCH_TIMEOUT_MS / 1000} seconds`);
}, SEARCH_TIMEOUT_MS);
};
const handleSearchesSent = (data: any) => {
const handleSearchesSent = (data: { searchInfo: SearchInfo }) => {
setAirDCPPSearchInfo(data.searchInfo);
};
const handleSearchError = (error: { message: string }) => {
setSearchError(error.message || "Search failed");
setIsSearching(false);
// Clear timeout on error
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
searchTimeoutRef.current = null;
}
};
const handleSearchCompleted = () => {
setIsSearching(false);
// Clear timeout when search completes
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
searchTimeoutRef.current = null;
}
};
// --- Subscribe once ---
socket.on("searchResultAdded", handleResultAdded);
socket.on("searchResultUpdated", handleResultUpdated);
socket.on("searchInitiated", handleSearchInitiated);
socket.on("searchesSent", handleSearchesSent);
socket.on("searchError", handleSearchError);
socket.on("searchCompleted", handleSearchCompleted);
return () => {
socket.off("searchResultAdded", handleResultAdded);
socket.off("searchResultUpdated", handleResultUpdated);
socket.off("searchInitiated", handleSearchInitiated);
socket.off("searchesSent", handleSearchesSent);
// if you want to fully close the socket:
// useStore.getState().disconnectSocket("/manual");
socket.off("searchError", handleSearchError);
socket.off("searchCompleted", handleSearchCompleted);
// Clean up timeout on unmount
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
};
}, []);
}, [SEARCH_TIMEOUT_MS]);
const {
data: settings,
isLoading,
isError,
isLoading: isLoadingSettings,
isError: isSettingsError,
} = useQuery({
queryKey: ["settings"],
queryFn: async () =>
await axios({
url: "http://localhost:3000/api/settings/getAllSettings",
url: `${SETTINGS_SERVICE_BASE_URI}/getAllSettings`,
method: "GET",
}),
});
const { data: hubs } = useQuery({
queryKey: ["hubs"],
const { data: hubs, isLoading: isLoadingHubs } = useQuery({
queryKey: ["hubs", settings?.data.directConnect?.client?.host],
queryFn: async () =>
await axios({
url: `${AIRDCPP_SERVICE_BASE_URI}/getHubs`,
@@ -110,45 +210,72 @@ export const AcquisitionPanel = (
host: settings?.data.directConnect?.client?.host,
},
}),
enabled: !isEmpty(settings?.data.directConnect?.client?.host),
enabled: !!settings?.data?.directConnect?.client?.host,
});
// Get AirDC++ config from settings
const airDCPPConfig: AirDCPPConfig | null = settings?.data?.directConnect?.client
? {
protocol: settings.data.directConnect.client.protocol || "ws",
hostname: typeof settings.data.directConnect.client.host === 'string'
? settings.data.directConnect.client.host
: `${settings.data.directConnect.client.host?.hostname || 'localhost'}:${settings.data.directConnect.client.host?.port || '5600'}`,
username: settings.data.directConnect.client.username || "admin",
password: settings.data.directConnect.client.password || "password",
}
: null;
useEffect(() => {
const dcppSearchQuery = {
query: {
pattern: `${sanitizedIssueName.replace(/#/g, "")}`,
extensions: ["cbz", "cbr", "cb7"],
},
hub_urls: map(hubs?.data, (item) => item.value),
priority: 5,
};
setDcppQuery(dcppSearchQuery);
if (hubs?.data && Array.isArray(hubs.data) && hubs.data.length > 0) {
const dcppSearchQuery = {
query: {
pattern: `${sanitizedIssueName.replace(/#/g, "")}`,
extensions: ["cbz", "cbr", "cb7"],
},
hub_urls: map(hubs.data, (item) => item.value),
priority: 5,
};
setDcppQuery(dcppSearchQuery as any);
}
}, [hubs, sanitizedIssueName]);
const search = async (searchData: any) => {
if (!airDCPPConfig) {
setSearchError("AirDC++ configuration not found in settings");
return;
}
if (!socketRef.current) {
setSearchError("Socket connection not available");
return;
}
setAirDCPPSearchResults([]);
socketRef.current?.emit("call", "socket.search", {
setIsSearching(true);
setSearchError(null);
socketRef.current.emit("call", "socket.search", {
query: searchData,
namespace: "/manual",
config: {
protocol: `ws`,
hostname: `192.168.1.119:5600`,
username: `admin`,
password: `password`,
},
config: airDCPPConfig,
});
};
const download = async (
searchInstanceId: Number,
resultId: String,
comicObjectId: String,
name: String,
size: Number,
type: any,
config: any,
searchInstanceId: number,
resultId: string,
comicObjectId: string,
name: string,
size: number,
type: SearchResult["type"],
config: AirDCPPConfig,
): Promise<void> => {
socketRef.current?.emit(
if (!socketRef.current) {
console.error("Socket connection not available");
return;
}
socketRef.current.emit(
"call",
"socket.download",
{
@@ -160,17 +287,27 @@ export const AcquisitionPanel = (
type,
config,
},
(data: any) => console.log(data),
(data: any) => console.log("Download initiated:", data),
);
};
const getDCPPSearchResults = async (searchQuery) => {
const getDCPPSearchResults = async (searchQuery: SearchFormValues) => {
if (!searchQuery.issueName || searchQuery.issueName.trim() === "") {
setSearchError("Please enter a search term");
return;
}
if (!hubs?.data || !Array.isArray(hubs.data) || hubs.data.length === 0) {
setSearchError("No hubs configured");
return;
}
const manualQuery = {
query: {
pattern: `${searchQuery.issueName}`,
pattern: `${searchQuery.issueName.trim()}`,
extensions: ["cbz", "cbr", "cb7"],
},
hub_urls: [hubs?.data[0].hub_url],
hub_urls: [hubs.data[0].hub_url],
priority: 5,
};
@@ -180,7 +317,12 @@ export const AcquisitionPanel = (
return (
<>
<div className="mt-5 mb-3">
{!isEmpty(hubs?.data) ? (
{isLoadingSettings || isLoadingHubs ? (
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<i className="icon-[solar--refresh-bold-duotone] h-5 w-5 animate-spin" />
Loading configuration...
</div>
) : !isEmpty(hubs?.data) ? (
<Form
onSubmit={getDCPPSearchResults}
initialValues={{
@@ -200,20 +342,31 @@ export const AcquisitionPanel = (
{...input}
className="dark:bg-slate-400 bg-slate-300 py-2 px-2 rounded-l-md border-gray-300 h-10 min-w-full dark:text-slate-800 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
placeholder="Type an issue/volume name"
disabled={isSearching}
/>
<button
className="sm:mt-0 min-w-fit rounded-r-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"
className="sm:mt-0 min-w-fit rounded-r-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 disabled:opacity-50 disabled:cursor-not-allowed"
type="submit"
disabled={isSearching}
>
<div className="flex flex-row">
Search DC++
<div className="h-5 w-5 ml-2">
<img
src="/src/client/assets/img/airdcpp_logo.svg"
className="h-5 w-5"
/>
</div>
<div className="flex flex-row items-center">
{isSearching ? (
<>
<i className="icon-[solar--refresh-bold-duotone] h-5 w-5 animate-spin mr-2" />
Searching...
</>
) : (
<>
Search DC++
<div className="h-5 w-5 ml-2">
<img
src="/src/client/assets/img/airdcpp_logo.svg"
className="h-5 w-5"
/>
</div>
</>
)}
</div>
</button>
</div>
@@ -234,26 +387,36 @@ export const AcquisitionPanel = (
</article>
)}
</div>
{/* Search Error Display */}
{searchError && (
<article
role="alert"
className="mt-4 rounded-lg text-sm max-w-screen-md border-s-4 border-red-500 bg-red-50 p-4 dark:border-s-4 dark:border-red-600 dark:bg-red-300 dark:text-slate-600"
>
<strong>Error:</strong> {searchError}
</article>
)}
{/* configured hub */}
{!isEmpty(hubs?.data) && (
{!isEmpty(hubs?.data) && hubs?.data[0] && (
<span className="inline-flex items-center bg-green-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-green-300">
<span className="pr-1 pt-1">
<i className="icon-[solar--server-2-bold-duotone] w-5 h-5"></i>
</span>
{hubs && hubs?.data[0].hub_url}
{hubs.data[0].hub_url}
</span>
)}
{/* AirDC++ search instance details */}
{!isNil(airDCPPSearchInstance) &&
!isEmpty(airDCPPSearchInfo) &&
!isNil(hubs) && (
{airDCPPSearchInstance &&
airDCPPSearchInfo &&
hubs?.data && (
<div className="flex flex-row gap-3 my-5 font-hasklig">
<div className="block max-w-sm h-fit p-6 text-sm bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
<dl>
<dt>
<div className="mb-1">
{hubs?.data.map((value, idx: string) => (
{hubs.data.map((value: Hub, idx: number) => (
<span className="tag is-warning" key={idx}>
{value.identity.name}
</span>
@@ -293,7 +456,7 @@ export const AcquisitionPanel = (
{/* AirDC++ results */}
<div className="">
{!isNil(airDCPPSearchResults) && !isEmpty(airDCPPSearchResults) ? (
{airDCPPSearchResults.length > 0 ? (
<div className="overflow-x-auto max-w-full mt-6">
<table className="w-full table-auto text-sm text-gray-900 dark:text-slate-100">
<thead>
@@ -345,9 +508,9 @@ export const AcquisitionPanel = (
<i className="icon-[solar--user-rounded-bold-duotone] w-4 h-4"></i>
{users.user.nicks}
</span>
{users.user.flags.map((flag, idx) => (
{users.user.flags.map((flag: string, flagIdx: number) => (
<span
key={idx}
key={flagIdx}
className="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900"
>
<i className="icon-[solar--tag-horizontal-bold-duotone] w-4 h-4"></i>
@@ -378,23 +541,21 @@ export const AcquisitionPanel = (
{/* ACTIONS */}
<td className="px-2 py-3">
<button
className="inline-flex items-center gap-1 rounded border border-green-500 bg-green-500 px-2 py-1 text-xs font-medium text-white hover:bg-transparent hover:text-green-400 dark:border-green-300 dark:bg-green-300 dark:text-slate-900 dark:hover:bg-transparent"
onClick={() =>
download(
airDCPPSearchInstance.id,
id,
comicObjectId,
name,
size,
type,
{
protocol: `ws`,
hostname: `192.168.1.119:5600`,
username: `admin`,
password: `password`,
},
)
}
className="inline-flex items-center gap-1 rounded border border-green-500 bg-green-500 px-2 py-1 text-xs font-medium text-white hover:bg-transparent hover:text-green-400 dark:border-green-300 dark:bg-green-300 dark:text-slate-900 dark:hover:bg-transparent disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => {
if (airDCPPSearchInstance && airDCPPConfig) {
download(
airDCPPSearchInstance.id,
id,
comicObjectId,
name,
size,
type,
airDCPPConfig,
);
}
}}
disabled={!airDCPPSearchInstance || !airDCPPConfig}
>
Download
<i className="icon-[solar--download-bold-duotone] w-4 h-4"></i>
@@ -406,7 +567,7 @@ export const AcquisitionPanel = (
</tbody>
</table>
</div>
) : (
) : !isSearching ? (
<div className="">
<article
role="alert"
@@ -432,6 +593,11 @@ export const AcquisitionPanel = (
</div>
</article>
</div>
) : (
<div className="flex items-center justify-center gap-2 text-sm text-gray-600 dark:text-gray-400 mt-6 p-4">
<i className="icon-[solar--refresh-bold-duotone] h-6 w-6 animate-spin" />
Searching...
</div>
)}
</div>
</>

View File

@@ -6,6 +6,7 @@ import { isEmpty, isNil, isUndefined, map } from "lodash";
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
import { determineCoverFile } from "../../shared/utils/metadata.utils";
import Header from "../shared/Header";
import useEmblaCarousel from "embla-carousel-react";
type WantedComicsListProps = {
comics: any;
@@ -16,107 +17,126 @@ export const WantedComicsList = ({
}: WantedComicsListProps): ReactElement => {
const navigate = useNavigate();
// embla carousel
const [emblaRef, emblaApi] = useEmblaCarousel({
loop: false,
align: "start",
containScroll: "trimSnaps",
slidesToScroll: 1,
});
return (
<>
<div>
<Header
headerContent="Wanted Comics"
subHeaderContent="Comics marked as wanted from various sources"
iconClassNames="fa-solid fa-binoculars mr-2"
link={"/wanted"}
/>
<div className="grid grid-cols-5 gap-6 mt-3">
{map(
comics,
({
_id,
rawFileDetails,
sourcedMetadata: { comicvine, comicInfo, locg },
wanted,
}) => {
const isComicBookMetadataAvailable = !isUndefined(comicvine);
const consolidatedComicMetadata = {
rawFileDetails,
comicvine,
comicInfo,
locg,
};
<div className="overflow-hidden -mr-4 sm:-mr-8 lg:-mr-16 xl:-mr-20 2xl:-mr-24 mt-3">
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex">
{map(
comics,
(
{
_id,
rawFileDetails,
sourcedMetadata: { comicvine, comicInfo, locg },
wanted,
},
idx,
) => {
const isComicBookMetadataAvailable = !isUndefined(comicvine);
const consolidatedComicMetadata = {
rawFileDetails,
comicvine,
comicInfo,
locg,
};
const {
issueName,
url,
publisher = null,
} = determineCoverFile(consolidatedComicMetadata);
const titleElement = (
<Link to={"/comic/details/" + _id}>
{ellipsize(issueName, 20)}
<p>{publisher}</p>
</Link>
);
return (
<Card
key={_id}
orientation={"vertical-2"}
imageUrl={url}
hasDetails
title={issueName ? titleElement : <span>No Name</span>}
>
<div className="pb-1">
<div className="flex flex-row gap-2">
{/* Issue type */}
{isComicBookMetadataAvailable &&
!isNil(detectIssueTypes(comicvine.description)) ? (
<div className="my-2">
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--book-2-line-duotone] w-5 h-5"></i>
</span>
const {
issueName,
url,
publisher = null,
} = determineCoverFile(consolidatedComicMetadata);
const titleElement = (
<Link to={"/comic/details/" + _id}>
{ellipsize(issueName, 20)}
<p>{publisher}</p>
</Link>
);
return (
<div
key={idx}
className="flex-[0_0_200px] min-w-0 sm:flex-[0_0_220px] md:flex-[0_0_240px] lg:flex-[0_0_260px] xl:flex-[0_0_280px] pr-[15px]"
>
<Card
orientation={"vertical-2"}
imageUrl={url}
hasDetails
title={issueName ? titleElement : <span>No Name</span>}
>
<div className="pb-1">
<div className="flex flex-row gap-2">
{/* Issue type */}
{isComicBookMetadataAvailable &&
!isNil(detectIssueTypes(comicvine.description)) ? (
<div className="my-2">
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--book-2-line-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{
detectIssueTypes(comicvine.description)
.displayName
}
</span>
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{
detectIssueTypes(comicvine.description)
.displayName
}
</span>
</span>
</div>
) : null}
{/* issues marked as wanted, part of this volume */}
{wanted?.markEntireVolumeWanted ? (
<div className="text-sm">sagla volume pahije</div>
) : (
<div className="my-2">
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--documents-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{wanted.issues.length}
</span>
</span>
</div>
)}
</div>
{/* comicVine metadata presence */}
{isComicBookMetadataAvailable && (
<img
src="/src/client/assets/img/cvlogo.svg"
alt={"ComicVine metadata detected."}
className="inline-block w-6 h-6 md:w-7 md:h-7 flex-shrink-0 object-contain"
/>
)}
{!isEmpty(locg) && (
<img
src="/src/client/assets/img/locglogo.svg"
className="w-7 h-7"
/>
)}
</div>
) : null}
{/* issues marked as wanted, part of this volume */}
{wanted?.markEntireVolumeWanted ? (
<div className="text-sm">sagla volume pahije</div>
) : (
<div className="my-2">
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--documents-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{wanted.issues.length}
</span>
</span>
</div>
)}
</Card>
</div>
{/* comicVine metadata presence */}
{isComicBookMetadataAvailable && (
<img
src="/src/client/assets/img/cvlogo.svg"
alt={"ComicVine metadata detected."}
className="w-7 h-7"
/>
)}
{!isEmpty(locg) && (
<img
src="/src/client/assets/img/locglogo.svg"
className="w-7 h-7"
/>
)}
</div>
</Card>
);
},
)}
);
},
)}
</div>
</div>
</div>
</>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import React, { ReactElement, useCallback, useEffect } from "react";
import React, { ReactElement, useCallback, useEffect, useRef } from "react";
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
import { format } from "date-fns";
import Loader from "react-loader-spinner";
@@ -27,12 +27,21 @@ interface IProps {
export const Import = (props: IProps): ReactElement => {
const queryClient = useQueryClient();
const { importJobQueue, socketIOInstance } = useStore(
const { importJobQueue, getSocket, setQueryClientRef } = useStore(
useShallow((state) => ({
importJobQueue: state.importJobQueue,
socketIOInstance: state.socketIOInstance,
getSocket: state.getSocket,
setQueryClientRef: state.setQueryClientRef,
})),
);
const previousResultCountRef = useRef<number>(0);
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
// Set the queryClient reference in the store so socket events can use it
useEffect(() => {
setQueryClientRef({ current: queryClient });
}, [queryClient, setQueryClientRef]);
const sessionId = localStorage.getItem("sessionId");
const { mutate: initiateImport } = useMutation({
@@ -44,24 +53,91 @@ export const Import = (props: IProps): ReactElement => {
}),
});
const { data, isError, isLoading } = useQuery({
const { data, isError, isLoading, refetch } = useQuery({
queryKey: ["allImportJobResults"],
queryFn: async () =>
await axios({
queryFn: async () => {
const response = await axios({
method: "GET",
url: "http://localhost:3000/api/jobqueue/getJobResultStatistics",
}),
params: {
_t: Date.now(), // Cache buster
},
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
},
});
// Track the result count
if (response.data?.length) {
previousResultCountRef.current = response.data.length;
}
return response;
},
refetchOnMount: true,
refetchOnWindowFocus: false,
staleTime: 0, // Always consider data stale
gcTime: 0, // Don't cache the data (replaces cacheTime in newer versions)
// Poll every 5 seconds when import is running
refetchInterval: importJobQueue.status === "running" || importJobQueue.status === "paused" ? 5000 : false,
});
// Listen for import queue drained event to refresh the table
useEffect(() => {
const socket = getSocket("/");
const handleQueueDrained = () => {
const initialCount = previousResultCountRef.current;
let attempts = 0;
const maxAttempts = 20; // Poll for up to 20 seconds
// Clear any existing polling interval
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
// Poll every second until we see new data or hit max attempts
pollingIntervalRef.current = setInterval(async () => {
attempts++;
const result = await refetch();
const newCount = result.data?.data?.length || 0;
if (newCount > initialCount) {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
} else if (attempts >= maxAttempts) {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
}
}, 1000);
};
socket.on("LS_IMPORT_QUEUE_DRAINED", handleQueueDrained);
return () => {
socket.off("LS_IMPORT_QUEUE_DRAINED", handleQueueDrained);
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
};
}, [getSocket, queryClient, refetch]);
const toggleQueue = (queueAction: string, queueStatus: string) => {
socketIOInstance.emit(
const socket = getSocket("/");
socket.emit(
"call",
"socket.setQueueStatus",
{
queueAction,
queueStatus,
},
(data) => console.log(data),
(data: any) => console.log(data),
);
};
/**
@@ -246,7 +322,7 @@ export const Import = (props: IProps): ReactElement => {
</thead>
<tbody className="divide-y divide-gray-200">
{data?.data.map((jobResult, id) => {
{data?.data.map((jobResult: any, id: number) => {
return (
<tr key={id}>
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">

View File

@@ -87,7 +87,7 @@ const renderCard = (props: ICardProps): ReactElement => {
<img
alt="Home"
src={props.imageUrl}
className="rounded-t-md object-cover"
className="rounded-t-md object-cover w-full"
/>
{props.title ? (

View File

@@ -2,17 +2,16 @@ import { create } from "zustand";
import io, { Socket } from "socket.io-client";
import { SOCKET_BASE_URI } from "../constants/endpoints";
import { isNil } from "lodash";
import { QueryClient } from "@tanstack/react-query";
import { toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.min.css";
const queryClient = new QueryClient();
// Type for global state
interface StoreState {
socketInstances: Record<string, Socket>;
getSocket: (namespace?: string) => Socket;
disconnectSocket: (namespace: string) => void;
queryClientRef: { current: any } | null;
setQueryClientRef: (ref: any) => void;
comicvine: {
scrapingStatus: string;
@@ -32,6 +31,8 @@ interface StoreState {
export const useStore = create<StoreState>((set, get) => ({
socketInstances: {},
queryClientRef: null,
setQueryClientRef: (ref: any) => set({ queryClientRef: ref }),
getSocket: (namespace = "/") => {
const fullNamespace = namespace === "/" ? "" : namespace;
@@ -97,7 +98,10 @@ export const useStore = create<StoreState>((set, get) => ({
status: "drained",
},
}));
queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] });
const queryClientRef = get().queryClientRef;
if (queryClientRef?.current) {
queryClientRef.current.invalidateQueries({ queryKey: ["allImportJobResults"] });
}
});
socket.on("CV_SCRAPING_STATUS", (data) => {