170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
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>
|
|
);
|
|
} |