proper sql filtering
This commit is contained in:
@@ -4,6 +4,7 @@ import { useQueries } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Save,
|
||||
Play,
|
||||
AlertTriangle,
|
||||
FileCode,
|
||||
Code,
|
||||
@@ -11,6 +12,9 @@ import {
|
||||
X,
|
||||
Zap,
|
||||
Settings2,
|
||||
ExternalLink,
|
||||
Copy,
|
||||
Check,
|
||||
} from "lucide-react";
|
||||
import SearchableSelect from "@/components/common/SearchableSelect";
|
||||
import yaml from "js-yaml";
|
||||
@@ -23,6 +27,9 @@ import TaskInspector from "@/components/workflows/TaskInspector";
|
||||
import { useActions } from "@/hooks/useActions";
|
||||
import { ActionsService } from "@/api";
|
||||
import { usePacks } from "@/hooks/usePacks";
|
||||
import { useRequestExecution } from "@/hooks/useExecutions";
|
||||
import RunWorkflowModal from "@/components/workflows/RunWorkflowModal";
|
||||
import type { ParamSchema } from "@/components/common/ParamSchemaForm";
|
||||
import { useWorkflow } from "@/hooks/useWorkflows";
|
||||
import {
|
||||
useSaveWorkflowFile,
|
||||
@@ -77,6 +84,7 @@ export default function WorkflowBuilderPage() {
|
||||
// Mutations
|
||||
const saveWorkflowFile = useSaveWorkflowFile();
|
||||
const updateWorkflowFile = useUpdateWorkflowFile();
|
||||
const requestExecution = useRequestExecution();
|
||||
|
||||
// Builder state
|
||||
const [state, setState] = useState<WorkflowBuilderState>(INITIAL_STATE);
|
||||
@@ -85,6 +93,9 @@ export default function WorkflowBuilderPage() {
|
||||
const [showErrors, setShowErrors] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||
const [runError, setRunError] = useState<string | null>(null);
|
||||
const [showRunModal, setShowRunModal] = useState(false);
|
||||
const [yamlCopied, setYamlCopied] = useState(false);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [showYamlPreview, setShowYamlPreview] = useState(false);
|
||||
const [sidebarTab, setSidebarTab] = useState<"actions" | "inputs">("actions");
|
||||
@@ -428,7 +439,7 @@ export default function WorkflowBuilderPage() {
|
||||
|
||||
if (errors.length > 0) {
|
||||
setShowErrors(true);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const definition = builderStateToDefinition(state, actionSchemaMap);
|
||||
@@ -495,16 +506,19 @@ export default function WorkflowBuilderPage() {
|
||||
if (!isEditing) {
|
||||
const newRef = `${state.packRef}.${state.name}`;
|
||||
navigate(`/actions/workflows/${newRef}/edit`, { replace: true });
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
setSaveSuccess(true);
|
||||
setTimeout(() => setSaveSuccess(false), 3000);
|
||||
|
||||
return true; // indicate success
|
||||
} catch (err: unknown) {
|
||||
const error = err as { body?: { message?: string }; message?: string };
|
||||
const message =
|
||||
error?.body?.message || error?.message || "Failed to save workflow";
|
||||
setSaveError(message);
|
||||
return false; // indicate failure
|
||||
}
|
||||
}, [
|
||||
state,
|
||||
@@ -516,6 +530,49 @@ export default function WorkflowBuilderPage() {
|
||||
navigate,
|
||||
]);
|
||||
|
||||
// Check whether the workflow has any parameters defined
|
||||
const hasParameters = useMemo(
|
||||
() => Object.keys(state.parameters).length > 0,
|
||||
[state.parameters],
|
||||
);
|
||||
|
||||
const handleRun = useCallback(async () => {
|
||||
setRunError(null);
|
||||
|
||||
if (hasParameters) {
|
||||
// Open the modal so the user can review / override parameter values
|
||||
setShowRunModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// No parameters — save and execute immediately
|
||||
const saved = await doSave();
|
||||
if (!saved) return; // save failed — error already shown
|
||||
|
||||
const actionRef = editRef || `${state.packRef}.${state.name}`;
|
||||
|
||||
try {
|
||||
const response = await requestExecution.mutateAsync({
|
||||
actionRef,
|
||||
parameters: {},
|
||||
});
|
||||
const executionId = response.data.id;
|
||||
window.open(`/executions/${executionId}`, "_blank");
|
||||
} catch (err: unknown) {
|
||||
const error = err as { body?: { message?: string }; message?: string };
|
||||
const message =
|
||||
error?.body?.message || error?.message || "Failed to start execution";
|
||||
setRunError(message);
|
||||
}
|
||||
}, [
|
||||
hasParameters,
|
||||
doSave,
|
||||
editRef,
|
||||
state.packRef,
|
||||
state.name,
|
||||
requestExecution,
|
||||
]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
// If there's a start-node problem, show the toast immediately and
|
||||
// require confirmation before saving
|
||||
@@ -547,6 +604,7 @@ export default function WorkflowBuilderPage() {
|
||||
}, [state, showYamlPreview, actionSchemaMap]);
|
||||
|
||||
const isSaving = saveWorkflowFile.isPending || updateWorkflowFile.isPending;
|
||||
const isExecuting = requestExecution.isPending;
|
||||
|
||||
if (isEditing && workflowLoading) {
|
||||
return (
|
||||
@@ -684,6 +742,16 @@ export default function WorkflowBuilderPage() {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Run error indicator */}
|
||||
{runError && (
|
||||
<span
|
||||
className="text-xs text-red-600 font-medium max-w-[200px] truncate"
|
||||
title={runError}
|
||||
>
|
||||
✗ {runError}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Save button */}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
@@ -693,6 +761,31 @@ export default function WorkflowBuilderPage() {
|
||||
<Save className="w-4 h-4" />
|
||||
{isSaving ? "Saving..." : isEditing ? "Update" : "Save"}
|
||||
</button>
|
||||
|
||||
{/* Run button */}
|
||||
<button
|
||||
onClick={handleRun}
|
||||
disabled={!isEditing || isSaving || isExecuting}
|
||||
title={
|
||||
!isEditing
|
||||
? "Save the workflow first to enable execution"
|
||||
: "Save & run this workflow"
|
||||
}
|
||||
className="flex items-center gap-1.5 px-4 py-1.5 bg-green-600 text-white text-sm font-medium rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors shadow-sm"
|
||||
>
|
||||
{isExecuting ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Running...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-4 h-4" />
|
||||
Run
|
||||
<ExternalLink className="w-3 h-3 opacity-60" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -771,6 +864,30 @@ export default function WorkflowBuilderPage() {
|
||||
<span className="text-[10px] text-gray-500 ml-1">
|
||||
(read-only preview of the generated YAML)
|
||||
</span>
|
||||
<div className="ml-auto">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(yamlPreview).then(() => {
|
||||
setYamlCopied(true);
|
||||
setTimeout(() => setYamlCopied(false), 2000);
|
||||
});
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded transition-colors text-gray-400 hover:text-gray-200 hover:bg-gray-700"
|
||||
title="Copy YAML to clipboard"
|
||||
>
|
||||
{yamlCopied ? (
|
||||
<>
|
||||
<Check className="w-3.5 h-3.5 text-green-400" />
|
||||
<span className="text-green-400">Copied</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre className="flex-1 overflow-auto p-6 text-sm font-mono text-green-400 whitespace-pre leading-relaxed">
|
||||
{yamlPreview}
|
||||
@@ -965,6 +1082,17 @@ export default function WorkflowBuilderPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Run workflow modal (shown when workflow has parameters) */}
|
||||
{showRunModal && (
|
||||
<RunWorkflowModal
|
||||
actionRef={editRef || `${state.packRef}.${state.name}`}
|
||||
paramSchema={state.parameters as unknown as ParamSchema}
|
||||
label={state.label || undefined}
|
||||
onSave={doSave}
|
||||
onClose={() => setShowRunModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Inline style for fade-in animation */}
|
||||
<style>{`
|
||||
@keyframes fadeInDown {
|
||||
|
||||
@@ -194,6 +194,7 @@ const ExecutionsResultsTable = memo(
|
||||
{executions.map((exec: any) => (
|
||||
<tr
|
||||
key={exec.id}
|
||||
data-execution-id={exec.id}
|
||||
className={`hover:bg-gray-50 cursor-pointer ${
|
||||
selectedExecutionId === exec.id
|
||||
? "bg-blue-50 hover:bg-blue-50"
|
||||
@@ -472,6 +473,66 @@ export default function ExecutionsPage() {
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
// --- Keyboard arrow-key navigation for execution list ---
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
|
||||
|
||||
// Don't interfere with inputs, selects, textareas
|
||||
const tag = (e.target as HTMLElement)?.tagName;
|
||||
if (tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA") return;
|
||||
|
||||
const list = filteredExecutions;
|
||||
if (!list || list.length === 0) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
setSelectedExecutionId((prevId) => {
|
||||
if (prevId == null) {
|
||||
// Nothing selected — pick first or last depending on direction
|
||||
const nextId =
|
||||
e.key === "ArrowDown" ? list[0].id : list[list.length - 1].id;
|
||||
requestAnimationFrame(() => {
|
||||
document
|
||||
.querySelector(`[data-execution-id="${nextId}"]`)
|
||||
?.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||
});
|
||||
return nextId;
|
||||
}
|
||||
|
||||
const currentIndex = list.findIndex((ex: any) => ex.id === prevId);
|
||||
if (currentIndex === -1) {
|
||||
const nextId = list[0].id;
|
||||
requestAnimationFrame(() => {
|
||||
document
|
||||
.querySelector(`[data-execution-id="${nextId}"]`)
|
||||
?.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||
});
|
||||
return nextId;
|
||||
}
|
||||
|
||||
let nextIndex: number;
|
||||
if (e.key === "ArrowDown") {
|
||||
nextIndex =
|
||||
currentIndex < list.length - 1 ? currentIndex + 1 : currentIndex;
|
||||
} else {
|
||||
nextIndex = currentIndex > 0 ? currentIndex - 1 : currentIndex;
|
||||
}
|
||||
|
||||
const nextId = list[nextIndex].id;
|
||||
requestAnimationFrame(() => {
|
||||
document
|
||||
.querySelector(`[data-execution-id="${nextId}"]`)
|
||||
?.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||
});
|
||||
return nextId;
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [filteredExecutions]);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)]">
|
||||
{/* Main content area */}
|
||||
|
||||
Reference in New Issue
Block a user