import { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import type { GitlabStatus, Repo, SpawnJob, SpawnResponse } from "./protocol"; import { Route } from "./protocol"; import { errMessage, fetchJson } from "./api"; import type { SessionsStore } from "./store"; const POLL_MS: number = 1500; const POLL_MAX_TICKS: number = 400; // ~10min then give up polling (spawn may still finish) interface Props { store: SessionsStore; pushToast: (text: string) => void; } export default function SpawnView({ store, pushToast }: Props) { const navigate = useNavigate(); const [status, setStatus] = useState(null); const [pat, setPat] = useState(""); const [repos, setRepos] = useState(null); const [query, setQuery] = useState(""); const [selected, setSelected] = useState(null); const [branch, setBranch] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [spawning, setSpawning] = useState(null); const timerRef = useRef(null); const tickRef = useRef(0); useEffect(() => { return () => { if (timerRef.current !== null) window.clearInterval(timerRef.current); }; }, []); const loadStatus = async (): Promise => { const s = await fetchJson(Route.GitlabStatus); setStatus(s); return s; }; const loadRepos = async (): Promise => { const list = await fetchJson(Route.GitlabRepos); setRepos(list); }; useEffect(() => { void (async () => { try { const s = await loadStatus(); if (s.connected) await loadRepos(); } catch (err) { setError(errMessage(err)); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const connect = async (): Promise => { if (pat.trim().length === 0) { setError("token required"); return; } setBusy(true); setError(""); try { await fetchJson(Route.GitlabConnect, { method: "POST", body: JSON.stringify({ token: pat.trim() }) }); setPat(""); await loadStatus(); await loadRepos(); } catch (err) { setError(errMessage(err)); } finally { setBusy(false); } }; const pick = (repo: Repo): void => { setSelected(repo); setBranch(repo.defaultBranch); }; const filtered: Repo[] = (repos ?? []).filter((r) => r.path.toLowerCase().includes(query.toLowerCase())); const spawn = async (): Promise => { if (selected === null) { setError("pick a repo first"); return; } setBusy(true); setError(""); try { const body = { repo: selected.path, ...(branch.trim().length > 0 ? { branch: branch.trim() } : {}) }; const res = await fetchJson(Route.Spawn, { method: "POST", body: JSON.stringify(body) }); setSpawning(res); startPolling(res.sessionId); } catch (err) { setError(errMessage(err)); } finally { setBusy(false); } }; const startPolling = (sessionId: string): void => { tickRef.current = 0; if (timerRef.current !== null) window.clearInterval(timerRef.current); timerRef.current = window.setInterval(() => { tickRef.current += 1; if (tickRef.current > POLL_MAX_TICKS) { if (timerRef.current !== null) window.clearInterval(timerRef.current); timerRef.current = null; return; } void (async () => { try { await fetchJson(Route.SpawnStatus).catch(() => undefined); await store.refresh(); const s = store.sessions.find((x) => x.id === sessionId); if (s !== undefined && s.online) { if (timerRef.current !== null) window.clearInterval(timerRef.current); timerRef.current = null; navigate(`/s/${sessionId}`); } } catch (err) { pushToast(errMessage(err)); } })(); }, POLL_MS); }; const jobLine = (): string => { if (spawning === null) return ""; const job: SpawnJob | undefined = store.spawnJobs.find((j) => j.sessionId === spawning.sessionId); if (job !== undefined) return `${job.repo}: ${job.state}`; return "waiting for session to come online…"; }; if (status === null) { return (

Spawn

{error.length > 0 ? `gitlab status failed: ${error}` : "checking gitlab…"}

); } if (!status.connected) { return (

Spawn

{ e.preventDefault(); void connect(); }} >

Connect GitLab

Paste a personal access token (api scope). Stored daemon-side only.

setPat(e.target.value)} /> {error.length > 0 &&

{error}

}
); } return (

Spawn

{spawning !== null && (

Spawning…

{jobLine()}

container {spawning.containerId.slice(0, 12)}

{error.length > 0 &&

{error}

}
)} {spawning === null && ( <>

Repository

setQuery(e.target.value)} /> {repos === null &&

loading repos…

} {repos !== null && filtered.length === 0 &&

no matching repos

}
{filtered.map((r) => (
pick(r)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") pick(r); }} >
{r.path}
default {r.defaultBranch} · {r.lastActivityAt.slice(0, 10)}
))}

Branch

setBranch(e.target.value)} />
{selected === null &&

select a repo above

} {error.length > 0 &&

{error}

}
)}
); }