re-uploading work
This commit is contained in:
316
web/src/pages/executions/ExecutionDetailPage.tsx
Normal file
316
web/src/pages/executions/ExecutionDetailPage.tsx
Normal file
@@ -0,0 +1,316 @@
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import { useExecution } from "@/hooks/useExecutions";
|
||||
import { useExecutionStream } from "@/hooks/useExecutionStream";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { ExecutionStatus } from "@/api";
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "succeeded":
|
||||
return "bg-green-100 text-green-800";
|
||||
case "failed":
|
||||
return "bg-red-100 text-red-800";
|
||||
case "running":
|
||||
return "bg-blue-100 text-blue-800";
|
||||
case "pending":
|
||||
case "scheduled":
|
||||
return "bg-yellow-100 text-yellow-800";
|
||||
case "timeout":
|
||||
return "bg-orange-100 text-orange-800";
|
||||
case "canceled":
|
||||
return "bg-gray-100 text-gray-800";
|
||||
case "paused":
|
||||
return "bg-purple-100 text-purple-800";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
export default function ExecutionDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { data: executionData, isLoading, error } = useExecution(Number(id));
|
||||
const execution = executionData?.data;
|
||||
|
||||
// Subscribe to real-time updates for this execution
|
||||
const { isConnected } = useExecutionStream({
|
||||
executionId: Number(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !execution) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
||||
<p>
|
||||
Error: {error ? (error as Error).message : "Execution not found"}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/executions"
|
||||
className="mt-4 inline-block text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
← Back to Executions
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isRunning =
|
||||
execution.status === ExecutionStatus.RUNNING ||
|
||||
execution.status === ExecutionStatus.SCHEDULING ||
|
||||
execution.status === ExecutionStatus.SCHEDULED ||
|
||||
execution.status === ExecutionStatus.REQUESTED;
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
to="/executions"
|
||||
className="text-blue-600 hover:text-blue-800 mb-2 inline-block"
|
||||
>
|
||||
← Back to Executions
|
||||
</Link>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-3xl font-bold">Execution #{execution.id}</h1>
|
||||
<span
|
||||
className={`px-3 py-1 text-sm rounded-full ${getStatusColor(execution.status)}`}
|
||||
>
|
||||
{execution.status}
|
||||
</span>
|
||||
{isRunning && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600" />
|
||||
<span>In Progress</span>
|
||||
</div>
|
||||
)}
|
||||
{isConnected && (
|
||||
<div className="flex items-center gap-2 text-xs text-green-600">
|
||||
<div className="h-2 w-2 rounded-full bg-green-600 animate-pulse" />
|
||||
<span>Live</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 mt-2">
|
||||
<Link
|
||||
to={`/actions/${execution.action_ref}`}
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{execution.action_ref}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Main Content */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Status & Timing */}
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Execution Details</h2>
|
||||
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Status</dt>
|
||||
<dd className="mt-1">
|
||||
<span
|
||||
className={`px-2 py-1 text-xs rounded ${getStatusColor(execution.status)}`}
|
||||
>
|
||||
{execution.status}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Created</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">
|
||||
{new Date(execution.created).toLocaleString()}
|
||||
<span className="text-gray-500 ml-2 text-xs">
|
||||
(
|
||||
{formatDistanceToNow(new Date(execution.created), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
)
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Updated</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">
|
||||
{new Date(execution.updated).toLocaleString()}
|
||||
</dd>
|
||||
</div>
|
||||
{execution.enforcement && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">
|
||||
Enforcement ID
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">
|
||||
{execution.enforcement}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{execution.parent && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">
|
||||
Parent Execution
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">
|
||||
<Link
|
||||
to={`/executions/${execution.parent}`}
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
#{execution.parent}
|
||||
</Link>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{execution.executor && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">
|
||||
Executor ID
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">
|
||||
{execution.executor}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{/* Config/Parameters */}
|
||||
{execution.config && Object.keys(execution.config).length > 0 && (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Configuration</h2>
|
||||
<pre className="bg-gray-50 p-4 rounded text-sm overflow-x-auto">
|
||||
{JSON.stringify(execution.config, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result */}
|
||||
{execution.result && Object.keys(execution.result).length > 0 && (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Result</h2>
|
||||
<pre className="bg-gray-50 p-4 rounded text-sm overflow-x-auto">
|
||||
{JSON.stringify(execution.result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timeline */}
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Timeline</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500" />
|
||||
{!isRunning && <div className="w-0.5 h-full bg-gray-300" />}
|
||||
</div>
|
||||
<div className="flex-1 pb-4">
|
||||
<p className="font-medium">Execution Created</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{new Date(execution.created).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{execution.status === ExecutionStatus.COMPLETED && (
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-3 h-3 rounded-full bg-green-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">Execution Completed</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{new Date(execution.updated).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{execution.status === ExecutionStatus.FAILED && (
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">Execution Failed</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{new Date(execution.updated).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isRunning && (
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500 animate-pulse" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-blue-600">In Progress...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Quick Info */}
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Quick Info</h2>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Action</p>
|
||||
<Link
|
||||
to={`/actions/${execution.action_ref}`}
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{execution.action_ref}
|
||||
</Link>
|
||||
</div>
|
||||
{execution.enforcement && (
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Enforcement ID</p>
|
||||
<p className="text-sm font-medium">{execution.enforcement}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<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">
|
||||
<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"
|
||||
>
|
||||
View Action
|
||||
</Link>
|
||||
<Link
|
||||
to={`/executions?action_ref=${execution.action_ref}`}
|
||||
className="block w-full px-4 py-2 text-sm text-center bg-gray-100 hover:bg-gray-200 rounded"
|
||||
>
|
||||
View All Executions
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
414
web/src/pages/executions/ExecutionsPage.tsx
Normal file
414
web/src/pages/executions/ExecutionsPage.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useExecutions } from "@/hooks/useExecutions";
|
||||
import { useExecutionStream } from "@/hooks/useExecutionStream";
|
||||
import { ExecutionStatus } from "@/api";
|
||||
import { useState, useMemo, memo, useCallback, useEffect } from "react";
|
||||
import { Search, X } from "lucide-react";
|
||||
import MultiSelect from "@/components/common/MultiSelect";
|
||||
|
||||
// Memoized filter input component to prevent re-render on WebSocket updates
|
||||
const FilterInput = memo(
|
||||
({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
}) => (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
FilterInput.displayName = "FilterInput";
|
||||
|
||||
// Status options moved outside component to prevent recreation
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: ExecutionStatus.REQUESTED, label: "Requested" },
|
||||
{ value: ExecutionStatus.SCHEDULING, label: "Scheduling" },
|
||||
{ value: ExecutionStatus.SCHEDULED, label: "Scheduled" },
|
||||
{ value: ExecutionStatus.RUNNING, label: "Running" },
|
||||
{ value: ExecutionStatus.COMPLETED, label: "Completed" },
|
||||
{ value: ExecutionStatus.FAILED, label: "Failed" },
|
||||
{ value: ExecutionStatus.CANCELING, label: "Canceling" },
|
||||
{ value: ExecutionStatus.CANCELLED, label: "Cancelled" },
|
||||
{ value: ExecutionStatus.TIMEOUT, label: "Timeout" },
|
||||
{ value: ExecutionStatus.ABANDONED, label: "Abandoned" },
|
||||
];
|
||||
|
||||
export default function ExecutionsPage() {
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 50;
|
||||
const [searchFilters, setSearchFilters] = useState({
|
||||
pack: "",
|
||||
rule: "",
|
||||
action: "",
|
||||
trigger: "",
|
||||
executor: "",
|
||||
});
|
||||
const [selectedStatuses, setSelectedStatuses] = useState<string[]>([]);
|
||||
|
||||
// Debounced filter state for API calls
|
||||
const [debouncedFilters, setDebouncedFilters] = useState(searchFilters);
|
||||
const [debouncedStatuses, setDebouncedStatuses] = useState(selectedStatuses);
|
||||
|
||||
// Debounce filter changes (500ms delay)
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedFilters(searchFilters);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchFilters]);
|
||||
|
||||
// Debounce status changes (300ms delay - shorter since it's a selection)
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedStatuses(selectedStatuses);
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [selectedStatuses]);
|
||||
|
||||
const queryParams = useMemo(() => {
|
||||
const params: any = { page, pageSize };
|
||||
if (debouncedFilters.pack) params.packName = debouncedFilters.pack;
|
||||
if (debouncedFilters.rule) params.ruleRef = debouncedFilters.rule;
|
||||
if (debouncedFilters.action) params.actionRef = debouncedFilters.action;
|
||||
if (debouncedFilters.trigger) params.triggerRef = debouncedFilters.trigger;
|
||||
if (debouncedFilters.executor)
|
||||
params.executor = parseInt(debouncedFilters.executor, 10);
|
||||
|
||||
// Include status filter if exactly one status is selected
|
||||
// API only supports single status, so we use the first one for filtering
|
||||
// and show all results if multiple are selected
|
||||
if (debouncedStatuses.length === 1) {
|
||||
params.status = debouncedStatuses[0] as ExecutionStatus;
|
||||
}
|
||||
|
||||
return params;
|
||||
}, [page, pageSize, debouncedFilters, debouncedStatuses]);
|
||||
|
||||
const { data, isLoading, error } = useExecutions(queryParams);
|
||||
|
||||
// Subscribe to real-time updates for all executions
|
||||
const { isConnected } = useExecutionStream({ enabled: true });
|
||||
|
||||
const executions = data?.data || [];
|
||||
const total = data?.pagination?.total_items || 0;
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
// Client-side filtering for multiple status selection (when > 1 selected)
|
||||
const filteredExecutions = useMemo(() => {
|
||||
// If no statuses selected or only one (already filtered by API), show all
|
||||
if (debouncedStatuses.length <= 1) {
|
||||
return executions;
|
||||
}
|
||||
// If multiple statuses selected, filter client-side
|
||||
return executions.filter((exec: any) =>
|
||||
debouncedStatuses.includes(exec.status),
|
||||
);
|
||||
}, [executions, debouncedStatuses]);
|
||||
|
||||
const handleFilterChange = useCallback((field: string, value: string) => {
|
||||
setSearchFilters((prev) => ({ ...prev, [field]: value }));
|
||||
setPage(1); // Reset to first page on filter change
|
||||
}, []);
|
||||
|
||||
const clearFilters = useCallback(() => {
|
||||
setSearchFilters({
|
||||
pack: "",
|
||||
rule: "",
|
||||
action: "",
|
||||
trigger: "",
|
||||
executor: "",
|
||||
});
|
||||
setSelectedStatuses([]);
|
||||
setPage(1); // Reset to first page
|
||||
}, []);
|
||||
|
||||
const hasActiveFilters =
|
||||
Object.values(searchFilters).some((v) => v !== "") ||
|
||||
selectedStatuses.length > 0;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
||||
<p>Error: {(error as Error).message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStatusColor = (status: ExecutionStatus) => {
|
||||
switch (status) {
|
||||
case ExecutionStatus.COMPLETED:
|
||||
return "bg-green-100 text-green-800";
|
||||
case ExecutionStatus.FAILED:
|
||||
case ExecutionStatus.TIMEOUT:
|
||||
return "bg-red-100 text-red-800";
|
||||
case ExecutionStatus.RUNNING:
|
||||
return "bg-blue-100 text-blue-800";
|
||||
case ExecutionStatus.SCHEDULED:
|
||||
case ExecutionStatus.SCHEDULING:
|
||||
case ExecutionStatus.REQUESTED:
|
||||
return "bg-yellow-100 text-yellow-800";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Executions</h1>
|
||||
{isLoading && hasActiveFilters && (
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Searching executions...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isConnected && (
|
||||
<div className="flex items-center gap-2 text-sm text-green-600">
|
||||
<div className="h-2 w-2 rounded-full bg-green-600 animate-pulse" />
|
||||
<span>Live Updates</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Filters */}
|
||||
<div className="bg-white shadow rounded-lg p-4 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Search className="h-5 w-5 text-gray-400" />
|
||||
<h2 className="text-lg font-semibold">Filter Executions</h2>
|
||||
</div>
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
Clear Filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
|
||||
<FilterInput
|
||||
label="Pack"
|
||||
value={searchFilters.pack}
|
||||
onChange={(value) => handleFilterChange("pack", value)}
|
||||
placeholder="e.g., core"
|
||||
/>
|
||||
<FilterInput
|
||||
label="Rule"
|
||||
value={searchFilters.rule}
|
||||
onChange={(value) => handleFilterChange("rule", value)}
|
||||
placeholder="e.g., core.on_timer"
|
||||
/>
|
||||
<FilterInput
|
||||
label="Action"
|
||||
value={searchFilters.action}
|
||||
onChange={(value) => handleFilterChange("action", value)}
|
||||
placeholder="e.g., core.echo"
|
||||
/>
|
||||
<FilterInput
|
||||
label="Trigger"
|
||||
value={searchFilters.trigger}
|
||||
onChange={(value) => handleFilterChange("trigger", value)}
|
||||
placeholder="e.g., core.timer"
|
||||
/>
|
||||
<FilterInput
|
||||
label="Executor ID"
|
||||
value={searchFilters.executor}
|
||||
onChange={(value) => handleFilterChange("executor", value)}
|
||||
placeholder="e.g., 1"
|
||||
/>
|
||||
<div>
|
||||
<MultiSelect
|
||||
label="Status"
|
||||
options={STATUS_OPTIONS}
|
||||
value={selectedStatuses}
|
||||
onChange={setSelectedStatuses}
|
||||
placeholder="All Statuses"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredExecutions.length === 0 ? (
|
||||
<div className="bg-white p-12 text-center rounded-lg shadow">
|
||||
<p>
|
||||
{executions.length === 0
|
||||
? "No executions found"
|
||||
: "No executions match the selected filters"}
|
||||
</p>
|
||||
{executions.length > 0 && hasActiveFilters && (
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="mt-3 text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="bg-white shadow rounded-lg overflow-hidden">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
ID
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Action
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Rule
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Trigger
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Created
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredExecutions.map((exec: any) => (
|
||||
<tr key={exec.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 font-mono text-sm">
|
||||
<Link
|
||||
to={`/executions/${exec.id}`}
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
#{exec.id}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="text-sm text-gray-900">
|
||||
{exec.action_ref}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{exec.rule_ref ? (
|
||||
<span className="text-sm text-gray-700">
|
||||
{exec.rule_ref}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 italic">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{exec.trigger_ref ? (
|
||||
<span className="text-sm text-gray-700">
|
||||
{exec.trigger_ref}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 italic">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span
|
||||
className={`px-2 py-1 text-xs rounded ${getStatusColor(exec.status)}`}
|
||||
>
|
||||
{exec.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500">
|
||||
{new Date(exec.created).toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{totalPages > 1 && (
|
||||
<div className="bg-gray-50 px-6 py-4 flex items-center justify-between border-t border-gray-200">
|
||||
<div className="flex-1 flex justify-between sm:hidden">
|
||||
<button
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={page === 1}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={page === totalPages}
|
||||
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-700">
|
||||
Showing{" "}
|
||||
<span className="font-medium">
|
||||
{(page - 1) * pageSize + 1}
|
||||
</span>{" "}
|
||||
to
|
||||
<span className="font-medium">
|
||||
{Math.min(page * pageSize, total)}
|
||||
</span>{" "}
|
||||
of
|
||||
<span className="font-medium">{total}</span> executions
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
|
||||
<button
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={page === 1}
|
||||
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={page === totalPages}
|
||||
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user