'use client';
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import {
ArrowLeft,
CheckCircle,
XCircle,
Clock,
PlayCircle,
Loader,
RefreshCw,
AlertCircle,
Play,
ChevronRight,
Layers,
LayoutList,
LayoutGrid,
} from 'lucide-react';
import type { ExecutionStatus, PipelineDetail } from '@/lib/pipeline-types';
import { getPipeline, listExecutions } from '@/lib/pipeline-api';
import ExecutionsView from '@/components/ExecutionsView';
// Status badge component
function StatusBadge({ status }: { status: ExecutionStatus['status'] }) {
const config = {
pending: {
icon: Clock,
color: 'bg-gray-100 text-gray-600',
},
running: {
icon: Loader,
color: 'bg-blue-100 text-blue-600',
},
completed: {
icon: CheckCircle,
color: 'bg-green-100 text-green-600',
},
failed: {
icon: XCircle,
color: 'bg-red-100 text-red-600',
},
cancelled: {
icon: AlertCircle,
color: 'bg-yellow-100 text-yellow-600',
},
};
const { icon: Icon, color } = config[status] || config.pending;
return (
{status.charAt(0).toUpperCase() + status.slice(1)}
);
}
// Format date string
function formatDate(dateStr: string | undefined): string {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
// Calculate duration
function formatDuration(start?: string, end?: string): string {
if (!start) return '-';
const startDate = new Date(start);
const endDate = end ? new Date(end) : new Date();
const ms = endDate.getTime() - startDate.getTime();
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
return `${(ms / 60000).toFixed(1)}m`;
}
// Execution card component
function ExecutionCard({
execution,
pipelineId,
onClick
}: {
execution: ExecutionStatus;
pipelineId: string;
onClick: () => void;
}) {
return (
{formatDate(execution.started_at)}
{execution.job_id ? (
Job:
e.stopPropagation()}
className="text-blue-600 hover:underline font-medium"
>
{execution.job_id.slice(0, 8)}...
) : execution.business_id ? (
Business:
{execution.business_id}
) : (
Manual execution
)}
{execution.stages_completed.length} / {execution.stages_requested.length} stages
{formatDuration(execution.started_at, execution.completed_at)}
{execution.error_message && (
{execution.error_message}
)}
{execution.id.slice(0, 12)}...
);
}
// View mode toggle component
function ViewToggle({
mode,
onChange
}: {
mode: 'table' | 'cards';
onChange: (mode: 'table' | 'cards') => void;
}) {
return (
);
}
/**
* Pipeline detail page - shows executions like Jobs page.
*/
export default function PipelinePage() {
const params = useParams();
const router = useRouter();
const pipelineId = params.pipelineId as string;
const [pipeline, setPipeline] = useState(null);
const [executions, setExecutions] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const [pipelineData, executionsData] = await Promise.all([
getPipeline(pipelineId),
listExecutions(pipelineId, { limit: 100 }),
]);
setPipeline(pipelineData);
setExecutions(executionsData);
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to load data');
} finally {
setLoading(false);
}
};
useEffect(() => {
if (pipelineId) {
fetchData();
}
}, [pipelineId]);
const handleExecutionClick = (execution: ExecutionStatus) => {
router.push(`/pipelines/${pipelineId}/executions/${execution.id}`);
};
const handleRunPipeline = () => {
router.push(`/pipelines/${pipelineId}/run`);
};
// Count executions by status
const statusCounts = {
total: executions.length,
completed: executions.filter(e => e.status === 'completed').length,
failed: executions.filter(e => e.status === 'failed').length,
running: executions.filter(e => e.status === 'running').length,
};
if (loading && !pipeline) {
return (
{[1, 2, 3].map((i) => (
))}
);
}
if (error && !pipeline) {
return (
);
}
return (
{/* Navigation */}
{/* Pipeline Header */}
{pipeline && (
{pipeline.name}
{pipeline.description}
{pipeline.is_enabled ? 'Enabled' : 'Disabled'}
v{pipeline.version}
Stages:
{pipeline.stages.map((stage, index) => (
{index + 1}. {stage}
))}
)}
{/* Stats Row */}
{statusCounts.total}
Total Runs
{statusCounts.completed}
Completed
{statusCounts.failed}
Failed
{statusCounts.running}
Running
{/* Executions Header with View Toggle */}
Executions
{/* Executions Content */}
{loading ? (
) : executions.length === 0 ? (
No Executions Yet
Run your first pipeline execution to see results here
) : viewMode === 'table' ? (
) : (
{executions.map((execution) => (
handleExecutionClick(execution)}
/>
))}
)}
);
}