Files
HartOMat/frontend/src/components/workflows/WorkflowCanvasToolbar.tsx
T

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>
)
}