Stage 4: App — create service/job forms (UI twin of castle expose)
CreateDeploymentForm builds a service or job in castle.yaml (PUT /config/...),
then deploys it (POST /deploy {name} → unit + Caddyfile + reload) and, for a
service, starts it (POST /services/{name}/start), then navigates to the new
detail page.
Reachable two ways:
- Program page Deployments section: 'Create service' / 'Create job' buttons,
prefilled from the program (name, program ref, run target, runner).
- Dashboard: standalone '+ Add service' / '+ Add job' (deployments can run
anything, not just castle programs).
Service form covers port / port_env / health / proxy path / proxy host; job
form covers schedule. Name validated against existing services/jobs.
Verified: the configs the form builds validate (PUT 200) and round-trip clean.
App builds; api 52 / core 94 green from prior stages.
This commit is contained in:
@@ -1,24 +1,68 @@
|
||||
import { useState } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { Server, Clock } from "lucide-react"
|
||||
import { Server, Clock, Plus } from "lucide-react"
|
||||
import type { ProgramDetail } from "@/types"
|
||||
import { useServices, useJobs } from "@/services/api/hooks"
|
||||
import { CreateDeploymentForm, type CreatePrefill } from "./CreateDeploymentForm"
|
||||
|
||||
/** The services and jobs that deploy a program. A program → 0-N services and
|
||||
* 0-N jobs; these are convenience links, not ownership (a deployment can run
|
||||
* anything, program-backed or not). */
|
||||
* anything, program-backed or not). The Create buttons just prefill the
|
||||
* standalone create form with sensible values. */
|
||||
export function DeploymentsSection({ program }: { program: ProgramDetail }) {
|
||||
const { services, jobs, behavior } = program
|
||||
const none = services.length === 0 && jobs.length === 0
|
||||
const [creating, setCreating] = useState<"service" | "job" | null>(null)
|
||||
|
||||
const { data: allServices } = useServices()
|
||||
const { data: allJobs } = useJobs()
|
||||
const existing =
|
||||
creating === "service"
|
||||
? (allServices ?? []).map((s) => s.id)
|
||||
: (allJobs ?? []).map((j) => j.id)
|
||||
|
||||
const prefill: CreatePrefill = {
|
||||
name: program.id,
|
||||
program: program.id,
|
||||
runTarget: program.id,
|
||||
runner: program.stack?.startsWith("python") || !program.stack ? "python" : "command",
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 mb-6">
|
||||
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-1">
|
||||
Deployments
|
||||
</h2>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">
|
||||
Deployments
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setCreating(creating === "service" ? null : "service")}
|
||||
className="flex items-center gap-1 text-xs text-[var(--primary)] hover:underline"
|
||||
>
|
||||
<Plus size={12} /> Create service
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCreating(creating === "job" ? null : "job")}
|
||||
className="flex items-center gap-1 text-xs text-[var(--primary)] hover:underline"
|
||||
>
|
||||
<Plus size={12} /> Create job
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--muted)] mb-4">
|
||||
Services and jobs that run this program.
|
||||
</p>
|
||||
|
||||
{none ? (
|
||||
{creating && (
|
||||
<CreateDeploymentForm
|
||||
kind={creating}
|
||||
prefill={prefill}
|
||||
existingNames={existing}
|
||||
onCancel={() => setCreating(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{none && !creating ? (
|
||||
<p className="text-sm text-[var(--muted)]">
|
||||
{behavior === "daemon"
|
||||
? "No service yet — this daemon isn't deployed."
|
||||
|
||||
Reference in New Issue
Block a user