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>
114 lines
3.5 KiB
TypeScript
114 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
TrendingUp,
|
|
TrendingDown,
|
|
MessageSquare,
|
|
CheckCircle,
|
|
AlertTriangle,
|
|
Star,
|
|
Activity,
|
|
} from 'lucide-react';
|
|
import type { WidgetConfig, StatCardData, StatCardConfig } from '@/lib/pipeline-types';
|
|
import { WidgetWrapper } from './WidgetWrapper';
|
|
|
|
interface StatCardProps {
|
|
config: WidgetConfig;
|
|
data: StatCardData | null;
|
|
loading: boolean;
|
|
error?: string;
|
|
onRefresh?: () => void;
|
|
}
|
|
|
|
// Icon mapping
|
|
const ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
'message-square': MessageSquare,
|
|
'check-circle': CheckCircle,
|
|
'alert-triangle': AlertTriangle,
|
|
star: Star,
|
|
activity: Activity,
|
|
};
|
|
|
|
// Color mapping
|
|
const COLORS: Record<string, string> = {
|
|
blue: 'text-blue-600 bg-blue-100 dark:text-blue-400 dark:bg-blue-900/30',
|
|
green: 'text-green-600 bg-green-100 dark:text-green-400 dark:bg-green-900/30',
|
|
red: 'text-red-600 bg-red-100 dark:text-red-400 dark:bg-red-900/30',
|
|
yellow: 'text-yellow-600 bg-yellow-100 dark:text-yellow-400 dark:bg-yellow-900/30',
|
|
purple: 'text-purple-600 bg-purple-100 dark:text-purple-400 dark:bg-purple-900/30',
|
|
gray: 'text-gray-600 bg-gray-100 dark:text-gray-400 dark:bg-gray-700',
|
|
};
|
|
|
|
/**
|
|
* Format a value according to a format string.
|
|
* Supports: {value:,} for thousands, {value:.1f} for decimals, {value:.1%} for percentages
|
|
*/
|
|
function formatValue(value: number | string, format?: string): string {
|
|
if (!format) return String(value);
|
|
|
|
const num = typeof value === 'string' ? parseFloat(value) : value;
|
|
if (isNaN(num)) return String(value);
|
|
|
|
// Simple format parsing
|
|
if (format.includes(':,}')) {
|
|
return num.toLocaleString();
|
|
}
|
|
if (format.includes(':.1f}')) {
|
|
return num.toFixed(1);
|
|
}
|
|
if (format.includes(':.2f}')) {
|
|
return num.toFixed(2);
|
|
}
|
|
if (format.includes(':.1%}')) {
|
|
return (num * 100).toFixed(1) + '%';
|
|
}
|
|
|
|
return String(value);
|
|
}
|
|
|
|
/**
|
|
* Stat card widget for displaying KPIs.
|
|
*/
|
|
export function StatCard({ config, data, loading, error, onRefresh }: StatCardProps) {
|
|
const widgetConfig = config.config as StatCardConfig;
|
|
const Icon = widgetConfig.icon ? ICONS[widgetConfig.icon] : Activity;
|
|
const colorClass = widgetConfig.color ? COLORS[widgetConfig.color] : COLORS.gray;
|
|
|
|
// Extract value and trend from data
|
|
const value = data?.[widgetConfig.value_key] ?? 0;
|
|
const trend = widgetConfig.trend_key ? data?.[widgetConfig.trend_key] : undefined;
|
|
|
|
return (
|
|
<WidgetWrapper config={config} loading={loading} error={error} onRefresh={onRefresh}>
|
|
<div className="flex items-center justify-between h-full">
|
|
<div className="flex-1">
|
|
<p className="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
|
{formatValue(value, widgetConfig.format)}
|
|
</p>
|
|
{trend !== undefined && (
|
|
<div className="flex items-center mt-1">
|
|
{Number(trend) >= 0 ? (
|
|
<TrendingUp className="w-4 h-4 text-green-500 mr-1" />
|
|
) : (
|
|
<TrendingDown className="w-4 h-4 text-red-500 mr-1" />
|
|
)}
|
|
<span
|
|
className={`text-sm ${
|
|
Number(trend) >= 0 ? 'text-green-600' : 'text-red-600'
|
|
}`}
|
|
>
|
|
{formatValue(trend, widgetConfig.trend_format || '{value:.1f}')}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{Icon && (
|
|
<div className={`p-3 rounded-full ${colorClass}`}>
|
|
<Icon className="w-6 h-6" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</WidgetWrapper>
|
|
);
|
|
}
|