app: Add-program flow with server-side filesystem picker
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.
This commit is contained in:
@@ -1,17 +1,41 @@
|
||||
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" />
|
||||
<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 ? (
|
||||
|
||||
Reference in New Issue
Block a user