diff --git a/orchestrator/src/client/App.tsx b/orchestrator/src/client/App.tsx index a336df2..e6ec43b 100644 --- a/orchestrator/src/client/App.tsx +++ b/orchestrator/src/client/App.tsx @@ -42,6 +42,7 @@ export const App: React.FC = () => { } /> } /> } /> + } /> diff --git a/orchestrator/src/client/pages/OrchestratorPage.tsx b/orchestrator/src/client/pages/OrchestratorPage.tsx index 3249589..95539e0 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.tsx @@ -25,7 +25,7 @@ import { usePipelineSources } from "./orchestrator/usePipelineSources"; import { getJobCounts } from "./orchestrator/utils"; export const OrchestratorPage: React.FC = () => { - const { tab } = useParams<{ tab: string }>(); + const { tab, jobId } = useParams<{ tab: string; jobId?: string }>(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); @@ -37,6 +37,19 @@ export const OrchestratorPage: React.FC = () => { 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) => { @@ -85,20 +98,23 @@ export const OrchestratorPage: React.FC = () => { useEffect(() => { const validTabs: FilterTab[] = ["ready", "discovered", "applied", "all"]; if (tab && !validTabs.includes(tab as FilterTab)) { - navigate("/ready", { replace: true }); + navigateWithContext("ready", null, true); } - }, [tab, navigate]); + }, [tab, navigateWithContext]); const [navOpen, setNavOpen] = useState(false); const [isManualImportOpen, setIsManualImportOpen] = useState(false); - const [selectedJobId, setSelectedJobId] = useState(null); const [isDetailDrawerOpen, setIsDetailDrawerOpen] = useState(false); const [isDesktop, setIsDesktop] = useState( () => (typeof window !== "undefined" ? window.matchMedia("(min-width: 1024px)").matches : false), ); const setActiveTab = (newTab: FilterTab) => { - navigate(`/${newTab}`); + navigateWithContext(newTab, selectedJobId); + }; + + const handleSelectJobId = (id: string | null) => { + navigateWithContext(activeTab, id); }; const { pipelineSources, setPipelineSources, toggleSource } = usePipelineSources(); @@ -112,13 +128,12 @@ export const OrchestratorPage: React.FC = () => { ); const handleManualImported = useCallback( - async (jobId: string) => { - setActiveTab("discovered"); - setSourceFilter("all"); + async (importedJobId: string) => { + // Refresh jobs and navigate to the new job in discovered tab await loadJobs(); - setSelectedJobId(jobId); + navigateWithContext("discovered", importedJobId); }, - [loadJobs], + [loadJobs, navigateWithContext], ); const handleRunPipeline = async () => { @@ -149,8 +164,8 @@ export const OrchestratorPage: React.FC = () => { } }; - const handleSelectJob = (jobId: string) => { - setSelectedJobId(jobId); + const handleSelectJob = (id: string) => { + handleSelectJobId(id); if (!isDesktop) { setIsDetailDrawerOpen(true); } @@ -158,19 +173,24 @@ export const OrchestratorPage: React.FC = () => { useEffect(() => { if (activeJobs.length === 0) { - setSelectedJobId(null); + if (selectedJobId) handleSelectJobId(null); return; } if (!selectedJobId || !activeJobs.some((job) => job.id === selectedJobId)) { - setSelectedJobId(activeJobs[0].id); + // Auto-select first job ONLY on desktop + if (isDesktop) { + navigateWithContext(activeTab, activeJobs[0].id, true); + } } - }, [activeJobs, selectedJobId]); + }, [activeJobs, selectedJobId, isDesktop, activeTab, navigateWithContext]); useEffect(() => { if (!selectedJobId) { setIsDetailDrawerOpen(false); + } else if (!isDesktop) { + setIsDetailDrawerOpen(true); } - }, [selectedJobId]); + }, [selectedJobId, isDesktop]); useEffect(() => { if (typeof window === "undefined") return; @@ -191,6 +211,14 @@ export const OrchestratorPage: React.FC = () => { } }, [isDesktop, isDetailDrawerOpen]); + const onDrawerOpenChange = (open: boolean) => { + setIsDetailDrawerOpen(open); + if (!open && !isDesktop) { + // Clear job ID from URL when closing drawer on mobile + handleSelectJobId(null); + } + }; + return ( <> { activeTab={activeTab} activeJobs={activeJobs} selectedJob={selectedJob} - onSelectJobId={setSelectedJobId} + onSelectJobId={handleSelectJobId} onJobUpdated={loadJobs} onSetActiveTab={setActiveTab} /> @@ -258,7 +286,7 @@ export const OrchestratorPage: React.FC = () => { /> {!isDesktop && ( - +
Job details
@@ -273,7 +301,7 @@ export const OrchestratorPage: React.FC = () => { activeTab={activeTab} activeJobs={activeJobs} selectedJob={selectedJob} - onSelectJobId={setSelectedJobId} + onSelectJobId={handleSelectJobId} onJobUpdated={loadJobs} onSetActiveTab={setActiveTab} />