inputs for workflows
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { Search, X, ChevronDown, ChevronRight, GripVertical } from "lucide-react";
|
||||
import {
|
||||
Search,
|
||||
X,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
GripVertical,
|
||||
} from "lucide-react";
|
||||
import type { PaletteAction } from "@/types/workflow";
|
||||
|
||||
interface ActionPaletteProps {
|
||||
@@ -24,7 +30,7 @@ export default function ActionPalette({
|
||||
action.label?.toLowerCase().includes(query) ||
|
||||
action.ref?.toLowerCase().includes(query) ||
|
||||
action.description?.toLowerCase().includes(query) ||
|
||||
action.pack_ref?.toLowerCase().includes(query)
|
||||
action.pack_ref?.toLowerCase().includes(query),
|
||||
);
|
||||
}, [actions, searchQuery]);
|
||||
|
||||
@@ -38,7 +44,7 @@ export default function ActionPalette({
|
||||
grouped.get(packRef)!.push(action);
|
||||
});
|
||||
return new Map(
|
||||
[...grouped.entries()].sort((a, b) => a[0].localeCompare(b[0]))
|
||||
[...grouped.entries()].sort((a, b) => a[0].localeCompare(b[0])),
|
||||
);
|
||||
}, [filteredActions]);
|
||||
|
||||
@@ -55,7 +61,7 @@ export default function ActionPalette({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 border-r border-gray-200 bg-gray-50 flex flex-col h-full overflow-hidden">
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<div className="p-3 border-b border-gray-200 bg-white flex-shrink-0">
|
||||
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider mb-2">
|
||||
Action Palette
|
||||
@@ -93,7 +99,9 @@ export default function ActionPalette({
|
||||
</div>
|
||||
) : filteredActions.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-xs text-gray-500">No actions match your search</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
No actions match your search
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="mt-1 text-xs text-blue-600 hover:text-blue-800"
|
||||
@@ -158,7 +166,7 @@ export default function ActionPalette({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
246
web/src/components/workflows/WorkflowInputsPanel.tsx
Normal file
246
web/src/components/workflows/WorkflowInputsPanel.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import { useState } from "react";
|
||||
import { Pencil, Plus, X, LogIn, LogOut } from "lucide-react";
|
||||
import SchemaBuilder from "@/components/common/SchemaBuilder";
|
||||
import type { ParamDefinition } from "@/types/workflow";
|
||||
|
||||
interface WorkflowInputsPanelProps {
|
||||
parameters: Record<string, ParamDefinition>;
|
||||
output: Record<string, ParamDefinition>;
|
||||
onParametersChange: (parameters: Record<string, ParamDefinition>) => void;
|
||||
onOutputChange: (output: Record<string, ParamDefinition>) => void;
|
||||
}
|
||||
|
||||
type ModalTarget = "parameters" | "output" | null;
|
||||
|
||||
function ParamSummaryList({
|
||||
schema,
|
||||
emptyMessage,
|
||||
onEdit,
|
||||
}: {
|
||||
schema: Record<string, ParamDefinition>;
|
||||
emptyMessage: string;
|
||||
onEdit: () => void;
|
||||
}) {
|
||||
const entries = Object.entries(schema);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="w-full px-3 py-4 border-2 border-dashed border-gray-200 rounded-lg text-center hover:border-blue-300 hover:bg-blue-50/30 transition-colors group"
|
||||
>
|
||||
<Plus className="w-4 h-4 text-gray-300 group-hover:text-blue-400 mx-auto mb-1" />
|
||||
<span className="text-[11px] text-gray-400 group-hover:text-blue-500">
|
||||
{emptyMessage}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{entries.map(([name, def]) => (
|
||||
<div
|
||||
key={name}
|
||||
className="flex items-center gap-1.5 px-2 py-1.5 bg-white border border-gray-150 rounded-md"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-mono text-[11px] font-medium text-gray-800 truncate">
|
||||
{name}
|
||||
</span>
|
||||
{def.required && (
|
||||
<span className="text-[9px] font-semibold text-red-500">*</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] text-blue-600/70">{def.type}</span>
|
||||
{def.secret && (
|
||||
<span className="text-[9px] text-amber-500">secret</span>
|
||||
)}
|
||||
{def.default !== undefined && (
|
||||
<span
|
||||
className="text-[9px] text-gray-400 truncate max-w-[80px]"
|
||||
title={`default: ${JSON.stringify(def.default)}`}
|
||||
>
|
||||
= {JSON.stringify(def.default)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="flex items-center gap-1 px-2 py-1 text-[11px] text-gray-400 hover:text-blue-600 transition-colors w-full"
|
||||
>
|
||||
<Pencil className="w-3 h-3" />
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WorkflowInputsPanel({
|
||||
parameters,
|
||||
output,
|
||||
onParametersChange,
|
||||
onOutputChange,
|
||||
}: WorkflowInputsPanelProps) {
|
||||
const [modalTarget, setModalTarget] = useState<ModalTarget>(null);
|
||||
|
||||
// Draft state for the modal so changes only apply on confirm
|
||||
const [draftSchema, setDraftSchema] = useState<
|
||||
Record<string, ParamDefinition>
|
||||
>({});
|
||||
|
||||
const openModal = (target: ModalTarget) => {
|
||||
if (target === "parameters") {
|
||||
setDraftSchema({ ...parameters });
|
||||
} else if (target === "output") {
|
||||
setDraftSchema({ ...output });
|
||||
}
|
||||
setModalTarget(target);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (modalTarget === "parameters") {
|
||||
onParametersChange(draftSchema);
|
||||
} else if (modalTarget === "output") {
|
||||
onOutputChange(draftSchema);
|
||||
}
|
||||
setModalTarget(null);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setModalTarget(null);
|
||||
};
|
||||
|
||||
const modalLabel =
|
||||
modalTarget === "parameters" ? "Input Parameters" : "Output";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-4">
|
||||
{/* Input Parameters */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<LogIn className="w-3.5 h-3.5 text-green-500" />
|
||||
<h4 className="text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Inputs
|
||||
</h4>
|
||||
</div>
|
||||
{Object.keys(parameters).length > 0 && (
|
||||
<span className="text-[10px] text-gray-400">
|
||||
{Object.keys(parameters).length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 mb-2">
|
||||
Referenced via{" "}
|
||||
<code className="px-0.5 bg-gray-100 rounded">
|
||||
{"{{ params.<name> }}"}
|
||||
</code>
|
||||
</p>
|
||||
<ParamSummaryList
|
||||
schema={parameters}
|
||||
emptyMessage="Add input parameters"
|
||||
onEdit={() => openModal("parameters")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Output Schema */}
|
||||
<div className="border-t border-gray-200 pt-3">
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<LogOut className="w-3.5 h-3.5 text-violet-500" />
|
||||
<h4 className="text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Output
|
||||
</h4>
|
||||
</div>
|
||||
{Object.keys(output).length > 0 && (
|
||||
<span className="text-[10px] text-gray-400">
|
||||
{Object.keys(output).length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 mb-2">
|
||||
Values this workflow produces on completion.
|
||||
</p>
|
||||
<ParamSummaryList
|
||||
schema={output}
|
||||
emptyMessage="Add output schema"
|
||||
onEdit={() => openModal("output")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Full-screen modal for SchemaBuilder editing */}
|
||||
{modalTarget && (
|
||||
<div className="fixed inset-0 z-[70] flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40"
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
{/* Modal */}
|
||||
<div className="relative bg-white rounded-xl shadow-2xl border border-gray-200 flex flex-col w-full max-w-2xl max-h-[80vh] mx-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 flex-shrink-0">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-gray-900">
|
||||
{modalLabel}
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
{modalTarget === "parameters"
|
||||
? "Define the inputs this workflow accepts when executed."
|
||||
: "Define the outputs this workflow produces upon completion."}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
<SchemaBuilder
|
||||
value={draftSchema}
|
||||
onChange={(schema) =>
|
||||
setDraftSchema(schema as Record<string, ParamDefinition>)
|
||||
}
|
||||
placeholder={
|
||||
modalTarget === "parameters"
|
||||
? '{"message": {"type": "string", "required": true}}'
|
||||
: '{"result": {"type": "string"}}'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-2 px-6 py-3 border-t border-gray-200 bg-gray-50 rounded-b-xl flex-shrink-0">
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors shadow-sm"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -9,10 +9,13 @@ import {
|
||||
Code,
|
||||
LayoutDashboard,
|
||||
X,
|
||||
Zap,
|
||||
Settings2,
|
||||
} from "lucide-react";
|
||||
import yaml from "js-yaml";
|
||||
import type { WorkflowYamlDefinition } from "@/types/workflow";
|
||||
import ActionPalette from "@/components/workflows/ActionPalette";
|
||||
import WorkflowInputsPanel from "@/components/workflows/WorkflowInputsPanel";
|
||||
import WorkflowCanvas from "@/components/workflows/WorkflowCanvas";
|
||||
import type { EdgeHoverInfo } from "@/components/workflows/WorkflowEdges";
|
||||
import TaskInspector from "@/components/workflows/TaskInspector";
|
||||
@@ -83,6 +86,7 @@ export default function WorkflowBuilderPage() {
|
||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [showYamlPreview, setShowYamlPreview] = useState(false);
|
||||
const [sidebarTab, setSidebarTab] = useState<"actions" | "inputs">("actions");
|
||||
const [highlightedTransition, setHighlightedTransition] = useState<{
|
||||
taskId: string;
|
||||
transitionIndex: number;
|
||||
@@ -747,12 +751,59 @@ export default function WorkflowBuilderPage() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Left: Action Palette */}
|
||||
{/* Left sidebar: tabbed Actions / Inputs */}
|
||||
<div className="w-64 border-r border-gray-200 bg-gray-50 flex flex-col h-full overflow-hidden">
|
||||
{/* Tab header */}
|
||||
<div className="flex border-b border-gray-200 bg-white flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setSidebarTab("actions")}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors ${
|
||||
sidebarTab === "actions"
|
||||
? "text-blue-600 border-b-2 border-blue-600 bg-blue-50/50"
|
||||
: "text-gray-500 hover:text-gray-700 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<Zap className="w-3.5 h-3.5" />
|
||||
Actions
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSidebarTab("inputs")}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors ${
|
||||
sidebarTab === "inputs"
|
||||
? "text-blue-600 border-b-2 border-blue-600 bg-blue-50/50"
|
||||
: "text-gray-500 hover:text-gray-700 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<Settings2 className="w-3.5 h-3.5" />
|
||||
Inputs
|
||||
{Object.keys(state.parameters).length > 0 && (
|
||||
<span className="text-[10px] bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded-full">
|
||||
{Object.keys(state.parameters).length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
{sidebarTab === "actions" ? (
|
||||
<ActionPalette
|
||||
actions={paletteActions}
|
||||
isLoading={actionsLoading}
|
||||
onAddTask={handleAddTaskFromPalette}
|
||||
/>
|
||||
) : (
|
||||
<WorkflowInputsPanel
|
||||
parameters={state.parameters}
|
||||
output={state.output}
|
||||
onParametersChange={(parameters) =>
|
||||
setState((prev) => ({ ...prev, parameters }))
|
||||
}
|
||||
onOutputChange={(output) =>
|
||||
setState((prev) => ({ ...prev, output }))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Center: Canvas */}
|
||||
<WorkflowCanvas
|
||||
|
||||
Reference in New Issue
Block a user