The Inputs editor and the canvas label show it, and the run forms leave panel inputs out.
191 lines
5.9 KiB
TypeScript
191 lines
5.9 KiB
TypeScript
import { useEffect, useState } from "react"
|
|
|
|
import type { DType, FlowDef_Input, FlowInput_Input } from "@/client"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import { parseByDtype } from "./FlowBoundary"
|
|
import { asText } from "./NodePanel"
|
|
|
|
/**
|
|
* A field per declared input, the way its type is entered.
|
|
*
|
|
* Held as text rather than as values: half-typed input is normal ("-", "1.",
|
|
* a JSON object with one brace), and a list of them is a sweep — both of
|
|
* which a parsed value would have thrown away. The caller parses on submit.
|
|
*/
|
|
export function ParamFields({
|
|
inputs,
|
|
values,
|
|
placeholderFor,
|
|
onChange,
|
|
}: {
|
|
inputs: FlowInput_Input[]
|
|
values: Record<string, string>
|
|
/** What an empty field says it takes, when the default is not enough. */
|
|
placeholderFor?: (dtype: DType | undefined) => string
|
|
onChange: (values: Record<string, string>) => void
|
|
}) {
|
|
return (
|
|
<div className="grid gap-3">
|
|
{inputs.map((declared) => {
|
|
const name = declared.spec?.name ?? ""
|
|
const dtype = declared.spec?.dtype
|
|
return (
|
|
<div key={name} className="grid gap-1.5">
|
|
<Label htmlFor={`param-${name}`} className="font-mono text-xs">
|
|
{name}
|
|
</Label>
|
|
{dtype === "bool" ? (
|
|
<Select
|
|
value={values[name] === "true" ? "true" : "false"}
|
|
onValueChange={(next) => onChange({ ...values, [name]: next })}
|
|
>
|
|
<SelectTrigger id={`param-${name}`} className="text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="true">true</SelectItem>
|
|
<SelectItem value="false">false</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
) : (
|
|
<Input
|
|
id={`param-${name}`}
|
|
value={values[name] ?? ""}
|
|
placeholder={
|
|
placeholderFor?.(dtype) ??
|
|
(dtype === "artifact"
|
|
? "@run:<id>.<output> or sha256:…"
|
|
: `${dtype ?? "float"} or @run:<id>.<output>`)
|
|
}
|
|
className="text-sm"
|
|
onChange={(event) =>
|
|
onChange({ ...values, [name]: event.target.value })
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** What the fields start from: the declared value of each, as text. */
|
|
export function initialText(inputs: FlowInput_Input[]): Record<string, string> {
|
|
return Object.fromEntries(
|
|
inputs.map((one) => [one.spec?.name ?? "", asText(one.initial)]),
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The declared inputs worth drawing a field for.
|
|
*
|
|
* A panel input is a control's to write, so a run keeps its starting value
|
|
* and the form does not offer it as a parameter.
|
|
*/
|
|
export function declaredInputs(definition: FlowDef_Input): FlowInput_Input[] {
|
|
return (definition.inputs ?? []).filter(
|
|
(one) => one.spec?.name && one.origin !== "panel",
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The parameters of one run, taken from the flow's inputs.
|
|
*
|
|
* A batch flow's inputs *are* its parameters — a run supplies values for them
|
|
* and keeps whatever it does not name — so this is a form over the same
|
|
* declarations the flow panel edits, prefilled with what they start from.
|
|
*/
|
|
export function RunDialog({
|
|
open,
|
|
definition,
|
|
pending,
|
|
onOpenChange,
|
|
onRun,
|
|
}: {
|
|
open: boolean
|
|
definition: FlowDef_Input
|
|
pending: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
onRun: (params: Record<string, unknown>) => void
|
|
}) {
|
|
const inputs = declaredInputs(definition)
|
|
const [values, setValues] = useState<Record<string, string>>({})
|
|
|
|
// Opening is what fills the form: an edit to the flow between two runs
|
|
// should show up, and the last run's values should not linger. The inputs
|
|
// are read then rather than watched, so typing over one is not undone.
|
|
// biome-ignore lint/correctness/useExhaustiveDependencies: opening is the dependency.
|
|
useEffect(() => {
|
|
if (!open) return
|
|
setValues(initialText(inputs))
|
|
}, [open])
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent data-testid="run-dialog">
|
|
<DialogHeader>
|
|
<DialogTitle>Run {definition.title || definition.name}</DialogTitle>
|
|
<DialogDescription>
|
|
{inputs.length === 0
|
|
? "This flow declares no parameters, so there is nothing to choose."
|
|
: "The run starts from these. Everything it computes is dropped afterwards except its result."}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<ParamFields inputs={inputs} values={values} onChange={setValues} />
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="brand"
|
|
disabled={pending}
|
|
data-testid="submit-run"
|
|
onClick={() => onRun(parseParams(inputs, values))}
|
|
>
|
|
Run
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The fields as the engine takes them.
|
|
*
|
|
* A field nobody filled in is left out, which is what keeps its declared
|
|
* value — an empty parameter is not the same as a zero.
|
|
*/
|
|
export function parseParams(
|
|
inputs: FlowInput_Input[],
|
|
values: Record<string, string>,
|
|
): Record<string, unknown> {
|
|
const params: Record<string, unknown> = {}
|
|
for (const declared of inputs) {
|
|
const name = declared.spec?.name ?? ""
|
|
const parsed = parseByDtype(declared.spec?.dtype, values[name] ?? "")
|
|
if (parsed !== null) params[name] = parsed
|
|
}
|
|
return params
|
|
}
|