This commit implements a plugin-like pipeline architecture with:
Pipeline Core Package (packages/pipeline-core/):
- BasePipeline abstract class all pipelines implement
- PipelineRegistry for database-backed discovery/management
- PipelineRunner for execution with status tracking
- DashboardConfig contracts for dynamic widget definitions
Database Migration (006_pipeline_registry.sql):
- pipeline.registry table for registered pipelines
- pipeline.executions table for execution history
- Views for execution stats and monitoring
ReviewIQ Pipeline Refactor:
- Implements BasePipeline interface
- Adds get_dashboard_config() with widget definitions
- Adds get_widget_data() methods for all dashboard widgets
- Maintains backward compatibility with Pipeline alias
Generic Pipeline API (api/routes/pipelines.py):
- GET /api/pipelines - List all registered pipelines
- GET /api/pipelines/{id} - Pipeline details
- POST /api/pipelines/{id}/execute - Execute pipeline
- GET /api/pipelines/{id}/dashboard - Dashboard config
- GET /api/pipelines/{id}/widgets/{w} - Widget data
- GET /api/pipelines/{id}/executions - Execution history
Frontend Dynamic Dashboard System:
- DynamicDashboard component renders from config
- WidgetRegistry maps types to components
- Widget components: StatCard, LineChart, BarChart,
PieChart, DataTable, Heatmap
- Pipeline API client library
Frontend Pipeline Pages:
- /pipelines - List all registered pipelines
- /pipelines/[id] - Dynamic dashboard for pipeline
- /pipelines/[id]/executions - Execution history
- Pipelines nav item in Sidebar
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
169 lines
5.7 KiB
TypeScript
169 lines
5.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useParams } 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';
|
|
|
|
/**
|
|
* Pipeline dashboard page.
|
|
*
|
|
* Displays the dynamic dashboard for a specific pipeline.
|
|
*/
|
|
export default function PipelineDashboardPage() {
|
|
const params = useParams();
|
|
const pipelineId = params.pipelineId as string;
|
|
|
|
const [pipeline, setPipeline] = useState<PipelineDetail | null>(null);
|
|
const [dashboardConfig, setDashboardConfig] = useState<DashboardConfig | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
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 (
|
|
<div className="p-6">
|
|
{/* Header skeleton */}
|
|
<div className="flex items-center mb-6 animate-pulse">
|
|
<div className="w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
<div className="ml-4 flex-1">
|
|
<div className="h-6 w-48 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
<div className="h-4 w-64 bg-gray-200 dark:bg-gray-700 rounded mt-2" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dashboard skeleton */}
|
|
<div className="space-y-6">
|
|
<div className="grid grid-cols-4 gap-4">
|
|
{[1, 2, 3, 4].map((i) => (
|
|
<div
|
|
key={i}
|
|
className="h-24 bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse"
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className="h-64 bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !pipeline || !dashboardConfig) {
|
|
return (
|
|
<div className="p-6">
|
|
<Link
|
|
href="/pipelines"
|
|
className="inline-flex items-center text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 mb-6"
|
|
>
|
|
<ArrowLeft className="w-4 h-4 mr-1" />
|
|
Back to Pipelines
|
|
</Link>
|
|
|
|
<div className="text-center py-12">
|
|
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
|
|
<p className="text-red-600 dark:text-red-400">
|
|
{error || 'Pipeline not found'}
|
|
</p>
|
|
<Link
|
|
href="/pipelines"
|
|
className="mt-4 inline-block px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
>
|
|
Back to Pipelines
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-6">
|
|
{/* Navigation */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<Link
|
|
href="/pipelines"
|
|
className="inline-flex items-center text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
|
>
|
|
<ArrowLeft className="w-4 h-4 mr-1" />
|
|
Back to Pipelines
|
|
</Link>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
<Link
|
|
href={`/pipelines/${pipelineId}/executions`}
|
|
className="inline-flex items-center px-3 py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md"
|
|
>
|
|
<History className="w-4 h-4 mr-2" />
|
|
Execution History
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Pipeline Info */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 mb-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
{pipeline.name}
|
|
</h2>
|
|
<p className="text-sm text-gray-500 mt-1">{pipeline.description}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-sm text-gray-500">
|
|
Version: <span className="font-medium">{pipeline.version}</span>
|
|
</p>
|
|
<p className="text-sm text-gray-500">
|
|
Input: <code className="text-xs bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded">{pipeline.input_type}</code>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
|
|
<p className="text-sm text-gray-500 mb-2">Stages:</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{pipeline.stages.map((stage, index) => (
|
|
<span
|
|
key={stage}
|
|
className="inline-flex items-center px-2 py-1 bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm rounded"
|
|
>
|
|
<span className="w-5 h-5 flex items-center justify-center bg-blue-100 dark:bg-blue-800 rounded-full text-xs font-medium mr-2">
|
|
{index + 1}
|
|
</span>
|
|
{stage}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dynamic Dashboard */}
|
|
<DynamicDashboard pipelineId={pipelineId} config={dashboardConfig} />
|
|
</div>
|
|
);
|
|
}
|