mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 13:22:11 +03:00
OmniRoute is an intelligent API gateway that unifies 20+ AI providers behind a single OpenAI-compatible endpoint. Features include intelligent routing with 6 strategies, multi-format translation (OpenAI/Claude/Gemini/Responses API), circuit breakers, semantic caching, combo fallback chains, real-time health monitoring, and a full dashboard with provider management, analytics, and CLI tool integration. Key highlights: - 20+ providers (Claude Code, Codex, Gemini CLI, GitHub Copilot, iFlow, Qwen, Kiro, etc.) - 6 routing strategies (Fill First, Round Robin, P2C, Random, Least Used, Cost Optimized) - Export/Import database backup with full archive support - Translator Playground with 4 modes (Playground, Chat Tester, Test Bench, Live Monitor) - 100% TypeScript across src/ and open-sse/ - Docker support with multi-stage builds - Comprehensive documentation and 9 dashboard screenshots
182 lines
5.0 KiB
TypeScript
182 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* DataTable — Shared UI primitive (T-29)
|
|
*
|
|
* Configurable data table with sticky header, row click,
|
|
* and optional loading/empty states. Extracts the shared
|
|
* table rendering pattern from RequestLoggerV2 and ProxyLogger.
|
|
*
|
|
* Usage:
|
|
* <DataTable
|
|
* columns={visibleColumns}
|
|
* data={filteredLogs}
|
|
* renderCell={(row, column) => <span>{row[column.key]}</span>}
|
|
* onRowClick={(row) => openDetail(row)}
|
|
* selectedId={selectedLog?.id}
|
|
* loading={isLoading}
|
|
* emptyIcon="📋"
|
|
* emptyMessage="No logs found"
|
|
* />
|
|
*/
|
|
|
|
interface DataTableColumn {
|
|
key: string;
|
|
label: string;
|
|
maxWidth?: string;
|
|
}
|
|
|
|
interface DataTableRow {
|
|
id?: string | number;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface DataTableProps {
|
|
columns?: DataTableColumn[];
|
|
data?: DataTableRow[];
|
|
renderCell: (row: DataTableRow, column: DataTableColumn) => React.ReactNode;
|
|
renderHeader?: (column: DataTableColumn) => React.ReactNode;
|
|
onRowClick?: (row: DataTableRow) => void;
|
|
selectedId?: string | number;
|
|
loading?: boolean;
|
|
maxHeight?: string;
|
|
emptyIcon?: string;
|
|
emptyMessage?: string;
|
|
}
|
|
|
|
export default function DataTable({
|
|
columns = [],
|
|
data = [],
|
|
renderCell,
|
|
renderHeader,
|
|
onRowClick,
|
|
selectedId,
|
|
loading = false,
|
|
maxHeight = "calc(100vh - 320px)",
|
|
emptyIcon = "📭",
|
|
emptyMessage = "No data found",
|
|
}: DataTableProps) {
|
|
if (loading) {
|
|
return (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
padding: "48px 24px",
|
|
color: "var(--text-secondary, #888)",
|
|
fontSize: "14px",
|
|
}}
|
|
>
|
|
<span style={{ animation: "spin 1s linear infinite", marginRight: "8px" }}>⏳</span>
|
|
Loading...
|
|
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (data.length === 0) {
|
|
return (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
padding: "48px 24px",
|
|
color: "var(--text-secondary, #888)",
|
|
fontSize: "14px",
|
|
}}
|
|
>
|
|
<span style={{ fontSize: "32px", marginBottom: "8px", opacity: 0.6 }}>{emptyIcon}</span>
|
|
{emptyMessage}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{ overflow: "auto", maxHeight, borderRadius: "8px" }}>
|
|
<table
|
|
style={{
|
|
width: "100%",
|
|
borderCollapse: "collapse",
|
|
fontSize: "12px",
|
|
tableLayout: "auto",
|
|
}}
|
|
>
|
|
<thead>
|
|
<tr>
|
|
{columns.map((col) => (
|
|
<th
|
|
key={col.key}
|
|
style={{
|
|
padding: "8px 10px",
|
|
textAlign: "left",
|
|
fontWeight: 600,
|
|
color: "var(--text-secondary, #888)",
|
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
|
position: "sticky",
|
|
top: 0,
|
|
background: "var(--bg-table-header, rgba(15,15,25,0.95))",
|
|
zIndex: 1,
|
|
whiteSpace: "nowrap",
|
|
fontSize: "11px",
|
|
textTransform: "uppercase",
|
|
letterSpacing: "0.5px",
|
|
}}
|
|
>
|
|
{renderHeader ? renderHeader(col) : col.label}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.map((row, idx) => (
|
|
<tr
|
|
key={row.id || idx}
|
|
onClick={() => onRowClick?.(row)}
|
|
style={{
|
|
cursor: onRowClick ? "pointer" : "default",
|
|
background:
|
|
row.id === selectedId
|
|
? "rgba(99,102,241,0.1)"
|
|
: idx % 2 === 0
|
|
? "transparent"
|
|
: "rgba(255,255,255,0.02)",
|
|
transition: "background 0.15s",
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (row.id !== selectedId) {
|
|
e.currentTarget.style.background = "rgba(255,255,255,0.04)";
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (row.id !== selectedId) {
|
|
e.currentTarget.style.background =
|
|
idx % 2 === 0 ? "transparent" : "rgba(255,255,255,0.02)";
|
|
}
|
|
}}
|
|
>
|
|
{columns.map((col) => (
|
|
<td
|
|
key={col.key}
|
|
style={{
|
|
padding: "6px 10px",
|
|
borderBottom: "1px solid rgba(255,255,255,0.04)",
|
|
whiteSpace: "nowrap",
|
|
maxWidth: col.maxWidth || "200px",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
}}
|
|
>
|
|
{renderCell(row, col)}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|