Stage 2: App — split the one editor into typed program/service/job forms
The single service-shaped DeploymentFields was reused for all three sections, so the program page bled service fields (port/proxy/health) and couldn't edit program config, and the job page couldn't edit its schedule. Replaced it with: - ProgramFields — description, source, stack, behavior, version, repo/ref, system deps (+ read-only commands). Edits the program catalog entry. - ServiceFields — run target, port, port_env, health, proxy path + host, env/secrets. Prefills correctly now (Stage 1) and round-trips through PUT. - JobFields — schedule, run target, env/secrets. ConfigPanel selects the form by section. Shared Field/TextField/useEnvSecrets/ FormFooter helpers in detail/fields.tsx. Retired DeploymentFields. run editing only touches python/command specs (container/node/remote run blocks preserved). Verified: GET service manifest → PUT back validates 200. App type-checks/builds.
This commit is contained in:
80
app/src/components/detail/JobFields.tsx
Normal file
80
app/src/components/detail/JobFields.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useState } from "react"
|
||||
import type { JobDetail } from "@/types"
|
||||
import { runnerLabel } from "@/lib/labels"
|
||||
import { Field, TextField, FormFooter, useEnvSecrets } from "./fields"
|
||||
|
||||
interface Props {
|
||||
job: JobDetail
|
||||
onSave: (name: string, config: Record<string, unknown>) => Promise<void>
|
||||
onDelete?: (name: string) => Promise<void>
|
||||
}
|
||||
|
||||
type Obj = Record<string, unknown>
|
||||
const obj = (v: unknown): Obj => (v as Obj) ?? {}
|
||||
|
||||
/** Edit a job's deployment config (schedule / run / env). */
|
||||
export function JobFields({ job, onSave, onDelete }: Props) {
|
||||
const m = job.manifest
|
||||
const run = obj(m.run)
|
||||
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
const [description, setDescription] = useState((m.description as string) ?? "")
|
||||
const [schedule, setSchedule] = useState((m.schedule as string) ?? "")
|
||||
const [runTarget, setRunTarget] = useState(
|
||||
(run.program as string) ?? ((run.argv as string[]) ?? []).join(" "),
|
||||
)
|
||||
|
||||
const { element: envEditor, merged } = useEnvSecrets(obj(obj(m.defaults).env) as Record<string, string>)
|
||||
const runner = (run.runner as string) ?? "?"
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
setSaved(false)
|
||||
try {
|
||||
const config: Record<string, unknown> = JSON.parse(JSON.stringify(m))
|
||||
delete config.id
|
||||
config.description = description || undefined
|
||||
config.schedule = schedule || undefined
|
||||
|
||||
const runOut = obj(config.run)
|
||||
if (runner === "command") runOut.argv = runTarget.split(" ").filter(Boolean)
|
||||
else if (runner === "python") runOut.program = runTarget
|
||||
config.run = runOut
|
||||
|
||||
const env = merged()
|
||||
if (Object.keys(env).length > 0) config.defaults = { ...obj(config.defaults), env }
|
||||
else if (config.defaults) delete (config.defaults as Obj).env
|
||||
|
||||
await onSave(job.id, config)
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<TextField label="Description" value={description} onChange={setDescription} />
|
||||
<TextField label="Schedule" value={schedule} onChange={setSchedule} width="w-48" mono placeholder="0 2 * * *" />
|
||||
<Field label="Runs">
|
||||
<span className="text-sm font-mono text-[var(--muted)]">{runnerLabel(runner)} · </span>
|
||||
<input
|
||||
value={runTarget}
|
||||
onChange={(e) => setRunTarget(e.target.value)}
|
||||
className="w-56 bg-black/30 border border-[var(--border)] rounded px-2 py-1 text-xs font-mono focus:outline-none focus:border-[var(--primary)]"
|
||||
/>
|
||||
</Field>
|
||||
{envEditor}
|
||||
<FormFooter
|
||||
saving={saving}
|
||||
saved={saved}
|
||||
onSave={handleSave}
|
||||
onDelete={onDelete ? () => onDelete(job.id) : undefined}
|
||||
deleteLabel="Remove job"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user