import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useEffect, useState } from "react" import type { RepoSnapshot, RepoSnapshotStatus } from "@/lib/api" import { Button } from "@/components/ui/button" import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@/components/ui/combobox" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Skeleton } from "@/components/ui/skeleton" import { InstructionsEditor } from "@/components/InstructionsEditor" import { ApiError, api, isGithubReauthError, loginUrl } from "@/lib/api" import { normalizeRepoFullName } from "@/lib/repo" function formatMutationError(e: Error): string { return isGithubReauthError(e) ? "GitHub token expired — sign in again using the link above." : e.message } const STATUS_LABEL: Record = { none: "Not built", building: "Building…", ready: "Ready", failed: "Failed", } const STATUS_CLASS: Record = { none: "text-muted-foreground", building: "text-amber-500", ready: "text-emerald-500", failed: "text-destructive", } export function RepoSnapshotsPanel() { const qc = useQueryClient() const [error, setError] = useState(null) const [addRepo, setAddRepo] = useState("") const [selected, setSelected] = useState(null) const [draft, setDraft] = useState("") const snapshots = useQuery({ queryKey: ["repoSnapshots"], queryFn: api.listRepoSnapshots, }) const repos = useQuery({ queryKey: ["repos"], queryFn: async () => { try { return await api.repos() } catch (e) { if (e instanceof ApiError && e.status === 401) return { installations: [], repositories: [] } throw e } }, }) const detail = useQuery({ queryKey: ["repoSnapshot", selected], queryFn: () => api.getRepoSnapshot(selected!), enabled: !!selected, // Poll while a build is running so status + logs update live. refetchInterval: (query) => query.state.data?.status === "building" ? 4000 : false, }) useEffect(() => { if (detail.data) setDraft(detail.data.dockerfile) }, [detail.data?.dockerfile, detail.data?.full_name]) const create = useMutation({ mutationFn: (full_name: string) => api.createRepoSnapshot(full_name), onSuccess: (record) => { void qc.invalidateQueries({ queryKey: ["repoSnapshots"] }) setSelected(record.full_name) setError(null) }, onError: (e: Error) => setError(formatMutationError(e)), }) const save = useMutation({ mutationFn: ({ full_name, value }: { full_name: string; value: string }) => api.saveRepoSnapshot(full_name, { dockerfile: value }), onSuccess: () => { void qc.invalidateQueries({ queryKey: ["repoSnapshots"] }) void qc.invalidateQueries({ queryKey: ["repoSnapshot", selected] }) setError(null) }, onError: (e: Error) => setError(formatMutationError(e)), }) const build = useMutation({ mutationFn: (full_name: string) => api.buildRepoSnapshot(full_name), onSuccess: () => { void qc.invalidateQueries({ queryKey: ["repoSnapshots"] }) void qc.invalidateQueries({ queryKey: ["repoSnapshot", selected] }) setError(null) }, onError: (e: Error) => setError(formatMutationError(e)), }) const remove = useMutation({ mutationFn: (full_name: string) => api.deleteRepoSnapshot(full_name), onSuccess: (_data, full_name) => { void qc.invalidateQueries({ queryKey: ["repoSnapshots"] }) if (selected === full_name) { setSelected(null) setDraft("") } setError(null) }, onError: (e: Error) => setError(formatMutationError(e)), }) if (snapshots.isLoading) { return } const configured = new Set((snapshots.data ?? []).map((s) => s.full_name)) const suggestedRepos = (repos.data?.repositories ?? []).filter( (r) => !configured.has(r.full_name) ) const normalizedAddRepo = normalizeRepoFullName(addRepo) const canAdd = normalizedAddRepo !== null && !configured.has(normalizedAddRepo) const active: RepoSnapshot | null = detail.data ?? snapshots.data?.find((s) => s.full_name === selected) ?? null const dirty = active != null && draft !== active.dockerfile const building = active?.status === "building" const handleAdd = () => { if (!normalizedAddRepo || !canAdd) return void create .mutateAsync(normalizedAddRepo) .then(() => setAddRepo("")) .catch(() => undefined) } const githubReauth = (repos.isError && isGithubReauthError(repos.error)) || (error !== null && /github token|re-login required/i.test(error)) return (
{githubReauth && (
Your GitHub connection expired.{" "} Sign in with GitHub again {" "} to list installed repos.
)}
setAddRepo(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() handleAdd() } }} className="sm:flex-1" />
{suggestedRepos.length > 0 && ( r.full_name)} value={addRepo} onValueChange={(v) => setAddRepo(typeof v === "string" ? v : "")} > No matches {suggestedRepos.map((r) => ( {r.full_name} {r.private && ( private )} ))} )}

Repositories

{(snapshots.data ?? []).length === 0 ? (

No repositories yet.

) : (
    {(snapshots.data ?? []).map((s) => (
  • ))}
)}
{!selected || !active ? (

Select a repository above to edit its Dockerfile and build a snapshot. Repos without a ready snapshot fall back to the default sandbox image.

) : ( <>

{active.full_name}

{STATUS_LABEL[active.status]} {active.snapshot_id && ( snapshot {active.snapshot_id} )} {active.last_built_at && ( built {new Date(active.last_built_at).toLocaleString()} )}
{dirty && ( Save before building )}
{active.status === "failed" && active.status_message && (

{active.status_message}

)} {active.build_log && (
                {active.build_log}
              
)} )} {error &&

{error}

}
) }