http_request action working nicely

This commit is contained in:
2026-02-09 23:21:23 -06:00
parent e31ecb781b
commit 966a5af188
18 changed files with 720 additions and 395 deletions

View File

@@ -3,12 +3,7 @@ import { useActions, useAction, useDeleteAction } from "@/hooks/useActions";
import { useExecutions } from "@/hooks/useExecutions";
import { useState, useMemo } from "react";
import { ChevronDown, ChevronRight, Search, X, Play } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { OpenAPI } from "@/api";
import ParamSchemaForm, {
validateParamSchema,
type ParamSchema,
} from "@/components/common/ParamSchemaForm";
import ExecuteActionModal from "@/components/common/ExecuteActionModal";
import ErrorDisplay from "@/components/common/ErrorDisplay";
export default function ActionsPage() {
@@ -573,229 +568,3 @@ function ActionDetail({ actionRef }: { actionRef: string }) {
</div>
);
}
function ExecuteActionModal({
action,
onClose,
}: {
action: any;
onClose: () => void;
}) {
const queryClient = useQueryClient();
// Initialize parameters with default values from schema
const paramSchema: ParamSchema = (action.param_schema as ParamSchema) || {};
const [parameters, setParameters] = useState<Record<string, any>>({});
const [paramErrors, setParamErrors] = useState<Record<string, string>>({});
const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>(
[{ key: "", value: "" }],
);
const executeAction = useMutation({
mutationFn: async (params: {
parameters: Record<string, any>;
envVars: Array<{ key: string; value: string }>;
}) => {
// Get the token by calling the TOKEN function
const token =
typeof OpenAPI.TOKEN === "function"
? await OpenAPI.TOKEN({} as any)
: OpenAPI.TOKEN;
const response = await fetch(
`${OpenAPI.BASE}/api/v1/executions/execute`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
action_ref: action.ref,
parameters: params.parameters,
env_vars: params.envVars
.filter((ev) => ev.key.trim() !== "")
.reduce(
(acc, ev) => {
acc[ev.key] = ev.value;
return acc;
},
{} as Record<string, string>,
),
}),
},
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Failed to execute action");
}
return response.json();
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["executions"] });
onClose();
// Redirect to execution detail page
if (data?.data?.id) {
window.location.href = `/executions/${data.data.id}`;
}
},
});
const validateForm = (): boolean => {
const errors = validateParamSchema(paramSchema, parameters);
setParamErrors(errors);
return Object.keys(errors).length === 0;
};
const handleExecute = async () => {
if (!validateForm()) {
return;
}
try {
await executeAction.mutateAsync({ parameters, envVars });
} catch (err) {
console.error("Failed to execute action:", err);
}
};
const addEnvVar = () => {
setEnvVars([...envVars, { key: "", value: "" }]);
};
const removeEnvVar = (index: number) => {
if (envVars.length > 1) {
setEnvVars(envVars.filter((_, i) => i !== index));
}
};
const updateEnvVar = (
index: number,
field: "key" | "value",
value: string,
) => {
const updated = [...envVars];
updated[index][field] = value;
setEnvVars(updated);
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold">Execute Action</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
>
<X className="h-6 w-6" />
</button>
</div>
<div className="mb-4">
<p className="text-sm text-gray-600">
Action:{" "}
<span className="font-mono text-gray-900">{action.ref}</span>
</p>
{action.description && (
<p className="text-sm text-gray-600 mt-1">{action.description}</p>
)}
</div>
{executeAction.error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm">
{(executeAction.error as Error).message}
</div>
)}
<div className="mb-6">
<h4 className="text-sm font-semibold text-gray-700 mb-2">
Parameters
</h4>
<ParamSchemaForm
schema={paramSchema}
values={parameters}
onChange={setParameters}
errors={paramErrors}
/>
</div>
<div className="mb-6">
<h4 className="text-sm font-semibold text-gray-700 mb-2">
Environment Variables
</h4>
<p className="text-xs text-gray-500 mb-3">
Optional environment variables for this execution (e.g., DEBUG,
LOG_LEVEL)
</p>
<div className="space-y-2">
{envVars.map((envVar, index) => (
<div key={index} className="flex gap-2 items-start">
<input
type="text"
placeholder="Key"
value={envVar.key}
onChange={(e) => updateEnvVar(index, "key", e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
placeholder="Value"
value={envVar.value}
onChange={(e) => updateEnvVar(index, "value", e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => removeEnvVar(index)}
disabled={envVars.length === 1}
className="px-3 py-2 text-red-600 hover:text-red-700 disabled:text-gray-300 disabled:cursor-not-allowed"
title="Remove"
>
<X className="h-5 w-5" />
</button>
</div>
))}
</div>
<button
type="button"
onClick={addEnvVar}
className="mt-2 text-sm text-blue-600 hover:text-blue-700"
>
+ Add Environment Variable
</button>
</div>
<div className="flex justify-end gap-3">
<button
onClick={onClose}
disabled={executeAction.isPending}
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300 disabled:opacity-50"
>
Cancel
</button>
<button
onClick={handleExecute}
disabled={executeAction.isPending}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 flex items-center gap-2"
>
{executeAction.isPending ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
Executing...
</>
) : (
<>
<Play className="h-4 w-4" />
Execute
</>
)}
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,8 +1,12 @@
import { useParams, Link } from "react-router-dom";
import { useExecution } from "@/hooks/useExecutions";
import { useAction } from "@/hooks/useActions";
import { useExecutionStream } from "@/hooks/useExecutionStream";
import { formatDistanceToNow } from "date-fns";
import { ExecutionStatus } from "@/api";
import { useState } from "react";
import { RotateCcw } from "lucide-react";
import ExecuteActionModal from "@/components/common/ExecuteActionModal";
const getStatusColor = (status: string) => {
switch (status) {
@@ -31,6 +35,11 @@ export default function ExecutionDetailPage() {
const { data: executionData, isLoading, error } = useExecution(Number(id));
const execution = executionData?.data;
// Fetch the action so we can get param_schema for the re-run modal
const { data: actionData } = useAction(execution?.action_ref || "");
const [showRerunModal, setShowRerunModal] = useState(false);
// Subscribe to real-time updates for this execution
const { isConnected } = useExecutionStream({
executionId: Number(id),
@@ -102,6 +111,19 @@ export default function ExecutionDetailPage() {
</div>
)}
</div>
<button
onClick={() => setShowRerunModal(true)}
disabled={!actionData?.data}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
title={
!actionData?.data
? "Loading action details..."
: "Re-run this action with the same parameters"
}
>
<RotateCcw className="h-4 w-4" />
Re-Run
</button>
</div>
<p className="text-gray-600 mt-2">
<Link
@@ -113,6 +135,15 @@ export default function ExecutionDetailPage() {
</p>
</div>
{/* Re-Run Modal */}
{showRerunModal && actionData?.data && (
<ExecuteActionModal
action={actionData.data}
onClose={() => setShowRerunModal(false)}
initialParameters={execution.config}
/>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Content */}
<div className="lg:col-span-2 space-y-6">
@@ -295,6 +326,13 @@ export default function ExecutionDetailPage() {
<div className="bg-white shadow rounded-lg p-6">
<h2 className="text-lg font-semibold mb-4">Quick Actions</h2>
<div className="space-y-2">
<button
onClick={() => setShowRerunModal(true)}
disabled={!actionData?.data}
className="block w-full px-4 py-2 text-sm text-center bg-blue-50 hover:bg-blue-100 text-blue-700 rounded disabled:opacity-50 disabled:cursor-not-allowed"
>
Re-Run with Same Parameters
</button>
<Link
to={`/actions/${execution.action_ref}`}
className="block w-full px-4 py-2 text-sm text-center bg-gray-100 hover:bg-gray-200 rounded"