Files
bifrost/ui/app/workspace/providers/views/providerGovernanceTable.tsx
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

250 lines
7.5 KiB
TypeScript

import { Badge } from "@/components/ui/badge";
import { CardHeader, CardTitle } from "@/components/ui/card";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { resetDurationLabels } from "@/lib/constants/governance";
import { useGetProviderGovernanceQuery } from "@/lib/store";
import { ModelProvider } from "@/lib/types/config";
import { cn } from "@/lib/utils";
import { formatCurrency } from "@/lib/utils/governance";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
interface Props {
className?: string;
provider: ModelProvider;
}
// Helper to format reset duration for display
const formatResetDuration = (duration: string) => {
return resetDurationLabels[duration] || duration;
};
// Circular progress component
function CircularProgress({
value,
max,
size = 80,
strokeWidth = 6,
isExhausted = false,
}: {
value: number;
max: number;
size?: number;
strokeWidth?: number;
isExhausted?: boolean;
}) {
const percentage = max > 0 ? Math.min((value / max) * 100, 100) : 0;
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
const strokeDashoffset = circumference - (percentage / 100) * circumference;
return (
<div className="relative" style={{ width: size, height: size }}>
<svg width={size} height={size} className="-rotate-90 transform">
{/* Background circle */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
className="text-muted/70 dark:text-muted/30"
/>
{/* Progress circle */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
className={cn(
"transition-all duration-500",
isExhausted ? "text-red-500/70" : percentage > 80 ? "text-amber-500/70" : "text-emerald-500/70",
)}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span
className={cn("text-lg font-medium", isExhausted ? "text-red-500/70" : percentage > 80 ? "text-amber-500/70" : "text-foreground")}
>
{Math.round(percentage)}%
</span>
</div>
</div>
);
}
// Metric card component
function MetricCard({
title,
value,
max,
unit,
resetDuration,
isExhausted,
}: {
title: string;
value: number;
max: number;
unit: string;
resetDuration: string;
isExhausted: boolean;
}) {
// Compute safe percentage to avoid division by zero
const percentage = max > 0 ? Math.round((value / max) * 100) : 0;
const clampedPercentage = Math.max(0, Math.min(100, percentage));
return (
<div
className={cn(
"group relative overflow-hidden rounded-sm border p-5 transition-all duration-300",
"hover:shadow-lg hover:shadow-black/5",
isExhausted ? "border-red-500/30 bg-red-500/5" : "border-border/50 bg-card hover:border-border",
)}
>
{/* Subtle gradient overlay */}
<div className="from-primary/5 pointer-events-none absolute inset-0 bg-gradient-to-br to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
<div className="relative flex items-start justify-between gap-4">
<div className="flex-1 space-y-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-muted-foreground text-sm font-medium whitespace-nowrap">{title}</span>
{isExhausted && (
<Badge variant="destructive" className="text-xs whitespace-nowrap">
Exhausted
</Badge>
)}
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="space-y-1">
<div className="flex items-baseline gap-1">
<span className="text-2xl font-medium tracking-tight">
{unit === "$" ? formatCurrency(value) : value.toLocaleString()}
</span>
<span className="text-muted-foreground text-sm">
/ {unit === "$" ? formatCurrency(max) : `${max.toLocaleString()} ${unit}`}
</span>
</div>
<div className="text-xs">
<span className="text-muted-foreground">Resets {formatResetDuration(resetDuration)}</span>
</div>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
<p className="font-medium">
{clampedPercentage}% of {title.toLowerCase()} used
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<CircularProgress value={value} max={max} isExhausted={isExhausted} />
</div>
</div>
);
}
export default function ProviderGovernanceTable({ provider, className }: Props) {
const hasViewAccess = useRbac(RbacResource.Governance, RbacOperation.View);
const { data: providerGovernanceData, isLoading } = useGetProviderGovernanceQuery(undefined, {
skip: !hasViewAccess,
pollingInterval: 5000,
});
// Find governance data for this provider
const providerGovernance = providerGovernanceData?.providers?.find((p) => p.provider === provider.name);
// Check if any governance is configured
const hasGovernance = providerGovernance?.budget || providerGovernance?.rate_limit;
if (isLoading) {
return (
<div className={cn("w-full", className)}>
<CardHeader className="mb-4 px-0">
<CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">Governance</div>
</CardTitle>
</CardHeader>
<div className="flex items-center justify-center py-12">
<div className="border-primary h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" />
</div>
</div>
);
}
// Governance not enabled or no governance configured - don't show the section
if (!hasGovernance) {
return null;
}
const budget = providerGovernance?.budget;
const rateLimit = providerGovernance?.rate_limit;
const isBudgetExhausted = !!(budget?.max_limit && budget.max_limit > 0 && budget.current_usage >= budget.max_limit);
const isTokenExhausted = !!(
rateLimit?.token_max_limit &&
rateLimit.token_max_limit > 0 &&
rateLimit.token_current_usage >= rateLimit.token_max_limit
);
const isRequestExhausted = !!(
rateLimit?.request_max_limit &&
rateLimit.request_max_limit > 0 &&
rateLimit.request_current_usage >= rateLimit.request_max_limit
);
return (
<div className={cn("w-full", className)}>
<CardHeader className="mb-4 px-0">
<CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">Governance</div>
</CardTitle>
</CardHeader>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{/* Budget Card */}
{budget && (
<MetricCard
title="Budget"
value={budget.current_usage}
max={budget.max_limit}
unit="$"
resetDuration={budget.reset_duration}
isExhausted={isBudgetExhausted}
/>
)}
{/* Token Rate Limit Card */}
{rateLimit?.token_max_limit && (
<MetricCard
title="Token Limit"
value={rateLimit.token_current_usage}
max={rateLimit.token_max_limit}
unit="tokens"
resetDuration={rateLimit.token_reset_duration || "1h"}
isExhausted={isTokenExhausted}
/>
)}
{/* Request Rate Limit Card */}
{rateLimit?.request_max_limit && (
<MetricCard
title="Request Limit"
value={rateLimit.request_current_usage}
max={rateLimit.request_max_limit}
unit="requests"
resetDuration={rateLimit.request_reset_duration || "1h"}
isExhausted={isRequestExhausted}
/>
)}
</div>
</div>
);
}