/** * Orchestrator layout with a split list/detail experience. */ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useParams, useNavigate, useSearchParams } from "react-router-dom"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer"; import { ManualImportSheet } from "../components"; import * as api from "../api"; import type { JobSource } from "../../shared/types"; import { DEFAULT_SORT } from "./orchestrator/constants"; import type { FilterTab, JobSort } from "./orchestrator/constants"; import { JobDetailPanel } from "./orchestrator/JobDetailPanel"; import { JobListPanel } from "./orchestrator/JobListPanel"; import { OrchestratorFilters } from "./orchestrator/OrchestratorFilters"; import { OrchestratorHeader } from "./orchestrator/OrchestratorHeader"; import { OrchestratorSummary } from "./orchestrator/OrchestratorSummary"; import { useFilteredJobs } from "./orchestrator/useFilteredJobs"; import { useOrchestratorData } from "./orchestrator/useOrchestratorData"; import { usePipelineSources } from "./orchestrator/usePipelineSources"; import { getJobCounts } from "./orchestrator/utils"; export const OrchestratorPage: React.FC = () => { const { tab, jobId } = useParams<{ tab: string; jobId?: string }>(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const activeTab = useMemo(() => { const validTabs: FilterTab[] = ["ready", "discovered", "applied", "all"]; if (tab && validTabs.includes(tab as FilterTab)) { return tab as FilterTab; } return "ready"; }, [tab]); // Helper to change URL while preserving search params const navigateWithContext = useCallback( (newTab: string, newJobId?: string | null, isReplace = false) => { const search = searchParams.toString(); const suffix = search ? `?${search}` : ""; const path = newJobId ? `/${newTab}/${newJobId}${suffix}` : `/${newTab}${suffix}`; navigate(path, { replace: isReplace }); }, [navigate, searchParams], ); const selectedJobId = jobId || null; // Sync searchQuery with URL const searchQuery = searchParams.get("q") || ""; const setSearchQuery = (q: string) => { setSearchParams( (prev) => { if (q) prev.set("q", q); else prev.delete("q"); return prev; }, { replace: true }, ); }; // Sync sourceFilter with URL const sourceFilter = (searchParams.get("source") as JobSource | "all") || "all"; const setSourceFilter = (source: JobSource | "all") => { setSearchParams( (prev) => { if (source !== "all") prev.set("source", source); else prev.delete("source"); return prev; }, { replace: true }, ); }; // Sync sort with URL const sort = useMemo((): JobSort => { const s = searchParams.get("sort"); if (!s) return DEFAULT_SORT; const [key, direction] = s.split("-"); return { key: key as any, direction: direction as any }; }, [searchParams]); const setSort = (newSort: JobSort) => { setSearchParams( (prev) => { if (newSort.key === DEFAULT_SORT.key && newSort.direction === DEFAULT_SORT.direction) { prev.delete("sort"); } else { prev.set("sort", `${newSort.key}-${newSort.direction}`); } return prev; }, { replace: true }, ); }; // Effect to sync URL if it was invalid useEffect(() => { const validTabs: FilterTab[] = ["ready", "discovered", "applied", "all"]; if (tab && !validTabs.includes(tab as FilterTab)) { navigateWithContext("ready", null, true); } }, [tab, navigateWithContext]); const [navOpen, setNavOpen] = useState(false); const [isManualImportOpen, setIsManualImportOpen] = useState(false); const [isDetailDrawerOpen, setIsDetailDrawerOpen] = useState(false); const [isDesktop, setIsDesktop] = useState( () => (typeof window !== "undefined" ? window.matchMedia("(min-width: 1024px)").matches : false), ); const setActiveTab = (newTab: FilterTab) => { navigateWithContext(newTab, selectedJobId); }; const handleSelectJobId = (id: string | null) => { navigateWithContext(activeTab, id); }; const { pipelineSources, setPipelineSources, toggleSource } = usePipelineSources(); const { jobs, stats, settings, isLoading, isPipelineRunning, setIsPipelineRunning, loadJobs } = useOrchestratorData(); const showSponsorInfo = settings?.showSponsorInfo ?? true; const activeJobs = useFilteredJobs(jobs, activeTab, sourceFilter, searchQuery, sort); const counts = useMemo(() => getJobCounts(jobs), [jobs]); const selectedJob = useMemo( () => (selectedJobId ? jobs.find((job) => job.id === selectedJobId) ?? null : null), [jobs, selectedJobId], ); const handleManualImported = useCallback( async (importedJobId: string) => { // Refresh jobs and navigate to the new job in discovered tab await loadJobs(); navigateWithContext("discovered", importedJobId); }, [loadJobs, navigateWithContext], ); const handleRunPipeline = async () => { try { setIsPipelineRunning(true); await api.runPipeline({ sources: pipelineSources }); toast.message("Pipeline started", { description: `Sources: ${pipelineSources.join(", ")}. This may take a few minutes.`, }); const pollInterval = setInterval(async () => { try { const status = await api.getPipelineStatus(); if (!status.isRunning) { clearInterval(pollInterval); setIsPipelineRunning(false); await loadJobs(); toast.success("Pipeline completed"); } } catch { // Ignore errors } }, 5000); } catch (error) { setIsPipelineRunning(false); const message = error instanceof Error ? error.message : "Failed to start pipeline"; toast.error(message); } }; const handleSelectJob = (id: string) => { handleSelectJobId(id); if (!isDesktop) { setIsDetailDrawerOpen(true); } }; useEffect(() => { if (activeJobs.length === 0) { if (selectedJobId) handleSelectJobId(null); return; } if (!selectedJobId || !activeJobs.some((job) => job.id === selectedJobId)) { // Auto-select first job ONLY on desktop if (isDesktop) { navigateWithContext(activeTab, activeJobs[0].id, true); } } }, [activeJobs, selectedJobId, isDesktop, activeTab, navigateWithContext]); useEffect(() => { if (!selectedJobId) { setIsDetailDrawerOpen(false); } else if (!isDesktop) { setIsDetailDrawerOpen(true); } }, [selectedJobId, isDesktop]); useEffect(() => { if (typeof window === "undefined") return; const media = window.matchMedia("(min-width: 1024px)"); const handleChange = () => setIsDesktop(media.matches); handleChange(); if (media.addEventListener) { media.addEventListener("change", handleChange); return () => media.removeEventListener("change", handleChange); } media.addListener(handleChange); return () => media.removeListener(handleChange); }, []); useEffect(() => { if (isDesktop && isDetailDrawerOpen) { setIsDetailDrawerOpen(false); } }, [isDesktop, isDetailDrawerOpen]); const onDrawerOpenChange = (open: boolean) => { setIsDetailDrawerOpen(open); if (!open && !isDesktop) { // Clear job ID from URL when closing drawer on mobile handleSelectJobId(null); } }; return ( <> setIsManualImportOpen(true)} />
{/* Main content: tabs/filters -> list/detail */}
{/* List/Detail grid - directly under tabs, no extra section */}
{/* Primary region: Job list with highest visual weight */} {/* Inspector panel: visually subordinate to list */} {isDesktop && (
)}
{!isDesktop && (
Job details
)} ); };