feat: default steps are read only
Server Deploy / deploy (push) Successful in 1m46s

This commit is contained in:
2026-07-28 13:34:08 +01:00
parent 26567766a9
commit 31e0000306
7 changed files with 80 additions and 23 deletions
+6 -4
View File
@@ -175,14 +175,16 @@ export default function StepsPage() {
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-3 text-text-secondary">
<button onClick={() => openEdit(s)} className="hover:text-text-primary">
Edit
{s.source === "default" ? "View" : "Edit"}
</button>
<a href={api.exportStepUrl(s.step_id)} download className="hover:text-text-primary">
Export
</a>
<button onClick={() => openEdit(s)} className="hover:text-danger">
Delete
</button>
{s.source !== "default" && (
<button onClick={() => openEdit(s)} className="hover:text-danger">
Delete
</button>
)}
</div>
</td>
</tr>
+9 -2
View File
@@ -457,7 +457,8 @@ export default function WorkflowBuilder() {
<div className="border-b border-border pb-4">
<label className="mb-1 block text-xs uppercase text-text-secondary">Command</label>
<textarea
className={`${inputClass} h-32 font-mono text-xs`}
className={`${inputClass} h-32 font-mono text-xs ${selectedLib?.source === "default" ? "cursor-not-allowed opacity-70" : ""}`}
readOnly={selectedLib?.source === "default"}
value={selectedRef.overrides?.script ?? selectedLib?.script ?? ""}
onChange={(e) =>
updateRef(selectedIdxInWf, {
@@ -466,7 +467,13 @@ export default function WorkflowBuilder() {
}
/>
<p className="mt-1 text-xs text-text-secondary">
Write <code className="text-signal">KEY=value</code> to <code className="text-signal">$WORKFLOW_ENV</code> to expose it to later steps.
{selectedLib?.source === "default" ? (
<>This step ships with Vantage, so its script is fixed. Set its inputs below, or duplicate it into a shared step to change the script.</>
) : (
<>
Write <code className="text-signal">KEY=value</code> to <code className="text-signal">$WORKFLOW_ENV</code> to expose it to later steps.
</>
)}
</p>
</div>
)}
+25 -15
View File
@@ -17,8 +17,10 @@ export function EditStepModal({ open, step, onClose }: { open: boolean; step: Wo
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
// Default steps are re-seeded from the image on every boot, so the server
// refuses to update or delete them. The form mirrors that rather than
// offering buttons that can only 409.
const locked = step?.source === "default";
const save = async () => {
setBusy(true); setError(null);
@@ -47,24 +49,30 @@ export function EditStepModal({ open, step, onClose }: { open: boolean; step: Wo
};
return (
<Modal open={open} onClose={onClose} title={step ? "Edit base step" : "New step"} wide>
<Modal open={open} onClose={onClose} title={locked ? "Default step" : step ? "Edit base step" : "New step"} wide>
<div className="space-y-4">
{error && <div className="rounded border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{error}</div>}
<p className="text-xs text-text-secondary">Reusable steps are shared across all workflows. Editing here changes it everywhere.</p>
{locked ? (
<p className="rounded border border-border bg-surface-2 px-3 py-2 text-xs text-text-secondary">
This step ships with Vantage and is read-only. It is re-seeded on every upgrade. Export it and import a copy to make your own version.
</p>
) : (
<p className="text-xs text-text-secondary">Reusable steps are shared across all workflows. Editing here changes it everywhere.</p>
)}
<div>
<label className="mb-1 block text-xs uppercase text-text-secondary">Name</label>
<input className={inputClass} value={name} onChange={(e) => setName(e.target.value)} />
<input className={inputClass} value={name} readOnly={locked} onChange={(e) => setName(e.target.value)} />
</div>
<div>
<label className="mb-1 block text-xs uppercase text-text-secondary">Interpreter</label>
<select className={inputClass} value={interpreter} onChange={(e) => setInterpreter(e.target.value as "bash" | "powershell")}>
<select className={inputClass} value={interpreter} disabled={locked} onChange={(e) => setInterpreter(e.target.value as "bash" | "powershell")}>
<option value="bash">bash</option>
<option value="powershell">powershell</option>
</select>
</div>
<div>
<label className="mb-1 block text-xs uppercase text-text-secondary">Script</label>
<textarea className={`${inputClass} h-40 font-mono text-xs`} value={script} onChange={(e) => setScript(e.target.value)} />
<textarea className={`${inputClass} h-40 font-mono text-xs`} value={script} readOnly={locked} onChange={(e) => setScript(e.target.value)} />
<p className="mt-1 text-xs text-text-secondary">Write <code className="text-signal">KEY=value</code> to <code className="text-signal">$WORKFLOW_ENV</code> to expose it to later steps.</p>
</div>
<div>
@@ -86,20 +94,22 @@ export function EditStepModal({ open, step, onClose }: { open: boolean; step: Wo
<div className="space-y-2">
{inputs.map((inp, i) => (
<div key={i} className="flex gap-2">
<input className={inputClass} placeholder="name" value={inp.name} onChange={(e) => setInputs(inputs.map((x, j) => j === i ? { ...x, name: e.target.value } : x))} />
<input className={inputClass} placeholder="default" value={inp.default} onChange={(e) => setInputs(inputs.map((x, j) => j === i ? { ...x, default: e.target.value } : x))} />
<input className={inputClass} placeholder="description" value={inp.description} onChange={(e) => setInputs(inputs.map((x, j) => j === i ? { ...x, description: e.target.value } : x))} />
<Button variant="ghost" size="sm" onClick={() => setInputs(inputs.filter((_, j) => j !== i))}></Button>
<input className={inputClass} readOnly={locked} placeholder="name" value={inp.name} onChange={(e) => setInputs(inputs.map((x, j) => j === i ? { ...x, name: e.target.value } : x))} />
<input className={inputClass} readOnly={locked} placeholder="default" value={inp.default} onChange={(e) => setInputs(inputs.map((x, j) => j === i ? { ...x, default: e.target.value } : x))} />
<input className={inputClass} readOnly={locked} placeholder="description" value={inp.description} onChange={(e) => setInputs(inputs.map((x, j) => j === i ? { ...x, description: e.target.value } : x))} />
{!locked && <Button variant="ghost" size="sm" onClick={() => setInputs(inputs.filter((_, j) => j !== i))}></Button>}
</div>
))}
</div>
<Button variant="ghost" size="sm" className="mt-2" onClick={() => setInputs([...inputs, { name: "", default: "", description: "" }])}>Add input</Button>
{!locked && (
<Button variant="ghost" size="sm" className="mt-2" onClick={() => setInputs([...inputs, { name: "", default: "", description: "" }])}>Add input</Button>
)}
</div>
<div className="flex items-center justify-between pt-2">
{step ? <Button variant="danger" onClick={del} loading={busy}>Delete step</Button> : <span />}
{step && !locked ? <Button variant="danger" onClick={del} loading={busy}>Delete step</Button> : <span />}
<div className="flex gap-2">
<Button variant="ghost" onClick={onClose}>Cancel</Button>
<Button variant="primary" onClick={save} loading={busy} disabled={!name.trim()}>Save</Button>
<Button variant="ghost" onClick={onClose}>{locked ? "Close" : "Cancel"}</Button>
{!locked && <Button variant="primary" onClick={save} loading={busy} disabled={!name.trim()}>Save</Button>}
</div>
</div>
</div>
File diff suppressed because one or more lines are too long