212 lines
8.1 KiB
TypeScript
212 lines
8.1 KiB
TypeScript
import {
|
|
BadgeInfo,
|
|
GitBranch,
|
|
LayoutGrid,
|
|
Loader2,
|
|
MousePointer2,
|
|
Play,
|
|
Plus,
|
|
RefreshCw,
|
|
Save,
|
|
Trash2,
|
|
} from 'lucide-react'
|
|
|
|
type WorkflowExecutionModeOption = {
|
|
value: string
|
|
label: string
|
|
}
|
|
|
|
interface WorkflowCanvasToolbarProps {
|
|
workflowName: string
|
|
blueprintLabel?: string | null
|
|
blueprintDescription?: string | null
|
|
graphFamilyLabel: string
|
|
graphFamilyClassName: string
|
|
executionMode: string
|
|
executionModeLabel: string
|
|
executionModeClassName: string
|
|
executionModeHint: string
|
|
dispatchContextId: string
|
|
executionModes: WorkflowExecutionModeOption[]
|
|
selectedEdgeCount: number
|
|
canAutoLayout: boolean
|
|
hasValidationErrors: boolean
|
|
isPreflightPending: boolean
|
|
isDispatchPending: boolean
|
|
isSaving: boolean
|
|
onDispatchContextIdChange: (value: string) => void
|
|
onExecutionModeChange: (value: string) => void
|
|
onOpenNodeMenu: () => void
|
|
onAutoLayout: () => void
|
|
onDeleteSelectedEdges: () => void
|
|
onPreflight: () => void
|
|
onDispatch: () => void
|
|
onSave: () => void
|
|
}
|
|
|
|
export function WorkflowCanvasToolbar({
|
|
workflowName,
|
|
blueprintLabel,
|
|
blueprintDescription,
|
|
graphFamilyLabel,
|
|
graphFamilyClassName,
|
|
executionMode,
|
|
executionModeLabel,
|
|
executionModeClassName,
|
|
executionModeHint,
|
|
dispatchContextId,
|
|
executionModes,
|
|
selectedEdgeCount,
|
|
canAutoLayout,
|
|
hasValidationErrors,
|
|
isPreflightPending,
|
|
isDispatchPending,
|
|
isSaving,
|
|
onDispatchContextIdChange,
|
|
onExecutionModeChange,
|
|
onOpenNodeMenu,
|
|
onAutoLayout,
|
|
onDeleteSelectedEdges,
|
|
onPreflight,
|
|
onDispatch,
|
|
onSave,
|
|
}: WorkflowCanvasToolbarProps) {
|
|
return (
|
|
<div className="border-b border-border-default bg-surface px-3 py-2">
|
|
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
<div className="min-w-0 space-y-1">
|
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
<div className="flex items-center gap-2 rounded-full border border-border-default bg-surface-hover/60 px-2 py-0.5 text-[11px] font-medium text-content-secondary">
|
|
<GitBranch size={13} />
|
|
Workflow Canvas
|
|
</div>
|
|
<h1 className="truncate text-sm font-semibold text-content">{workflowName}</h1>
|
|
<span className={`rounded-full px-2 py-0.5 text-[11px] font-medium ${graphFamilyClassName}`}>
|
|
{graphFamilyLabel}
|
|
</span>
|
|
<span className={`rounded-full px-2 py-0.5 text-[11px] font-medium ${executionModeClassName}`}>
|
|
{executionModeLabel}
|
|
</span>
|
|
{blueprintLabel && (
|
|
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-700 dark:bg-slate-900/40 dark:text-slate-300">
|
|
{blueprintLabel}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2 text-[11px] text-content-muted">
|
|
{(blueprintDescription || executionModeHint) && (
|
|
<span className="inline-flex max-w-3xl items-center gap-1 rounded-full border border-border-default bg-surface px-2 py-0.5">
|
|
<BadgeInfo size={11} />
|
|
{blueprintDescription ?? executionModeHint}
|
|
</span>
|
|
)}
|
|
<span
|
|
className="inline-flex items-center gap-1 rounded-full border border-border-default bg-surface px-2 py-0.5"
|
|
title="Right-click anywhere on the canvas to open the searchable node picker."
|
|
>
|
|
<MousePointer2 size={11} />
|
|
Right-click to add
|
|
</span>
|
|
<span
|
|
className="inline-flex items-center gap-1 rounded-full border border-border-default bg-surface px-2 py-0.5"
|
|
title="Select an edge and press Delete, or use right-click / double-click to remove it."
|
|
>
|
|
<Trash2 size={11} />
|
|
Delete removes connections
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={onOpenNodeMenu}
|
|
className="flex items-center gap-1.5 rounded-lg border border-border-default px-2.5 py-1.5 text-sm text-content hover:bg-surface-hover"
|
|
title="Open searchable node picker"
|
|
>
|
|
<Plus size={14} />
|
|
Node
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={onAutoLayout}
|
|
disabled={!canAutoLayout}
|
|
className="flex items-center gap-1.5 rounded-lg border border-border-default px-2.5 py-1.5 text-sm text-content hover:bg-surface-hover disabled:opacity-50"
|
|
title="Automatically align nodes into a readable graph layout"
|
|
>
|
|
<LayoutGrid size={14} />
|
|
Align
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={onDeleteSelectedEdges}
|
|
disabled={selectedEdgeCount === 0}
|
|
className="flex items-center gap-1.5 rounded-lg border border-border-default px-2.5 py-1.5 text-sm text-content hover:bg-surface-hover disabled:opacity-50"
|
|
title="Delete the currently selected connection(s)"
|
|
>
|
|
<Trash2 size={14} />
|
|
{selectedEdgeCount > 1 ? `Delete (${selectedEdgeCount})` : 'Delete'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-2 flex flex-wrap items-center justify-between gap-2 border-t border-border-default/70 pt-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<label className="flex items-center gap-2 rounded-lg border border-border-default bg-surface px-2 py-1.5 text-xs text-content-secondary">
|
|
<span className="whitespace-nowrap">Context</span>
|
|
<input
|
|
value={dispatchContextId}
|
|
onChange={event => onDispatchContextIdChange(event.target.value)}
|
|
placeholder="context id"
|
|
className="w-40 bg-transparent text-sm text-content focus:outline-none lg:w-52"
|
|
/>
|
|
</label>
|
|
<label className="flex items-center gap-2 rounded-lg border border-border-default bg-surface px-2 py-1.5 text-xs text-content-secondary">
|
|
<span className="whitespace-nowrap">Mode</span>
|
|
<select
|
|
value={executionMode}
|
|
onChange={event => onExecutionModeChange(event.target.value)}
|
|
className="bg-transparent text-sm text-content focus:outline-none"
|
|
>
|
|
{executionModes.map(mode => (
|
|
<option key={mode.value} value={mode.value}>
|
|
{mode.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<button
|
|
onClick={onPreflight}
|
|
disabled={isPreflightPending || hasValidationErrors}
|
|
className="flex items-center gap-1.5 rounded-lg border border-border-default px-2.5 py-1.5 text-sm text-content hover:bg-surface-hover disabled:opacity-50"
|
|
title="Validate graph runtime readiness without dispatching tasks"
|
|
>
|
|
{isPreflightPending ? <Loader2 size={14} className="animate-spin" /> : <RefreshCw size={14} />}
|
|
{isPreflightPending ? 'Checking…' : 'Dry Run'}
|
|
</button>
|
|
<button
|
|
onClick={onDispatch}
|
|
disabled={isDispatchPending || hasValidationErrors}
|
|
className="flex items-center gap-1.5 rounded-lg border border-border-default px-2.5 py-1.5 text-sm text-content hover:bg-surface-hover disabled:opacity-50"
|
|
title="Manual graph runtime dispatch for workflow debugging"
|
|
>
|
|
{isDispatchPending ? <Loader2 size={14} className="animate-spin" /> : <Play size={14} />}
|
|
{isDispatchPending ? 'Dispatching…' : 'Run'}
|
|
</button>
|
|
<button
|
|
onClick={onSave}
|
|
disabled={isSaving || hasValidationErrors}
|
|
className="flex items-center gap-1.5 rounded-lg bg-accent px-2.5 py-1.5 text-sm text-white hover:bg-accent-hover disabled:opacity-50"
|
|
>
|
|
<Save size={14} />
|
|
{isSaving ? 'Saving…' : 'Save'}
|
|
</button>
|
|
</div>
|
|
<p className="text-[11px] text-content-muted">
|
|
{executionModeHint}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|