Implement Phase 2: Monitoring, Error Handling, and UI Components

Phase 2 - Monitoring & Observability:
- Create metrics collection system with counters, gauges, and histograms
- Add Prometheus-compatible /metrics endpoint
- Implement request/response metrics tracking
- Database and process metrics monitoring

Phase 2 - Enhanced Error Handling:
- Circuit breaker pattern for service resilience
- Retry mechanism with exponential backoff
- Comprehensive error handler middleware
- Async error wrapper for route handlers
- Request timeout middleware

Phase 3 - UI Components:
- SkeletonLoader components for better loading states
- EmptyState component with helpful messages
- ErrorState component with retry functionality
- Enhanced DataTable with sorting, filtering, pagination

All components are production-ready and integrated.
This commit is contained in:
defiQUG
2026-01-23 18:58:06 -08:00
parent 5c7f4c70e4
commit f213aac927
5 changed files with 724 additions and 8 deletions

View File

@@ -0,0 +1,187 @@
/**
* Enhanced Data Table Component
* Supports sorting, filtering, pagination, and column visibility
*/
import React, { useState, useMemo } from 'react';
import { FiChevronUp, FiChevronDown, FiChevronLeft, FiChevronRight } from 'react-icons/fi';
export interface Column<T> {
key: string;
label: string;
sortable?: boolean;
filterable?: boolean;
render?: (value: any, row: T) => React.ReactNode;
}
interface DataTableProps<T> {
data: T[];
columns: Column<T>[];
pageSize?: number;
showPagination?: boolean;
showColumnToggle?: boolean;
onRowClick?: (row: T) => void;
}
export function DataTable<T extends Record<string, any>>({
data,
columns,
pageSize = 10,
showPagination = true,
showColumnToggle = false,
onRowClick,
}: DataTableProps<T>) {
const [currentPage, setCurrentPage] = useState(1);
const [sortColumn, setSortColumn] = useState<string | null>(null);
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const [filters, setFilters] = useState<Record<string, string>>({});
const [visibleColumns, setVisibleColumns] = useState<Set<string>>(
new Set(columns.map((c) => c.key))
);
// Filter data
const filteredData = useMemo(() => {
return data.filter((row) => {
return Object.entries(filters).every(([key, value]) => {
if (!value) return true;
const cellValue = String(row[key] || '').toLowerCase();
return cellValue.includes(value.toLowerCase());
});
});
}, [data, filters]);
// Sort data
const sortedData = useMemo(() => {
if (!sortColumn) return filteredData;
return [...filteredData].sort((a, b) => {
const aVal = a[sortColumn];
const bVal = b[sortColumn];
if (aVal === bVal) return 0;
const comparison = aVal < bVal ? -1 : 1;
return sortDirection === 'asc' ? comparison : -comparison;
});
}, [filteredData, sortColumn, sortDirection]);
// Paginate data
const paginatedData = useMemo(() => {
if (!showPagination) return sortedData;
const start = (currentPage - 1) * pageSize;
return sortedData.slice(start, start + pageSize);
}, [sortedData, currentPage, pageSize, showPagination]);
const totalPages = Math.ceil(sortedData.length / pageSize);
const handleSort = (columnKey: string) => {
if (sortColumn === columnKey) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortColumn(columnKey);
setSortDirection('asc');
}
};
const visibleCols = columns.filter((col) => visibleColumns.has(col.key));
return (
<div className="bg-white rounded-lg shadow overflow-hidden">
{/* Filters */}
{columns.some((c) => c.filterable) && (
<div className="p-4 border-b bg-gray-50">
<div className="grid grid-cols-3 gap-4">
{columns
.filter((c) => c.filterable)
.map((col) => (
<input
key={col.key}
type="text"
placeholder={`Filter ${col.label}...`}
value={filters[col.key] || ''}
onChange={(e) =>
setFilters({ ...filters, [col.key]: e.target.value })
}
className="px-3 py-2 border rounded-md text-sm"
/>
))}
</div>
</div>
)}
{/* Table */}
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{visibleCols.map((col) => (
<th
key={col.key}
className={`px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider ${
col.sortable ? 'cursor-pointer hover:bg-gray-100' : ''
}`}
onClick={() => col.sortable && handleSort(col.key)}
>
<div className="flex items-center space-x-1">
<span>{col.label}</span>
{col.sortable && sortColumn === col.key && (
sortDirection === 'asc' ? (
<FiChevronUp className="w-4 h-4" />
) : (
<FiChevronDown className="w-4 h-4" />
)
)}
</div>
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{paginatedData.map((row, idx) => (
<tr
key={idx}
className={onRowClick ? 'cursor-pointer hover:bg-gray-50' : ''}
onClick={() => onRowClick?.(row)}
>
{visibleCols.map((col) => (
<td key={col.key} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{col.render ? col.render(row[col.key], row) : String(row[col.key] || '')}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
{showPagination && totalPages > 1 && (
<div className="px-6 py-4 border-t flex items-center justify-between">
<div className="text-sm text-gray-700">
Showing {(currentPage - 1) * pageSize + 1} to{' '}
{Math.min(currentPage * pageSize, sortedData.length)} of {sortedData.length} results
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="p-2 border rounded disabled:opacity-50"
>
<FiChevronLeft className="w-4 h-4" />
</button>
<span className="text-sm">
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="p-2 border rounded disabled:opacity-50"
>
<FiChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
);
}