proper sql filtering
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
import { useChildExecutions } from "@/hooks/useExecutions";
|
||||
import { useExecutionStream } from "@/hooks/useExecutionStream";
|
||||
|
||||
interface WorkflowTasksPanelProps {
|
||||
/** The parent (workflow) execution ID */
|
||||
@@ -95,6 +96,11 @@ export default function WorkflowTasksPanel({
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
||||
const { data, isLoading, error } = useChildExecutions(parentExecutionId);
|
||||
|
||||
// Subscribe to the unfiltered execution stream so that child execution
|
||||
// WebSocket notifications update the ["executions", { parent }] query cache
|
||||
// in real-time (the detail page only subscribes filtered by its own ID).
|
||||
useExecutionStream({ enabled: true });
|
||||
|
||||
const tasks = useMemo(() => {
|
||||
if (!data?.data) return [];
|
||||
return data.data;
|
||||
@@ -211,15 +217,20 @@ export default function WorkflowTasksPanel({
|
||||
const maxRetries = wt?.max_retries ?? 0;
|
||||
const timedOut = wt?.timed_out ?? false;
|
||||
|
||||
// Compute duration from created → updated (best available)
|
||||
// Compute duration from started_at → updated (actual run time)
|
||||
const startedAt = task.started_at
|
||||
? new Date(task.started_at)
|
||||
: null;
|
||||
const created = new Date(task.created);
|
||||
const updated = new Date(task.updated);
|
||||
const isTerminal =
|
||||
task.status === "completed" ||
|
||||
task.status === "failed" ||
|
||||
task.status === "timeout";
|
||||
const durationMs =
|
||||
wt?.duration_ms ??
|
||||
(task.status === "completed" ||
|
||||
task.status === "failed" ||
|
||||
task.status === "timeout"
|
||||
? updated.getTime() - created.getTime()
|
||||
(isTerminal && startedAt
|
||||
? updated.getTime() - startedAt.getTime()
|
||||
: null);
|
||||
|
||||
return (
|
||||
@@ -277,7 +288,10 @@ export default function WorkflowTasksPanel({
|
||||
<div className="col-span-2 text-sm text-gray-500">
|
||||
{task.status === "running" ? (
|
||||
<span className="text-blue-600">
|
||||
{formatDistanceToNow(created, { addSuffix: false })}…
|
||||
{formatDistanceToNow(startedAt ?? created, {
|
||||
addSuffix: false,
|
||||
})}
|
||||
…
|
||||
</span>
|
||||
) : durationMs != null && durationMs > 0 ? (
|
||||
formatDuration(durationMs)
|
||||
|
||||
@@ -70,11 +70,14 @@ const ExecutionPreviewPanel = memo(function ExecutionPreviewPanel({
|
||||
execution?.status === "scheduled" ||
|
||||
execution?.status === "requested";
|
||||
|
||||
const startedAt = execution?.started_at
|
||||
? new Date(execution.started_at)
|
||||
: null;
|
||||
const created = execution ? new Date(execution.created) : null;
|
||||
const updated = execution ? new Date(execution.updated) : null;
|
||||
const durationMs =
|
||||
created && updated && !isRunning
|
||||
? updated.getTime() - created.getTime()
|
||||
startedAt && updated && !isRunning
|
||||
? updated.getTime() - startedAt.getTime()
|
||||
: null;
|
||||
|
||||
return (
|
||||
@@ -175,9 +178,9 @@ const ExecutionPreviewPanel = memo(function ExecutionPreviewPanel({
|
||||
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
||||
Elapsed
|
||||
</dt>
|
||||
<dd className="mt-0.5 text-sm text-blue-600 flex items-center gap-1.5">
|
||||
<dd className="mt-0.5 text-blue-600 flex items-center gap-1.5">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
{formatDistanceToNow(created!)}
|
||||
{formatDistanceToNow(startedAt ?? created!)}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
@@ -240,41 +243,39 @@ const ExecutionPreviewPanel = memo(function ExecutionPreviewPanel({
|
||||
</div>
|
||||
|
||||
{/* Config / Parameters */}
|
||||
{execution.config &&
|
||||
Object.keys(execution.config).length > 0 && (
|
||||
<div className="px-4 py-3">
|
||||
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1.5">
|
||||
Parameters
|
||||
</dt>
|
||||
<dd>
|
||||
<pre className="bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-x-auto max-h-48 overflow-y-auto">
|
||||
{JSON.stringify(execution.config, null, 2)}
|
||||
</pre>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{execution.config && Object.keys(execution.config).length > 0 && (
|
||||
<div className="px-4 py-3">
|
||||
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1.5">
|
||||
Parameters
|
||||
</dt>
|
||||
<dd>
|
||||
<pre className="bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-x-auto max-h-48 overflow-y-auto">
|
||||
{JSON.stringify(execution.config, null, 2)}
|
||||
</pre>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result */}
|
||||
{execution.result &&
|
||||
Object.keys(execution.result).length > 0 && (
|
||||
<div className="px-4 py-3">
|
||||
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1.5">
|
||||
Result
|
||||
</dt>
|
||||
<dd>
|
||||
<pre
|
||||
className={`border rounded p-3 text-xs overflow-x-auto max-h-64 overflow-y-auto ${
|
||||
execution.status === ("failed" as ExecutionStatus) ||
|
||||
execution.status === ("timeout" as ExecutionStatus)
|
||||
? "bg-red-50 border-red-200"
|
||||
: "bg-gray-50 border-gray-200"
|
||||
}`}
|
||||
>
|
||||
{JSON.stringify(execution.result, null, 2)}
|
||||
</pre>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{execution.result && Object.keys(execution.result).length > 0 && (
|
||||
<div className="px-4 py-3">
|
||||
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1.5">
|
||||
Result
|
||||
</dt>
|
||||
<dd>
|
||||
<pre
|
||||
className={`border rounded p-3 text-xs overflow-x-auto max-h-64 overflow-y-auto ${
|
||||
execution.status === ("failed" as ExecutionStatus) ||
|
||||
execution.status === ("timeout" as ExecutionStatus)
|
||||
? "bg-red-50 border-red-200"
|
||||
: "bg-gray-50 border-gray-200"
|
||||
}`}
|
||||
>
|
||||
{JSON.stringify(execution.result, null, 2)}
|
||||
</pre>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -128,6 +128,7 @@ const ChildExecutionRow = memo(function ChildExecutionRow({
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
data-execution-id={execution.id}
|
||||
className={`hover:bg-gray-50/80 group border-t border-gray-100 cursor-pointer ${
|
||||
selectedExecutionId === execution.id
|
||||
? "bg-blue-50 hover:bg-blue-50"
|
||||
@@ -314,6 +315,7 @@ const WorkflowExecutionRow = memo(function WorkflowExecutionRow({
|
||||
<>
|
||||
{/* Main execution row */}
|
||||
<tr
|
||||
data-execution-id={execution.id}
|
||||
className={`hover:bg-gray-50 border-b border-gray-200 cursor-pointer ${
|
||||
selectedExecutionId === execution.id
|
||||
? "bg-blue-50 hover:bg-blue-50"
|
||||
|
||||
195
web/src/components/workflows/RunWorkflowModal.tsx
Normal file
195
web/src/components/workflows/RunWorkflowModal.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { Play, X, ExternalLink } from "lucide-react";
|
||||
import ParamSchemaForm, {
|
||||
validateParamSchema,
|
||||
extractProperties,
|
||||
type ParamSchema,
|
||||
} from "@/components/common/ParamSchemaForm";
|
||||
import { useRequestExecution } from "@/hooks/useExecutions";
|
||||
|
||||
interface RunWorkflowModalProps {
|
||||
/** The workflow's action ref (e.g., "examples.hello_workflow") */
|
||||
actionRef: string;
|
||||
/** The workflow's param_schema in flat format */
|
||||
paramSchema: ParamSchema;
|
||||
/** Called before executing — should save the workflow. Return true if save succeeded. */
|
||||
onSave: () => Promise<boolean>;
|
||||
/** Called when the modal is closed (cancel or after successful execution) */
|
||||
onClose: () => void;
|
||||
/** Optional label for display */
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal for running a workflow with optional parameter overrides.
|
||||
*
|
||||
* Shown from the workflow builder's "Run" button when the workflow has
|
||||
* parameters defined. Displays a ParamSchemaForm pre-populated with
|
||||
* default values, saves the workflow first, then creates an execution
|
||||
* and opens the execution detail page in a new tab.
|
||||
*/
|
||||
export default function RunWorkflowModal({
|
||||
actionRef,
|
||||
paramSchema,
|
||||
onSave,
|
||||
onClose,
|
||||
label,
|
||||
}: RunWorkflowModalProps) {
|
||||
const requestExecution = useRequestExecution();
|
||||
|
||||
const paramProperties = extractProperties(paramSchema);
|
||||
|
||||
// Build initial values from schema defaults
|
||||
const buildInitialValues = (): Record<string, unknown> => {
|
||||
const values: Record<string, unknown> = {};
|
||||
for (const [key, prop] of Object.entries(paramProperties)) {
|
||||
if (prop?.default !== undefined) {
|
||||
values[key] = prop.default;
|
||||
}
|
||||
}
|
||||
return values;
|
||||
};
|
||||
|
||||
const [parameters, setParameters] =
|
||||
useState<Record<string, unknown>>(buildInitialValues);
|
||||
const [paramErrors, setParamErrors] = useState<Record<string, string>>({});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [phase, setPhase] = useState<"idle" | "saving" | "executing">("idle");
|
||||
|
||||
const isSubmitting = phase !== "idle";
|
||||
|
||||
const handleExecute = useCallback(async () => {
|
||||
// Validate parameters against schema
|
||||
const errors = validateParamSchema(paramSchema, parameters);
|
||||
setParamErrors(errors);
|
||||
if (Object.keys(errors).length > 0) return;
|
||||
|
||||
setError(null);
|
||||
|
||||
// Phase 1: Save the workflow
|
||||
setPhase("saving");
|
||||
try {
|
||||
const saved = await onSave();
|
||||
if (!saved) {
|
||||
setPhase("idle");
|
||||
return; // save failed — error shown by parent
|
||||
}
|
||||
} catch {
|
||||
setError("Failed to save workflow");
|
||||
setPhase("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 2: Execute
|
||||
setPhase("executing");
|
||||
try {
|
||||
// Strip out empty-string values so the backend applies schema defaults
|
||||
// for parameters the user left blank.
|
||||
const cleanedParams: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(parameters)) {
|
||||
if (value !== "" && value !== undefined) {
|
||||
cleanedParams[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await requestExecution.mutateAsync({
|
||||
actionRef,
|
||||
parameters: cleanedParams,
|
||||
});
|
||||
const executionId = response.data.id;
|
||||
|
||||
// Open execution in new tab and close the modal
|
||||
window.open(`/executions/${executionId}`, "_blank");
|
||||
onClose();
|
||||
} catch (err: unknown) {
|
||||
const e = err as { body?: { message?: string }; message?: string };
|
||||
const message =
|
||||
e?.body?.message || e?.message || "Failed to start execution";
|
||||
setError(message);
|
||||
setPhase("idle");
|
||||
}
|
||||
}, [paramSchema, parameters, onSave, actionRef, requestExecution, onClose]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl border border-gray-200 max-w-lg w-full max-h-[85vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-200 flex-shrink-0">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-base font-semibold text-gray-900 truncate">
|
||||
Run Workflow
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 font-mono mt-0.5 truncate">
|
||||
{label || actionRef}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
className="p-1 rounded hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors disabled:opacity-50 flex-shrink-0"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex-1 overflow-y-auto px-5 py-4">
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-1.5">
|
||||
Parameters
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
Override default values or leave as-is to use the schema defaults.
|
||||
</p>
|
||||
<ParamSchemaForm
|
||||
schema={paramSchema}
|
||||
values={parameters}
|
||||
onChange={setParameters}
|
||||
errors={paramErrors}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-2.5 px-5 py-3 border-t border-gray-200 bg-gray-50 rounded-b-lg flex-shrink-0">
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExecute}
|
||||
disabled={isSubmitting}
|
||||
className="flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium text-white bg-green-600 rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors shadow-sm"
|
||||
>
|
||||
{phase === "saving" ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Saving…
|
||||
</>
|
||||
) : phase === "executing" ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Executing…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-4 h-4" />
|
||||
Run
|
||||
<ExternalLink className="w-3 h-3 opacity-60" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user