mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(frontend): add subscription tier selector to admin rate-limits page
Adds tier badge display and dropdown selector to the admin rate-limits page. Admins can now view and change a user's subscription tier (FREE/PRO/BUSINESS/ENTERPRISE) with multiplier info. The dropdown calls POST /api/copilot/admin/rate_limit/tier and re-fetches limits to reflect the new tier. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,18 +3,46 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import type { UserRateLimitResponse } from "@/app/api/__generated__/models/userRateLimitResponse";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { UsageBar } from "../../components/UsageBar";
|
||||
|
||||
const TIERS = ["FREE", "PRO", "BUSINESS", "ENTERPRISE"] as const;
|
||||
type Tier = (typeof TIERS)[number];
|
||||
|
||||
const TIER_MULTIPLIERS: Record<Tier, string> = {
|
||||
FREE: "1x base limits",
|
||||
PRO: "10x base limits",
|
||||
BUSINESS: "30x base limits",
|
||||
ENTERPRISE: "60x base limits",
|
||||
};
|
||||
|
||||
const TIER_COLORS: Record<Tier, string> = {
|
||||
FREE: "bg-gray-100 text-gray-700",
|
||||
PRO: "bg-blue-100 text-blue-700",
|
||||
BUSINESS: "bg-purple-100 text-purple-700",
|
||||
ENTERPRISE: "bg-amber-100 text-amber-700",
|
||||
};
|
||||
|
||||
interface Props {
|
||||
data: UserRateLimitResponse;
|
||||
onReset: (resetWeekly: boolean) => Promise<void>;
|
||||
onTierChange?: (newTier: string) => Promise<void>;
|
||||
/** Override the outer container classes (default: bordered card). */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function RateLimitDisplay({ data, onReset, className }: Props) {
|
||||
export function RateLimitDisplay({
|
||||
data,
|
||||
onReset,
|
||||
onTierChange,
|
||||
className,
|
||||
}: Props) {
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
const [resetWeekly, setResetWeekly] = useState(false);
|
||||
const [isChangingTier, setIsChangingTier] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const currentTier = ((data as Record<string, unknown>).tier as Tier) ?? "PRO";
|
||||
|
||||
async function handleReset() {
|
||||
const msg = resetWeekly
|
||||
@@ -30,19 +58,76 @@ export function RateLimitDisplay({ data, onReset, className }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTierChange(newTier: string) {
|
||||
if (newTier === currentTier || !onTierChange) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
`Change tier from ${currentTier} to ${newTier}? This will change the user's rate limits.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
setIsChangingTier(true);
|
||||
try {
|
||||
await onTierChange(newTier);
|
||||
toast({
|
||||
title: "Tier updated",
|
||||
description: `Changed to ${newTier} (${TIER_MULTIPLIERS[newTier as Tier]}).`,
|
||||
});
|
||||
} catch {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to update tier.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsChangingTier(false);
|
||||
}
|
||||
}
|
||||
|
||||
const nothingToReset = resetWeekly
|
||||
? data.daily_tokens_used === 0 && data.weekly_tokens_used === 0
|
||||
: data.daily_tokens_used === 0;
|
||||
|
||||
return (
|
||||
<div className={className ?? "rounded-md border bg-white p-6"}>
|
||||
<h2 className="mb-1 text-lg font-semibold">
|
||||
Rate Limits for {data.user_email ?? data.user_id}
|
||||
</h2>
|
||||
{data.user_email && (
|
||||
<p className="mb-4 text-xs text-gray-500">User ID: {data.user_id}</p>
|
||||
)}
|
||||
{!data.user_email && <div className="mb-4" />}
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="mb-1 text-lg font-semibold">
|
||||
Rate Limits for {data.user_email ?? data.user_id}
|
||||
</h2>
|
||||
{data.user_email && (
|
||||
<p className="text-xs text-gray-500">User ID: {data.user_id}</p>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`rounded-full px-3 py-1 text-xs font-medium ${TIER_COLORS[currentTier] ?? "bg-gray-100 text-gray-700"}`}
|
||||
>
|
||||
{currentTier}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Subscription Tier
|
||||
</label>
|
||||
<select
|
||||
aria-label="Subscription tier"
|
||||
value={currentTier}
|
||||
onChange={(e) => handleTierChange(e.target.value)}
|
||||
className="rounded-md border bg-white px-3 py-1.5 text-sm"
|
||||
disabled={isChangingTier || !onTierChange}
|
||||
>
|
||||
{TIERS.map((tier) => (
|
||||
<option key={tier} value={tier}>
|
||||
{tier} — {TIER_MULTIPLIERS[tier]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isChangingTier && (
|
||||
<span className="text-xs text-gray-500">Updating...</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -14,6 +14,7 @@ export function RateLimitManager() {
|
||||
handleSearch,
|
||||
handleSelectUser,
|
||||
handleReset,
|
||||
handleTierChange,
|
||||
} = useRateLimitManager();
|
||||
|
||||
return (
|
||||
@@ -74,7 +75,11 @@ export function RateLimitManager() {
|
||||
)}
|
||||
|
||||
{rateLimitData && (
|
||||
<RateLimitDisplay data={rateLimitData} onReset={handleReset} />
|
||||
<RateLimitDisplay
|
||||
data={rateLimitData}
|
||||
onReset={handleReset}
|
||||
onTierChange={handleTierChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -199,6 +199,29 @@ export function useRateLimitManager() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTierChange(newTier: string) {
|
||||
if (!rateLimitData) return;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/copilot/admin/rate_limit/tier`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
user_id: rateLimitData.user_id,
|
||||
tier: newTier,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to update tier");
|
||||
}
|
||||
|
||||
// Re-fetch rate limit data to reflect new tier limits
|
||||
await fetchRateLimit(rateLimitData.user_id);
|
||||
}
|
||||
|
||||
return {
|
||||
isSearching,
|
||||
isLoadingRateLimit,
|
||||
@@ -208,5 +231,6 @@ export function useRateLimitManager() {
|
||||
handleSearch,
|
||||
handleSelectUser,
|
||||
handleReset,
|
||||
handleTierChange,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user