first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
import { Button } from "@/components/ui/button";
import { Link } from "@tanstack/react-router";
import { LayoutGrid } from "lucide-react";
export function ModelCatalogEmptyState() {
return (
<div className="flex min-h-[80vh] w-full flex-col items-center justify-center gap-4 py-16 text-center">
<div className="text-muted-foreground">
<LayoutGrid className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />
</div>
<div className="flex flex-col gap-1">
<h1 className="text-muted-foreground text-xl font-medium">No providers configured yet</h1>
<div className="text-muted-foreground mx-auto mt-2 max-w-[600px] text-sm font-normal">
Configure your first model provider to see an overview of all providers, API keys, models, and usage metrics.
</div>
<div className="mx-auto mt-6 flex flex-row flex-wrap items-center justify-center gap-2">
<Button asChild data-testid="modelcatalog-configure-providers-cta">
<Link to="/workspace/providers">Configure Providers</Link>
</Button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,201 @@
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { ProviderIconType, RenderProviderIcon } from "@/lib/constants/icons";
import { ProviderLabels } from "@/lib/constants/logs";
import { Info } from "lucide-react";
function formatCost(dollars: number) {
return `$${dollars.toFixed(4)}`;
}
export interface ModelCatalogRow {
providerName: string;
isCustom: boolean;
baseProviderType?: string;
modelsUsed: string[];
totalTraffic24h: number;
totalCost24h: number;
}
interface ModelCatalogTableProps {
rows: ModelCatalogRow[];
providers: string[];
providerFilter: string;
onProviderFilterChange: (value: string) => void;
totalProviders: number;
totalModels: number;
totalRequests24h: number;
totalCost24h: number;
isLoadingModels: boolean;
}
export default function ModelCatalogTable({
rows,
providers,
providerFilter,
onProviderFilterChange,
totalProviders,
totalModels,
totalRequests24h,
totalCost24h,
isLoadingModels,
}: ModelCatalogTableProps) {
const summaryCards = [
{ label: "Total Providers", value: totalProviders.toLocaleString() },
{ label: "Total Models", value: totalModels.toLocaleString() },
{ label: "Total Requests (24h)", value: totalRequests24h.toLocaleString() },
{ label: "Total Cost (24h)", value: formatCost(totalCost24h) },
];
return (
<div className="space-y-6">
{/* Summary Cards */}
<div className="grid grid-cols-4 gap-4">
{summaryCards.map((card) => (
<Card key={card.label} className="py-4 shadow-none">
<CardContent className="px-4">
<p className="text-muted-foreground text-xs">{card.label}</p>
<p className="mt-1 text-xl font-semibold">{card.value}</p>
</CardContent>
</Card>
))}
</div>
{/* Header + Filter */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">Model Catalog</h2>
<p className="text-muted-foreground text-sm">Overview of all configured providers, models, and usage.</p>
</div>
<Select
value={providerFilter || "all"}
onValueChange={(val) => onProviderFilterChange(val === "all" ? "" : val)}
data-testid="model-catalog-provider-filter"
>
<SelectTrigger className="w-[200px]" data-testid="model-catalog-provider-trigger">
<SelectValue placeholder="All Providers" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Providers</SelectItem>
{providers.map((p) => (
<SelectItem key={p} value={p}>
{ProviderLabels[p as keyof typeof ProviderLabels] || p}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Table */}
<div className="rounded-sm border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Provider</TableHead>
<TableHead>
<TooltipProvider>
<div className="flex items-center gap-1">
Models
<Tooltip>
<TooltipTrigger data-testid="model-catalog-models-info-trigger">
<Info className="text-muted-foreground h-3.5 w-3.5" />
</TooltipTrigger>
<TooltipContent side="bottom">Models used in the last 30 days</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</TableHead>
<TableHead className="text-right">Total Traffic (24h)</TableHead>
<TableHead className="text-right">Total Cost (24h)</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center">
<span className="text-muted-foreground text-sm">No matching providers found.</span>
</TableCell>
</TableRow>
) : (
rows.map((row) => (
<TableRow key={row.providerName}>
<TableCell>
<div className="flex items-center gap-2">
<RenderProviderIcon
provider={(row.isCustom ? row.baseProviderType : row.providerName) as ProviderIconType}
size="sm"
className="h-4 w-4 shrink-0"
/>
<span className="font-medium">
{row.isCustom
? row.providerName
: ProviderLabels[row.providerName as keyof typeof ProviderLabels] || row.providerName}
</span>
{row.isCustom && (
<Badge variant="secondary" className="text-muted-foreground px-1.5 py-0.5 text-[10px] font-bold">
CUSTOM
</Badge>
)}
</div>
</TableCell>
<TableCell>
{isLoadingModels ? (
<div className="flex items-center gap-1">
<Skeleton className="h-5 w-24 rounded-full" />
<Skeleton className="h-5 w-32 rounded-full" />
<Skeleton className="h-5 w-20 rounded-full" />
</div>
) : (
<ModelsUsedCell models={row.modelsUsed} />
)}
</TableCell>
<TableCell className="text-right font-mono text-sm">{row.totalTraffic24h.toLocaleString()}</TableCell>
<TableCell className="text-right font-mono text-sm">{formatCost(row.totalCost24h)}</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
);
}
function ModelsUsedCell({ models: rawModels }: { models: string[] }) {
const models = Array.from(new Set(rawModels.filter(Boolean)));
if (models.length === 0) {
return <span className="text-muted-foreground text-sm">-</span>;
}
const MAX_VISIBLE = 3;
const visible = models.slice(0, MAX_VISIBLE);
const remaining = models.length - MAX_VISIBLE;
return (
<TooltipProvider>
<div className="flex flex-wrap items-center gap-1">
{visible.map((m) => (
<Badge key={m} variant="outline" className="text-xs font-normal">
{m}
</Badge>
))}
{remaining > 0 && (
<Tooltip>
<TooltipTrigger data-testid="model-catalog-models-overflow-trigger">
<Badge variant="outline" className="text-xs font-normal">
+{remaining} more
</Badge>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
{models.slice(MAX_VISIBLE).join(", ")}
</TooltipContent>
</Tooltip>
)}
</div>
</TooltipProvider>
);
}

View File

@@ -0,0 +1,170 @@
import FullPageLoader from "@/components/fullPageLoader";
import { NoPermissionView } from "@/components/noPermissionView";
import { ProviderNames } from "@/lib/constants/logs";
import { useGetModelsQuery, useGetProvidersQuery, useLazyGetLogsModelHistogramQuery, useLazyGetLogsStatsQuery } from "@/lib/store";
import { KnownProvider } from "@/lib/types/config";
import { LogStats } from "@/lib/types/logs";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { useEffect, useMemo, useState } from "react";
import { ModelCatalogEmptyState } from "./modelCatalogEmptyState";
import ModelCatalogTable, { ModelCatalogRow } from "./modelCatalogTable";
export default function ModelCatalogView() {
const hasAccess = useRbac(RbacResource.ModelProvider, RbacOperation.View);
const [providerFilter, setProviderFilter] = useState("");
const [statsMap, setStatsMap] = useState<Map<string, LogStats>>(new Map());
const [modelsUsedMap, setModelsUsedMap] = useState<Map<string, string[]>>(new Map());
const [isLoadingModels, setIsLoadingModels] = useState(true);
const {
data: providers,
isLoading: isLoadingProviders,
error: providersError,
refetch: refetchProviders,
} = useGetProvidersQuery(undefined, { skip: !hasAccess });
const { data: modelsData } = useGetModelsQuery({ unfiltered: true }, { skip: !hasAccess });
// Global 24h stats for summary cards (lazy so we get fresh timestamps)
const [triggerGlobalStats, { data: globalStats }] = useLazyGetLogsStatsQuery();
// Per-provider traffic stats (lazy, fired when providers load)
const [triggerStats] = useLazyGetLogsStatsQuery();
const [triggerModelHistogram] = useLazyGetLogsModelHistogramQuery();
useEffect(() => {
if (!hasAccess) return;
const now = new Date().toISOString();
const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
triggerGlobalStats({ filters: { start_time: dayAgo, end_time: now } });
}, [hasAccess, triggerGlobalStats]);
useEffect(() => {
if (!providers || providers.length === 0) return;
let cancelled = false;
const now = new Date().toISOString();
const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
Promise.all(
providers.map((p) =>
triggerStats({ filters: { providers: [p.name], start_time: dayAgo, end_time: now } })
.unwrap()
.then((stats) => [p.name, stats] as const)
.catch(
() =>
[
p.name,
{
total_requests: 0,
success_rate: 0,
user_facing_success_rate: 0,
average_latency: 0,
user_facing_total_requests:0,
total_tokens: 0,
total_cost: 0
},
] as const,
),
),
).then((results) => {
if (!cancelled) setStatsMap(new Map(results));
});
return () => {
cancelled = true;
};
}, [providers, triggerStats]);
// Per-provider models used in last 30 days
useEffect(() => {
if (!providers || providers.length === 0) return;
let cancelled = false;
setIsLoadingModels(true);
const now = new Date().toISOString();
const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
Promise.all(
providers.map((p) =>
triggerModelHistogram({ filters: { providers: [p.name], start_time: monthAgo, end_time: now } })
.unwrap()
.then((data): [string, string[]] => [p.name, data.models ?? []])
.catch((): [string, string[]] => [p.name, []]),
),
).then((results) => {
if (!cancelled) {
setModelsUsedMap(new Map(results));
setIsLoadingModels(false);
}
});
return () => {
cancelled = true;
};
}, [providers, triggerModelHistogram]);
// Build table rows
const rows: ModelCatalogRow[] = useMemo(() => {
if (!providers) return [];
return providers.map((p) => {
const isCustom = !ProviderNames.includes(p.name as KnownProvider);
const modelsUsed = modelsUsedMap.get(p.name) ?? [];
const providerStats = statsMap.get(p.name);
const totalTraffic24h = providerStats?.total_requests ?? 0;
const totalCost24h = providerStats?.total_cost ?? 0;
return {
providerName: p.name,
isCustom,
baseProviderType: p.custom_provider_config?.base_provider_type,
modelsUsed,
totalTraffic24h,
totalCost24h,
};
});
}, [providers, statsMap, modelsUsedMap]);
// Filter rows by provider
const filteredRows = useMemo(() => {
if (!providerFilter) return rows;
return rows.filter((r) => r.providerName === providerFilter);
}, [rows, providerFilter]);
if (isLoadingProviders) {
return <FullPageLoader />;
}
if (!hasAccess) {
return <NoPermissionView entity="model catalog" />;
}
if (providersError) {
return (
<div className="flex h-full flex-col items-center justify-center gap-4 text-center">
<p className="text-muted-foreground text-sm">Failed to load providers</p>
<button type="button" data-testid="model-catalog-retry-btn" onClick={refetchProviders} className="text-sm underline">
Retry
</button>
</div>
);
}
if (!providers || providers.length === 0) {
return <ModelCatalogEmptyState />;
}
return (
<div className="mx-auto w-full max-w-7xl">
<ModelCatalogTable
rows={filteredRows}
providers={(providers ?? []).map((p) => p.name)}
providerFilter={providerFilter}
onProviderFilterChange={setProviderFilter}
totalProviders={(providers ?? []).length}
totalModels={modelsData?.total ?? 0}
totalRequests24h={globalStats?.total_requests ?? 0}
totalCost24h={globalStats?.total_cost ?? 0}
isLoadingModels={isLoadingModels}
/>
</div>
);
}