mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(ui): add batch processing dashboard page and translations
This commit is contained in:
206
src/app/(dashboard)/dashboard/batch/page.tsx
Normal file
206
src/app/(dashboard)/dashboard/batch/page.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Card from "@/shared/components/Card";
|
||||
import EmptyState from "@/shared/components/EmptyState";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
|
||||
export default function BatchPage() {
|
||||
const [batches, setBatches] = useState([]);
|
||||
const [files, setFiles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<"batches" | "files">("batches");
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [batchesRes, filesRes] = await Promise.all([
|
||||
fetch("/api/batches"),
|
||||
fetch("/api/files"),
|
||||
]);
|
||||
if (batchesRes.ok) {
|
||||
const data = await batchesRes.json();
|
||||
setBatches(data.batches || []);
|
||||
}
|
||||
if (filesRes.ok) {
|
||||
const data = await filesRes.json();
|
||||
setFiles(data.files || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch batches/files", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderBatches = () => {
|
||||
if (batches.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon="view_list"
|
||||
title="No batches found"
|
||||
description="There are no OpenAI-compatible batches processing right now."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{batches.map((batch: any) => (
|
||||
<Card key={batch.id} className="p-4 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm font-semibold">{batch.id}</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded text-xs font-medium ${
|
||||
batch.status === "completed"
|
||||
? "bg-emerald-500/10 text-emerald-500"
|
||||
: batch.status === "failed"
|
||||
? "bg-red-500/10 text-red-500"
|
||||
: batch.status === "in_progress"
|
||||
? "bg-blue-500/10 text-blue-500"
|
||||
: "bg-surface text-text-muted"
|
||||
}`}
|
||||
>
|
||||
{batch.status}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-text-muted">
|
||||
{formatDistanceToNow(batch.createdAt, { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-text-muted text-xs">Endpoint</span>
|
||||
<span>{batch.endpoint}</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-text-muted text-xs">Input File</span>
|
||||
<span className="font-mono text-xs truncate" title={batch.inputFileId}>
|
||||
{batch.inputFileId}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-text-muted text-xs">Output File</span>
|
||||
<span className="font-mono text-xs truncate">{batch.outputFileId || "—"}</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-text-muted text-xs">Progress</span>
|
||||
<span>
|
||||
{batch.requestCountsCompleted || 0} / {batch.requestCountsTotal || 0} reqs
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFiles = () => {
|
||||
if (files.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon="insert_drive_file"
|
||||
title="No files found"
|
||||
description="Files uploaded for batch processing will appear here."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{files.map((file: any) => (
|
||||
<Card key={file.id} className="p-4 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm font-semibold">{file.id}</span>
|
||||
</div>
|
||||
<span className="text-xs text-text-muted">
|
||||
{formatDistanceToNow(file.createdAt, { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-text-muted text-xs">Filename</span>
|
||||
<span className="truncate">{file.filename}</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-text-muted text-xs">Purpose</span>
|
||||
<span>{file.purpose}</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-text-muted text-xs">Size</span>
|
||||
<span>{(file.bytes / 1024).toFixed(2)} KB</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-text-muted text-xs">Status</span>
|
||||
<span className="capitalize">{file.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-y-auto custom-scrollbar">
|
||||
<div className="flex flex-col px-6 py-6 md:py-8 max-w-6xl mx-auto w-full gap-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-text-main mb-1">Batch Processing</h1>
|
||||
<p className="text-text-muted">Monitor asynchronous batch requests and files</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={fetchData}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-surface/50 border border-border/50 text-sm hover:bg-surface disabled:opacity-50"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">refresh</span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 border-b border-border/50 pb-px">
|
||||
<button
|
||||
onClick={() => setActiveTab("batches")}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "batches"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-text-muted hover:text-text-main"
|
||||
}`}
|
||||
>
|
||||
Batches ({batches.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("files")}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "files"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-text-muted hover:text-text-main"
|
||||
}`}
|
||||
>
|
||||
Files ({files.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading && batches.length === 0 && files.length === 0 ? (
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
</div>
|
||||
) : activeTab === "batches" ? (
|
||||
renderBatches()
|
||||
) : (
|
||||
renderFiles()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/app/api/batches/route.ts
Normal file
18
src/app/api/batches/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { listBatches } from "@/lib/localDb";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const limit = Number.parseInt(url.searchParams.get("limit") || "100", 10);
|
||||
const batches = listBatches(undefined, limit);
|
||||
return NextResponse.json({ batches });
|
||||
} catch (error) {
|
||||
console.log("Error fetching batches:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch batches" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
18
src/app/api/files/route.ts
Normal file
18
src/app/api/files/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { listFiles } from "@/lib/localDb";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const limit = Number.parseInt(url.searchParams.get("limit") || "100", 10);
|
||||
const files = listFiles({ limit });
|
||||
return NextResponse.json({ files });
|
||||
} catch (error) {
|
||||
console.log("Error fetching files:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch files" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -146,6 +146,7 @@
|
||||
"dashboard": "Dashboard",
|
||||
"providers": "Providers",
|
||||
"combos": "Combos",
|
||||
"batch": "Batch Jobs",
|
||||
"usage": "Usage",
|
||||
"analytics": "Analytics",
|
||||
"costs": "Costs",
|
||||
|
||||
@@ -4,6 +4,7 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [
|
||||
"api-manager",
|
||||
"providers",
|
||||
"combos",
|
||||
"batch",
|
||||
"costs",
|
||||
"analytics",
|
||||
"cache",
|
||||
@@ -51,6 +52,7 @@ const PRIMARY_SIDEBAR_ITEMS: readonly SidebarItemDefinition[] = [
|
||||
{ id: "api-manager", href: "/dashboard/api-manager", i18nKey: "apiManager", icon: "vpn_key" },
|
||||
{ id: "providers", href: "/dashboard/providers", i18nKey: "providers", icon: "dns" },
|
||||
{ id: "combos", href: "/dashboard/combos", i18nKey: "combos", icon: "layers" },
|
||||
{ id: "batch", href: "/dashboard/batch", i18nKey: "batch", icon: "view_list" },
|
||||
{ id: "costs", href: "/dashboard/costs", i18nKey: "costs", icon: "account_balance_wallet" },
|
||||
{ id: "analytics", href: "/dashboard/analytics", i18nKey: "analytics", icon: "analytics" },
|
||||
{ id: "cache", href: "/dashboard/cache", i18nKey: "cache", icon: "cached" },
|
||||
|
||||
@@ -29,6 +29,7 @@ test("primary sidebar items place limits after cache", () => {
|
||||
"api-manager",
|
||||
"providers",
|
||||
"combos",
|
||||
"batch",
|
||||
"costs",
|
||||
"analytics",
|
||||
"cache",
|
||||
|
||||
Reference in New Issue
Block a user