135 lines
4.7 KiB
TypeScript
135 lines
4.7 KiB
TypeScript
'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 unknown 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">
|
|
<thead className="bg-gray-50 sticky top-0">
|
|
<tr>
|
|
{tableConfig.columns.map((col) => (
|
|
<th
|
|
key={col.key}
|
|
className={`px-4 py-3 text-xs font-semibold text-gray-600 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 divide-y divide-gray-200">
|
|
{rows.map((row, rowIndex) => (
|
|
<tr
|
|
key={row[tableConfig.row_key] as string || rowIndex}
|
|
className="hover:bg-gray-50"
|
|
>
|
|
{tableConfig.columns.map((col) => (
|
|
<td
|
|
key={col.key}
|
|
className={`px-4 py-3 text-sm text-gray-900 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 bg-gray-50">
|
|
<div className="text-sm text-gray-600">
|
|
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 text-gray-600 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<ChevronLeft className="w-5 h-5" />
|
|
</button>
|
|
<span className="text-sm text-gray-700">
|
|
Page {currentPage} of {totalPages}
|
|
</span>
|
|
<button
|
|
onClick={() => onPageChange(currentPage + 1)}
|
|
disabled={currentPage >= totalPages}
|
|
className="p-1 rounded text-gray-600 hover:bg-gray-200 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);
|
|
}
|