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)} + /> + ))} +
+ )}
); }