Register an existing repo as a program from the /programs page — the web equivalent of `castle program add`. An inline form with an editable, autocompleting path bar browses the *server's* filesystem (a browser's own file dialog only sees the client machine) and also accepts a git URL. - core: extract the adopt logic (target parsing, stack/command detection, ProgramSpec build) into castle_core.adopt so the CLI and API share it; the CLI's add.py now delegates to it. - castle-api: GET /fs/browse (directory listing, per-entry stat errors skipped so one unreadable child can't 403 the dir) and POST /programs/adopt. - app: AddProgramForm + Add-program button on the Programs page; useBrowse / useAdoptProgram hooks. Also tidy ProgramCard: deployments render below the description, and the deployment name label is hidden when it matches the program title.
49 lines
1.5 KiB
TypeScript
49 lines
1.5 KiB
TypeScript
import { useState } from "react"
|
|
import { Plus } from "lucide-react"
|
|
import { usePrograms } from "@/services/api/hooks"
|
|
import { ProgramList } from "@/components/ProgramList"
|
|
import { MonorepoBanner } from "@/components/MonorepoBanner"
|
|
import { PageHeader } from "@/components/PageHeader"
|
|
import { AddProgramForm } from "@/components/AddProgramForm"
|
|
|
|
export function Programs() {
|
|
const { data: programs, isLoading } = usePrograms()
|
|
const [adding, setAdding] = useState(false)
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto px-6 py-8">
|
|
<PageHeader
|
|
title="Programs"
|
|
subtitle="Software catalog"
|
|
actions={
|
|
<button
|
|
onClick={() => setAdding((a) => !a)}
|
|
className="flex items-center gap-1 px-3 py-1.5 text-sm rounded border border-[var(--border)] text-[var(--muted)] hover:text-[var(--foreground)] hover:border-[var(--primary)] transition-colors"
|
|
>
|
|
<Plus size={14} /> Add program
|
|
</button>
|
|
}
|
|
/>
|
|
|
|
<MonorepoBanner />
|
|
|
|
{adding && (
|
|
<div className="mb-6 max-w-2xl">
|
|
<AddProgramForm
|
|
existingNames={(programs ?? []).map((p) => p.id)}
|
|
onCancel={() => setAdding(false)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{isLoading ? (
|
|
<p className="text-[var(--muted)]">Loading...</p>
|
|
) : programs && programs.length > 0 ? (
|
|
<ProgramList programs={programs} filterable />
|
|
) : (
|
|
<p className="text-[var(--muted)]">No programs yet.</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|