diff --git a/web/app/pipelines/[pipelineId]/page.tsx b/web/app/pipelines/[pipelineId]/page.tsx
index e7867cb..fdb03e0 100644
--- a/web/app/pipelines/[pipelineId]/page.tsx
+++ b/web/app/pipelines/[pipelineId]/page.tsx
@@ -1,85 +1,268 @@
'use client';
import { useState, useEffect } from 'react';
-import { useParams } from 'next/navigation';
+import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
-import { AlertCircle, ArrowLeft, History, Settings } from 'lucide-react';
-import type { DashboardConfig, PipelineDetail } from '@/lib/pipeline-types';
-import { getPipeline, getDashboardConfig } from '@/lib/pipeline-api';
-import { DynamicDashboard } from '@/components/dashboard';
+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 dashboard page.
- *
- * Displays the dynamic dashboard for a specific pipeline.
+ * Pipeline detail page - shows executions like Jobs page.
*/
-export default function PipelineDashboardPage() {
+export default function PipelinePage() {
const params = useParams();
+ const router = useRouter();
const pipelineId = params.pipelineId as string;
const [pipeline, setPipeline] = useState(null);
- const [dashboardConfig, setDashboardConfig] = 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(() => {
- const fetchData = async () => {
- setLoading(true);
- setError(null);
-
- try {
- const [pipelineData, configData] = await Promise.all([
- getPipeline(pipelineId),
- getDashboardConfig(pipelineId),
- ]);
- setPipeline(pipelineData);
- setDashboardConfig(configData);
- } catch (e) {
- setError(e instanceof Error ? e.message : 'Failed to load pipeline');
- } finally {
- setLoading(false);
- }
- };
-
if (pipelineId) {
fetchData();
}
}, [pipelineId]);
- if (loading) {
- return (
-
- {/* Header skeleton */}
-
+ const handleExecutionClick = (execution: ExecutionStatus) => {
+ router.push(`/pipelines/${pipelineId}/executions/${execution.id}`);
+ };
- {/* Dashboard skeleton */}
-
-
- {[1, 2, 3, 4].map((i) => (
-
+ 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 || !dashboardConfig) {
+ if (error && !pipeline) {
return (
-
+
Back to Pipelines
@@ -87,82 +270,158 @@ export default function PipelineDashboardPage() {
-
- {error || 'Pipeline not found'}
-
-
{error}
+
);
}
return (
-
+
{/* Navigation */}
Back to Pipelines
-
-
- Execution History
-
+
+ Refresh
+
+
- {/* Pipeline Info */}
-
-
-
-
- {pipeline.name}
-
-
{pipeline.description}
-
-
-
- Version: {pipeline.version}
-
-
- Input: {pipeline.input_type}
-
-
-
-
-
-
Stages:
-
- {pipeline.stages.map((stage, index) => (
-
-
- {index + 1}
-
- {stage}
+ {/* 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
- {/* Dynamic Dashboard */}
-
+ {/* 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)}
+ />
+ ))}
+
+ )}
);
}