feat: Add extensible multi-pipeline integration system
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>
This commit is contained in:
134
web/components/dashboard/widgets/DataTable.tsx
Normal file
134
web/components/dashboard/widgets/DataTable.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
'use client';
|
||||
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import type { WidgetConfig, TableData, TableWidgetConfig } from '@/lib/pipeline-types';
|
||||
import { WidgetWrapper } from './WidgetWrapper';
|
||||
|
||||
interface DataTableWidgetProps {
|
||||
config: WidgetConfig;
|
||||
data: TableData | null;
|
||||
loading: boolean;
|
||||
error?: string;
|
||||
onRefresh?: () => void;
|
||||
onPageChange?: (page: number) => void;
|
||||
currentPage?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data table widget with pagination.
|
||||
*/
|
||||
export function DataTableWidget({
|
||||
config,
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
onRefresh,
|
||||
onPageChange,
|
||||
currentPage = 1,
|
||||
}: DataTableWidgetProps) {
|
||||
const tableConfig = config.config as TableWidgetConfig;
|
||||
const rows = data?.data || [];
|
||||
const total = data?.total || 0;
|
||||
const pageSize = tableConfig.page_size || 10;
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
return (
|
||||
<WidgetWrapper config={config} loading={loading} error={error} onRefresh={onRefresh}>
|
||||
{rows.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full text-gray-500">
|
||||
No data available
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Table */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800 sticky top-0">
|
||||
<tr>
|
||||
{tableConfig.columns.map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
className={`px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider ${
|
||||
col.align === 'right'
|
||||
? 'text-right'
|
||||
: col.align === 'center'
|
||||
? 'text-center'
|
||||
: 'text-left'
|
||||
}`}
|
||||
style={{ width: col.width ? `${col.width}px` : undefined }}
|
||||
>
|
||||
{col.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{rows.map((row, rowIndex) => (
|
||||
<tr
|
||||
key={row[tableConfig.row_key] as string || rowIndex}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
{tableConfig.columns.map((col) => (
|
||||
<td
|
||||
key={col.key}
|
||||
className={`px-4 py-3 text-sm text-gray-900 dark:text-gray-100 whitespace-nowrap ${
|
||||
col.align === 'right'
|
||||
? 'text-right'
|
||||
: col.align === 'center'
|
||||
? 'text-center'
|
||||
: 'text-left'
|
||||
}`}
|
||||
>
|
||||
{formatCellValue(row[col.key], col.format)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{tableConfig.show_pagination !== false && totalPages > 1 && onPageChange && (
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
||||
<div className="text-sm text-gray-500">
|
||||
Showing {(currentPage - 1) * pageSize + 1} to{' '}
|
||||
{Math.min(currentPage * pageSize, total)} of {total}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages}
|
||||
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</WidgetWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function formatCellValue(value: unknown, format?: string): string {
|
||||
if (value === null || value === undefined) return '-';
|
||||
if (typeof value === 'number') {
|
||||
if (format?.includes('%')) {
|
||||
return `${value.toFixed(1)}%`;
|
||||
}
|
||||
return value.toLocaleString();
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
Reference in New Issue
Block a user