more polish on workflows
Some checks failed
CI / Rustfmt (push) Failing after 25s
CI / Clippy (push) Failing after 2m3s
CI / Cargo Audit & Deny (push) Successful in 33s
CI / Web Blocking Checks (push) Failing after 26s
CI / Security Blocking Checks (push) Successful in 8s
CI / Security Advisory Checks (push) Has been cancelled
CI / Web Advisory Checks (push) Has been cancelled
CI / Tests (push) Has been cancelled
Some checks failed
CI / Rustfmt (push) Failing after 25s
CI / Clippy (push) Failing after 2m3s
CI / Cargo Audit & Deny (push) Successful in 33s
CI / Web Blocking Checks (push) Failing after 26s
CI / Security Blocking Checks (push) Successful in 8s
CI / Security Advisory Checks (push) Has been cancelled
CI / Web Advisory Checks (push) Has been cancelled
CI / Tests (push) Has been cancelled
This commit is contained in:
@@ -22,10 +22,6 @@ export type ApiResponse_WorkflowResponse = {
|
||||
* Workflow description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Whether the workflow is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Workflow ID
|
||||
*/
|
||||
@@ -72,4 +68,3 @@ export type ApiResponse_WorkflowResponse = {
|
||||
*/
|
||||
message?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -14,10 +14,6 @@ export type CreateWorkflowRequest = {
|
||||
* Workflow description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Whether the workflow is enabled
|
||||
*/
|
||||
enabled?: boolean | null;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
@@ -47,4 +43,3 @@ export type CreateWorkflowRequest = {
|
||||
*/
|
||||
version: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -19,10 +19,6 @@ export type PaginatedResponse_WorkflowSummary = {
|
||||
* Workflow description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Whether the workflow is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Workflow ID
|
||||
*/
|
||||
@@ -57,4 +53,3 @@ export type PaginatedResponse_WorkflowSummary = {
|
||||
*/
|
||||
pagination: PaginationMeta;
|
||||
};
|
||||
|
||||
|
||||
@@ -14,10 +14,6 @@ export type UpdateWorkflowRequest = {
|
||||
* Workflow description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Whether the workflow is enabled
|
||||
*/
|
||||
enabled?: boolean | null;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
@@ -39,4 +35,3 @@ export type UpdateWorkflowRequest = {
|
||||
*/
|
||||
version?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -18,10 +18,6 @@ export type WorkflowResponse = {
|
||||
* Workflow description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Whether the workflow is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Workflow ID
|
||||
*/
|
||||
@@ -63,4 +59,3 @@ export type WorkflowResponse = {
|
||||
*/
|
||||
version: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -14,10 +14,6 @@ export type WorkflowSummary = {
|
||||
* Workflow description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Whether the workflow is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Workflow ID
|
||||
*/
|
||||
@@ -47,4 +43,3 @@ export type WorkflowSummary = {
|
||||
*/
|
||||
version: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ export class WorkflowsService {
|
||||
page,
|
||||
pageSize,
|
||||
tags,
|
||||
enabled,
|
||||
search,
|
||||
packRef,
|
||||
}: {
|
||||
@@ -73,10 +72,6 @@ export class WorkflowsService {
|
||||
* Filter by tag(s) - comma-separated list
|
||||
*/
|
||||
tags?: string | null,
|
||||
/**
|
||||
* Filter by enabled status
|
||||
*/
|
||||
enabled?: boolean | null,
|
||||
/**
|
||||
* Search term for label/description (case-insensitive)
|
||||
*/
|
||||
@@ -93,7 +88,6 @@ export class WorkflowsService {
|
||||
'page': page,
|
||||
'page_size': pageSize,
|
||||
'tags': tags,
|
||||
'enabled': enabled,
|
||||
'search': search,
|
||||
'pack_ref': packRef,
|
||||
},
|
||||
@@ -125,10 +119,6 @@ export class WorkflowsService {
|
||||
* Workflow description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Whether the workflow is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Workflow ID
|
||||
*/
|
||||
@@ -216,10 +206,6 @@ export class WorkflowsService {
|
||||
* Workflow description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Whether the workflow is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Workflow ID
|
||||
*/
|
||||
@@ -308,10 +294,6 @@ export class WorkflowsService {
|
||||
* Workflow description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Whether the workflow is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Workflow ID
|
||||
*/
|
||||
|
||||
@@ -63,7 +63,9 @@ const ARROW_LENGTH = 12;
|
||||
const ARROW_HALF_WIDTH = 5;
|
||||
const ARROW_DIRECTION_LOOKBACK_PX = 10;
|
||||
const ARROW_DIRECTION_SAMPLES = 48;
|
||||
const ARROW_SHAFT_OVERLAP_PX = 2;
|
||||
// Keep a small amount of shaft under the arrowhead so sample-based trimming
|
||||
// does not leave a visible gap on simple bezier edges without waypoints.
|
||||
const ARROW_SHAFT_OVERLAP_PX = 4;
|
||||
|
||||
/** Color for each edge type (alias for shared constant) */
|
||||
const EDGE_COLORS = EDGE_TYPE_COLORS;
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
import { useState } from "react";
|
||||
import { Pencil, Plus, X, LogIn, LogOut } from "lucide-react";
|
||||
import {
|
||||
Pencil,
|
||||
Plus,
|
||||
X,
|
||||
LogIn,
|
||||
LogOut,
|
||||
SlidersHorizontal,
|
||||
} from "lucide-react";
|
||||
import SchemaBuilder from "@/components/common/SchemaBuilder";
|
||||
import type { ParamDefinition } from "@/types/workflow";
|
||||
import type { CancellationPolicy, ParamDefinition } from "@/types/workflow";
|
||||
import { CANCELLATION_POLICY_LABELS } from "@/types/workflow";
|
||||
|
||||
interface WorkflowInputsPanelProps {
|
||||
label: string;
|
||||
version: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
cancellationPolicy: CancellationPolicy;
|
||||
parameters: Record<string, ParamDefinition>;
|
||||
output: Record<string, ParamDefinition>;
|
||||
onLabelChange: (label: string) => void;
|
||||
onVersionChange: (version: string) => void;
|
||||
onDescriptionChange: (description: string) => void;
|
||||
onTagsChange: (tags: string[]) => void;
|
||||
onCancellationPolicyChange: (policy: CancellationPolicy) => void;
|
||||
onParametersChange: (parameters: Record<string, ParamDefinition>) => void;
|
||||
onOutputChange: (output: Record<string, ParamDefinition>) => void;
|
||||
}
|
||||
@@ -82,8 +100,18 @@ function ParamSummaryList({
|
||||
}
|
||||
|
||||
export default function WorkflowInputsPanel({
|
||||
label,
|
||||
version,
|
||||
description,
|
||||
tags,
|
||||
cancellationPolicy,
|
||||
parameters,
|
||||
output,
|
||||
onLabelChange,
|
||||
onVersionChange,
|
||||
onDescriptionChange,
|
||||
onTagsChange,
|
||||
onCancellationPolicyChange,
|
||||
onParametersChange,
|
||||
onOutputChange,
|
||||
}: WorkflowInputsPanelProps) {
|
||||
@@ -123,8 +151,103 @@ export default function WorkflowInputsPanel({
|
||||
<>
|
||||
<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 gap-1.5 mb-2">
|
||||
<SlidersHorizontal className="w-3.5 h-3.5 text-blue-500" />
|
||||
<h4 className="text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Workflow
|
||||
</h4>
|
||||
</div>
|
||||
<div className="space-y-2.5 rounded-lg border border-gray-200 bg-white p-3">
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-600 mb-1">
|
||||
Label
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={label}
|
||||
onChange={(e) => onLabelChange(e.target.value)}
|
||||
className="w-full px-2.5 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Workflow Label"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-600 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
className="w-full px-2.5 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Workflow description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-600 mb-1">
|
||||
Version
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={version}
|
||||
onChange={(e) => onVersionChange(e.target.value)}
|
||||
className="w-full px-2.5 py-2 border border-gray-300 rounded-md text-sm font-mono focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="1.0.0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-600 mb-1">
|
||||
Tags
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tags.join(", ")}
|
||||
onChange={(e) =>
|
||||
onTagsChange(
|
||||
e.target.value
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean),
|
||||
)
|
||||
}
|
||||
className="w-full px-2.5 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="tag-one, tag-two"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-600 mb-1">
|
||||
Cancellation Policy
|
||||
</label>
|
||||
<select
|
||||
value={cancellationPolicy}
|
||||
onChange={(e) =>
|
||||
onCancellationPolicyChange(
|
||||
e.target.value as CancellationPolicy,
|
||||
)
|
||||
}
|
||||
className="w-full px-2.5 py-2 border border-gray-300 rounded-md text-sm text-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
||||
title="Controls how running tasks behave when the workflow is cancelled"
|
||||
>
|
||||
{Object.entries(CANCELLATION_POLICY_LABELS).map(
|
||||
([value, optionLabel]) => (
|
||||
<option key={value} value={value}>
|
||||
{optionLabel}
|
||||
</option>
|
||||
),
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input Parameters */}
|
||||
<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">
|
||||
<LogIn className="w-3.5 h-3.5 text-green-500" />
|
||||
|
||||
@@ -10,7 +10,6 @@ interface WorkflowsQueryParams {
|
||||
pageSize?: number;
|
||||
packRef?: string;
|
||||
tags?: string;
|
||||
enabled?: boolean;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
@@ -23,7 +22,6 @@ export function useWorkflows(params?: WorkflowsQueryParams) {
|
||||
page: params?.page || 1,
|
||||
pageSize: params?.pageSize || 50,
|
||||
tags: params?.tags,
|
||||
enabled: params?.enabled,
|
||||
search: params?.search,
|
||||
packRef: params?.packRef,
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ExternalLink,
|
||||
Copy,
|
||||
Check,
|
||||
PanelLeftClose,
|
||||
} from "lucide-react";
|
||||
import SearchableSelect from "@/components/common/SearchableSelect";
|
||||
import yaml from "js-yaml";
|
||||
@@ -40,7 +41,6 @@ import type {
|
||||
WorkflowBuilderState,
|
||||
PaletteAction,
|
||||
TransitionPreset,
|
||||
CancellationPolicy,
|
||||
} from "@/types/workflow";
|
||||
import {
|
||||
generateUniqueTaskName,
|
||||
@@ -54,7 +54,6 @@ import {
|
||||
removeTaskFromTransitions,
|
||||
renameTaskInTransitions,
|
||||
findStartingTaskIds,
|
||||
CANCELLATION_POLICY_LABELS,
|
||||
} from "@/types/workflow";
|
||||
|
||||
const INITIAL_STATE: WorkflowBuilderState = {
|
||||
@@ -68,10 +67,13 @@ const INITIAL_STATE: WorkflowBuilderState = {
|
||||
vars: {},
|
||||
tasks: [],
|
||||
tags: [],
|
||||
enabled: true,
|
||||
cancellationPolicy: "allow_finish",
|
||||
};
|
||||
|
||||
const ACTIONS_SIDEBAR_WIDTH = 256;
|
||||
const WORKFLOW_OPTIONS_DEFAULT_WIDTH = 360;
|
||||
const WORKFLOW_OPTIONS_STORAGE_KEY = "workflow-builder-options-width";
|
||||
|
||||
export default function WorkflowBuilderPage() {
|
||||
const navigate = useNavigate();
|
||||
const { ref: editRef } = useParams<{ ref?: string }>();
|
||||
@@ -104,10 +106,19 @@ export default function WorkflowBuilderPage() {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [showYamlPreview, setShowYamlPreview] = useState(false);
|
||||
const [sidebarTab, setSidebarTab] = useState<"actions" | "inputs">("actions");
|
||||
const [workflowOptionsWidth, setWorkflowOptionsWidth] = useState<number>(() => {
|
||||
if (typeof window === "undefined") {
|
||||
return WORKFLOW_OPTIONS_DEFAULT_WIDTH;
|
||||
}
|
||||
const saved = window.localStorage.getItem(WORKFLOW_OPTIONS_STORAGE_KEY);
|
||||
const parsed = saved ? Number(saved) : NaN;
|
||||
return Number.isFinite(parsed) ? parsed : WORKFLOW_OPTIONS_DEFAULT_WIDTH;
|
||||
});
|
||||
const [highlightedTransition, setHighlightedTransition] = useState<{
|
||||
taskId: string;
|
||||
transitionIndex: number;
|
||||
} | null>(null);
|
||||
const [isResizingSidebar, setIsResizingSidebar] = useState(false);
|
||||
|
||||
// Start-node warning toast state
|
||||
const [startWarningVisible, setStartWarningVisible] = useState(false);
|
||||
@@ -261,6 +272,71 @@ export default function WorkflowBuilderPage() {
|
||||
return null;
|
||||
}, [state.tasks, startingTaskIds]);
|
||||
|
||||
const getMaxWorkflowOptionsWidth = useCallback(() => {
|
||||
if (typeof window === "undefined") {
|
||||
return WORKFLOW_OPTIONS_DEFAULT_WIDTH;
|
||||
}
|
||||
return Math.max(
|
||||
ACTIONS_SIDEBAR_WIDTH,
|
||||
Math.floor(window.innerWidth * 0.5),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const clampWorkflowOptionsWidth = useCallback(
|
||||
(width: number) =>
|
||||
Math.min(
|
||||
Math.max(Math.round(width), ACTIONS_SIDEBAR_WIDTH),
|
||||
getMaxWorkflowOptionsWidth(),
|
||||
),
|
||||
[getMaxWorkflowOptionsWidth],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setWorkflowOptionsWidth((prev) => clampWorkflowOptionsWidth(prev));
|
||||
}, [clampWorkflowOptionsWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWorkflowOptionsWidth((prev) => clampWorkflowOptionsWidth(prev));
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, [clampWorkflowOptionsWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(
|
||||
WORKFLOW_OPTIONS_STORAGE_KEY,
|
||||
String(workflowOptionsWidth),
|
||||
);
|
||||
}, [workflowOptionsWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isResizingSidebar) return;
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
setWorkflowOptionsWidth(clampWorkflowOptionsWidth(event.clientX));
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizingSidebar(false);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [isResizingSidebar, clampWorkflowOptionsWidth]);
|
||||
|
||||
// Render-phase state adjustment: detect warning key changes for immediate
|
||||
// show/hide without refs or synchronous setState inside effects.
|
||||
const warningKey = startNodeWarning
|
||||
@@ -475,7 +551,6 @@ export default function WorkflowBuilderPage() {
|
||||
out_schema:
|
||||
Object.keys(state.output).length > 0 ? state.output : undefined,
|
||||
tags: state.tags.length > 0 ? state.tags : undefined,
|
||||
enabled: state.enabled,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -493,7 +568,6 @@ export default function WorkflowBuilderPage() {
|
||||
out_schema:
|
||||
Object.keys(state.output).length > 0 ? state.output : undefined,
|
||||
tags: state.tags.length > 0 ? state.tags : undefined,
|
||||
enabled: state.enabled,
|
||||
};
|
||||
try {
|
||||
await saveWorkflowFile.mutateAsync(fileData);
|
||||
@@ -635,6 +709,8 @@ export default function WorkflowBuilderPage() {
|
||||
|
||||
const isSaving = saveWorkflowFile.isPending || updateWorkflowFile.isPending;
|
||||
const isExecuting = requestExecution.isPending;
|
||||
const sidebarWidth =
|
||||
sidebarTab === "inputs" ? workflowOptionsWidth : ACTIONS_SIDEBAR_WIDTH;
|
||||
|
||||
if (isEditing && workflowLoading) {
|
||||
return (
|
||||
@@ -675,7 +751,7 @@ export default function WorkflowBuilderPage() {
|
||||
disabled={isEditing}
|
||||
/>
|
||||
|
||||
<span className="text-gray-400 text-lg font-light">/</span>
|
||||
<span className="text-gray-400 text-lg font-light">/</span>
|
||||
|
||||
{/* Workflow name */}
|
||||
<input
|
||||
@@ -692,24 +768,9 @@ export default function WorkflowBuilderPage() {
|
||||
/>
|
||||
|
||||
<span className="text-gray-400 text-lg font-light">—</span>
|
||||
|
||||
{/* Label */}
|
||||
<input
|
||||
type="text"
|
||||
value={state.label}
|
||||
onChange={(e) => updateMetadata({ label: e.target.value })}
|
||||
className="px-2 py-1.5 border border-gray-300 rounded text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-1 min-w-[160px] max-w-[300px]"
|
||||
placeholder="Workflow Label"
|
||||
/>
|
||||
|
||||
{/* Version */}
|
||||
<input
|
||||
type="text"
|
||||
value={state.version}
|
||||
onChange={(e) => updateMetadata({ version: e.target.value })}
|
||||
className="px-2 py-1.5 border border-gray-300 rounded text-sm font-mono focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-20"
|
||||
placeholder="1.0.0"
|
||||
/>
|
||||
<span className="truncate text-sm text-gray-600">
|
||||
{state.label || "Untitled workflow"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -819,59 +880,6 @@ export default function WorkflowBuilderPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description row (collapsible) */}
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={state.description}
|
||||
onChange={(e) => updateMetadata({ description: e.target.value })}
|
||||
className="flex-1 px-2 py-1 border border-gray-200 rounded text-xs text-gray-600 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Workflow description (optional)"
|
||||
/>
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
value={state.tags.join(", ")}
|
||||
onChange={(e) =>
|
||||
updateMetadata({
|
||||
tags: e.target.value
|
||||
.split(",")
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean),
|
||||
})
|
||||
}
|
||||
className="px-2 py-1 border border-gray-200 rounded text-xs text-gray-600 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 w-40"
|
||||
placeholder="Tags (comma-sep)"
|
||||
/>
|
||||
<label className="flex items-center gap-1 text-xs text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={state.enabled}
|
||||
onChange={(e) => updateMetadata({ enabled: e.target.checked })}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
Enabled
|
||||
</label>
|
||||
<select
|
||||
value={state.cancellationPolicy}
|
||||
onChange={(e) =>
|
||||
updateMetadata({
|
||||
cancellationPolicy: e.target.value as CancellationPolicy,
|
||||
})
|
||||
}
|
||||
className="px-2 py-1 border border-gray-200 rounded text-xs text-gray-600 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
||||
title="Cancellation policy: controls how running tasks behave when the workflow is cancelled"
|
||||
>
|
||||
{Object.entries(CANCELLATION_POLICY_LABELS).map(
|
||||
([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
),
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Validation errors panel */}
|
||||
@@ -987,8 +995,11 @@ export default function WorkflowBuilderPage() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Left sidebar: tabbed Actions / Inputs */}
|
||||
<div className="w-64 border-r border-gray-200 bg-gray-50 flex flex-col h-full overflow-hidden">
|
||||
{/* Left sidebar: tabbed Actions / Workflow Options */}
|
||||
<div
|
||||
className="border-r border-gray-200 bg-gray-50 flex flex-col h-full overflow-hidden relative flex-shrink-0"
|
||||
style={{ width: sidebarWidth }}
|
||||
>
|
||||
{/* Tab header */}
|
||||
<div className="flex border-b border-gray-200 bg-white flex-shrink-0">
|
||||
<button
|
||||
@@ -1011,7 +1022,7 @@ export default function WorkflowBuilderPage() {
|
||||
}`}
|
||||
>
|
||||
<Settings2 className="w-3.5 h-3.5" />
|
||||
Inputs
|
||||
Workflow Options
|
||||
{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}
|
||||
@@ -1029,8 +1040,22 @@ export default function WorkflowBuilderPage() {
|
||||
/>
|
||||
) : (
|
||||
<WorkflowInputsPanel
|
||||
label={state.label}
|
||||
version={state.version}
|
||||
description={state.description}
|
||||
tags={state.tags}
|
||||
cancellationPolicy={state.cancellationPolicy}
|
||||
parameters={state.parameters}
|
||||
output={state.output}
|
||||
onLabelChange={(label) => updateMetadata({ label })}
|
||||
onVersionChange={(version) => updateMetadata({ version })}
|
||||
onDescriptionChange={(description) =>
|
||||
updateMetadata({ description })
|
||||
}
|
||||
onTagsChange={(tags) => updateMetadata({ tags })}
|
||||
onCancellationPolicyChange={(cancellationPolicy) =>
|
||||
updateMetadata({ cancellationPolicy })
|
||||
}
|
||||
onParametersChange={(parameters) =>
|
||||
setState((prev) => ({ ...prev, parameters }))
|
||||
}
|
||||
@@ -1039,6 +1064,30 @@ export default function WorkflowBuilderPage() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sidebarTab === "inputs" && (
|
||||
<div
|
||||
className={`absolute top-0 right-0 h-full w-2 translate-x-1/2 cursor-col-resize group ${
|
||||
isResizingSidebar ? "z-30" : "z-10"
|
||||
}`}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
setIsResizingSidebar(true);
|
||||
}}
|
||||
title="Resize workflow options panel"
|
||||
>
|
||||
<div
|
||||
className={`mx-auto h-full w-px transition-colors ${
|
||||
isResizingSidebar
|
||||
? "bg-blue-500"
|
||||
: "bg-transparent group-hover:bg-blue-300"
|
||||
}`}
|
||||
/>
|
||||
<div className="absolute top-3 right-0 -translate-y-1/2 translate-x-1/2 rounded-full border border-gray-200 bg-white p-1 text-gray-300 shadow-sm group-hover:text-blue-500">
|
||||
<PanelLeftClose className="w-3 h-3" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Center: Canvas */}
|
||||
|
||||
@@ -224,8 +224,6 @@ export interface WorkflowBuilderState {
|
||||
tasks: WorkflowTask[];
|
||||
/** Tags */
|
||||
tags: string[];
|
||||
/** Whether the workflow is enabled */
|
||||
enabled: boolean;
|
||||
/** Cancellation policy (default: allow_finish) */
|
||||
cancellationPolicy: CancellationPolicy;
|
||||
}
|
||||
@@ -285,7 +283,6 @@ export interface ActionYamlDefinition {
|
||||
ref: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
workflow_file: string;
|
||||
parameters?: Record<string, unknown>;
|
||||
output?: Record<string, unknown>;
|
||||
@@ -358,8 +355,6 @@ export interface SaveWorkflowFileRequest {
|
||||
out_schema?: Record<string, unknown>;
|
||||
/** Tags */
|
||||
tags?: string[];
|
||||
/** Whether the workflow is enabled */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/** An action summary used in the action palette */
|
||||
@@ -581,7 +576,6 @@ export function builderStateToActionYaml(
|
||||
const action: ActionYamlDefinition = {
|
||||
ref: `${state.packRef}.${state.name}`,
|
||||
label: state.label,
|
||||
enabled: state.enabled,
|
||||
workflow_file: `workflows/${state.name}.workflow.yaml`,
|
||||
};
|
||||
|
||||
@@ -751,7 +745,6 @@ export function definitionToBuilderState(
|
||||
vars: definition.vars || {},
|
||||
tasks,
|
||||
tags: definition.tags || [],
|
||||
enabled: true,
|
||||
cancellationPolicy: definition.cancellation_policy || "allow_finish",
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user