Refine admin UI and models routing

This commit is contained in:
Evan Yang
2026-03-07 18:15:44 +08:00
parent 89fc48eea2
commit 226188e22d
26 changed files with 188 additions and 227 deletions
+4 -2
View File
@@ -72,14 +72,16 @@ def _normalize_legacy_prompt_completion_pricing(
for field in ("prompt", "completion"):
raw_value = normalized.get(field)
if not isinstance(raw_value, (str, int, float)):
continue
try:
value = float(raw_value) if raw_value is not None else None
value = float(raw_value)
except (TypeError, ValueError):
continue
# Older admin overrides were occasionally saved in "per 1M tokens"
# units instead of the backend's expected per-token unit.
if value is not None and value > 0.1:
if value > 0.1:
normalized[field] = value / 1_000_000
return normalized
+8 -1
View File
@@ -1,6 +1,7 @@
"""Tests for pricing unit normalization across model sources."""
import json
from types import TracebackType
import pytest
@@ -74,7 +75,13 @@ async def test_generic_provider_missing_pricing_defaults_to_zero(
async def __aenter__(self) -> "FakeClient":
return self
async def __aexit__(self, exc_type, exc, tb) -> None:
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
del exc_type, exc, tb
return None
async def get(
+2 -5
View File
@@ -5,7 +5,7 @@ import {
type KeyboardEvent,
} from 'react';
import { format } from 'date-fns';
import { CalendarIcon, Filter, X } from 'lucide-react';
import { CalendarIcon, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Card,
@@ -219,10 +219,7 @@ export function LogFilters({
return (
<Card className='mb-6'>
<CardHeader>
<CardTitle className='flex items-center gap-2'>
<Filter className='h-5 w-5' />
Filters
</CardTitle>
<CardTitle>Filters</CardTitle>
<CardDescription>
Filter logs by date, level, request ID, text search, status code,
method, endpoint and limit
+7 -12
View File
@@ -174,12 +174,7 @@ export default function LogsPage() {
title='System Logs'
description='View and filter application logs.'
actions={
<Button
onClick={() => refetchLogs()}
variant='outline'
size='sm'
className='w-full sm:w-auto'
>
<Button onClick={() => refetchLogs()} variant='outline' size='sm'>
<RefreshCw className='mr-2 h-4 w-4' />
Refresh
</Button>
@@ -208,21 +203,21 @@ export default function LogsPage() {
<Card>
<CardHeader>
<CardTitle className='flex flex-col items-start gap-2 sm:flex-row sm:items-center sm:justify-between'>
<span className='text-lg sm:text-xl'>Log Entries</span>
<div className='flex flex-col items-start gap-2 sm:flex-row sm:items-center sm:justify-between'>
<CardTitle>Log Entries</CardTitle>
{logsData && (
<Badge variant='secondary' className='text-xs sm:text-sm'>
<Badge variant='secondary'>
{logsData.logs.length} entries
</Badge>
)}
</CardTitle>
</div>
{hasActiveFilters && (
<CardDescription className='text-xs sm:text-sm'>
<CardDescription>
Showing logs filtered by {activeFilterDescription}
</CardDescription>
)}
</CardHeader>
<CardContent className='overflow-hidden p-3 sm:p-6'>
<CardContent className='overflow-hidden'>
{isLoading ? (
<div className='space-y-2'>
{Array.from({ length: 8 }).map((_, index) => (
+14
View File
@@ -648,6 +648,20 @@ export default function DashboardPage() {
}
}, [chartConfigs, activeChartId]);
if (!isAuthResolved) {
return (
<AppPageShell contentClassName='mx-auto w-full max-w-5xl'>
<div className='space-y-6'>
<div className='space-y-2'>
<Skeleton className='h-8 w-56 max-w-full sm:h-9' />
<Skeleton className='h-4 w-80 max-w-full' />
</div>
<SectionLoading label='summary' />
<SectionLoading label='metrics' />
</div>
</AppPageShell>
);
}
if (!isAuthenticated) {
return <CheatSheet />;
}
+24 -13
View File
@@ -20,6 +20,13 @@ import { Skeleton } from '@/components/ui/skeleton';
import { AlertCircle, Plus, Server } from 'lucide-react';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty';
import {
AlertDialog,
AlertDialogAction,
@@ -333,7 +340,7 @@ export default function ProvidersPage() {
description='Manage your AI provider connections and credentials.'
actions={
<DialogTrigger asChild>
<Button className='flex items-center gap-2'>
<Button>
<Plus className='h-4 w-4' />
Add Provider
</Button>
@@ -376,18 +383,22 @@ export default function ProvidersPage() {
</Alert>
) : providers.length === 0 ? (
<Card>
<CardContent className='flex flex-col items-center justify-center py-12'>
<Server className='text-muted-foreground mb-4 h-12 w-12' />
<h3 className='mb-2 text-lg font-semibold'>
No providers configured
</h3>
<p className='text-muted-foreground mb-4 text-sm'>
Get started by adding your first upstream provider
</p>
<Button onClick={() => setIsCreateDialogOpen(true)}>
<Plus className='mr-2 h-4 w-4' />
Add Provider
</Button>
<CardContent className='py-12'>
<Empty>
<EmptyHeader>
<EmptyMedia variant='icon'>
<Server className='h-4 w-4' />
</EmptyMedia>
<EmptyTitle>No providers configured</EmptyTitle>
<EmptyDescription>
Get started by adding your first upstream provider.
</EmptyDescription>
</EmptyHeader>
<Button onClick={() => setIsCreateDialogOpen(true)}>
<Plus className='mr-2 h-4 w-4' />
Add Provider
</Button>
</Empty>
</CardContent>
</Card>
) : (
+1 -4
View File
@@ -483,10 +483,7 @@ export function ApiEndpointTester({ models }: ApiEndpointTesterProps) {
return (
<Card className='w-full'>
<CardHeader>
<CardTitle className='flex items-center gap-2'>
<Send className='h-5 w-5' />
API Endpoint Tester
</CardTitle>
<CardTitle>API Endpoint Tester</CardTitle>
<CardDescription>
Comprehensive testing of OpenAI-compatible API endpoints through the
secure proxy (resolves CORS and network issues)
+1 -1
View File
@@ -46,7 +46,7 @@ export const API_ENDPOINTS = {
},
models: {
name: 'List Models',
path: '/model',
path: '/models',
icon: List,
description: 'List all available models from the provider',
},
+1 -1
View File
@@ -40,7 +40,7 @@ const NAV_ITEMS = [
{ title: 'Dashboard', url: '/', icon: LayoutDashboardIcon },
{ title: 'Balances', url: '/balances', icon: WalletIcon },
{ title: 'Logs', url: '/logs', icon: FileTextIcon },
{ title: 'Models', url: '/model', icon: DatabaseIcon },
{ title: 'Models', url: '/models', icon: DatabaseIcon },
{ title: 'Providers', url: '/providers', icon: ServerIcon },
{ title: 'Settings', url: '/settings', icon: SettingsIcon },
] as const;
+1 -1
View File
@@ -52,7 +52,7 @@ const data = {
},
{
title: 'Models',
url: '/model',
url: '/models',
icon: DatabaseIcon,
},
{
+5 -11
View File
@@ -78,23 +78,17 @@ export function DashboardBalanceSummary({
return (
<div className='grid grid-cols-2 gap-2.5 max-[359px]:grid-cols-1 sm:gap-3 lg:grid-cols-3'>
{cards.map((card) => (
<Card
key={card.title}
size='sm'
className='min-h-[6.25rem] sm:min-h-[7rem]'
>
<Card key={card.title} size='sm'>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-1'>
<CardTitle className='text-muted-foreground text-[11px] font-medium sm:text-sm'>
{card.title}
</CardTitle>
<div className='flex size-6 items-center justify-center sm:size-7'>
<card.icon
className={`h-3.5 w-3.5 sm:h-4 sm:w-4 ${card.color}`}
/>
</div>
<span className='inline-flex size-6 items-center justify-center sm:size-7'>
<card.icon className={`size-3.5 sm:size-4 ${card.color}`} />
</span>
</CardHeader>
<CardContent className='pt-0'>
<div className='text-base font-semibold break-words sm:text-xl'>
<div className='text-base font-semibold break-words tabular-nums sm:text-xl'>
{card.value}
</div>
</CardContent>
+27 -30
View File
@@ -107,10 +107,15 @@ export function DetailedWalletBalance({
return (
<>
<Card className='h-full w-full shadow-sm'>
<Card>
<CardHeader className='pb-4'>
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
<CardTitle className='text-xl'>Cashu Wallet Balance</CardTitle>
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
<div className='space-y-1.5'>
<CardTitle>Cashu Wallet Balance</CardTitle>
<CardDescription>
Detailed balance breakdown by mint and currency
</CardDescription>
</div>
<div className='flex w-full gap-2 sm:w-auto'>
<Button
variant='default'
@@ -127,7 +132,6 @@ export function DetailedWalletBalance({
size='icon'
onClick={() => refetch()}
disabled={isLoading || isFetching}
className='h-8 w-8'
>
<RefreshCw
className={cn(
@@ -139,19 +143,13 @@ export function DetailedWalletBalance({
</Button>
</div>
</div>
<CardDescription>
Detailed balance breakdown by mint and currency
</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className='space-y-4'>
<div className='grid gap-3 md:grid-cols-3'>
{Array.from({ length: 3 }).map((_, index) => (
<Card
key={`wallet-stat-skeleton-${index}`}
className='shadow-none'
>
<Card key={`wallet-stat-skeleton-${index}`}>
<CardHeader className='space-y-2 pb-1'>
<Skeleton className='h-3.5 w-28' />
<Skeleton className='h-3 w-8' />
@@ -182,47 +180,47 @@ export function DetailedWalletBalance({
) : (
<div className='space-y-6'>
<div className='grid gap-3 md:grid-cols-3'>
<Card className='shadow-none'>
<Card>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>
<CardTitle className='text-muted-foreground text-sm font-medium'>
Your Balance (Total)
</CardTitle>
<span className='inline-flex size-8 items-center justify-center text-green-600 dark:text-green-300'>
<Coins className='h-4 w-4' />
<span className='inline-flex size-8 items-center justify-center'>
<Coins className='size-4 text-green-600 dark:text-green-300' />
</span>
</CardHeader>
<CardContent>
<p className='text-primary text-2xl font-bold tracking-tight'>
<CardContent className='pt-0'>
<p className='text-primary text-2xl font-semibold tracking-tight tabular-nums'>
{formatAmount(totals.totalOwner)}
</p>
</CardContent>
</Card>
<Card className='shadow-none'>
<Card>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>
<CardTitle className='text-muted-foreground text-sm font-medium'>
Total Wallet
</CardTitle>
<span className='inline-flex size-8 items-center justify-center text-blue-600 dark:text-blue-300'>
<Wallet className='h-4 w-4' />
<span className='inline-flex size-8 items-center justify-center'>
<Wallet className='size-4 text-blue-600 dark:text-blue-300' />
</span>
</CardHeader>
<CardContent>
<p className='text-2xl font-bold tracking-tight'>
<CardContent className='pt-0'>
<p className='text-2xl font-semibold tracking-tight tabular-nums'>
{formatAmount(totals.totalWallet)}
</p>
</CardContent>
</Card>
<Card className='shadow-none'>
<Card>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>
<CardTitle className='text-muted-foreground text-sm font-medium'>
User Balance
</CardTitle>
<span className='inline-flex size-8 items-center justify-center text-purple-600 dark:text-purple-300'>
<User className='h-4 w-4' />
<span className='inline-flex size-8 items-center justify-center'>
<User className='size-4 text-purple-600 dark:text-purple-300' />
</span>
</CardHeader>
<CardContent>
<p className='text-2xl font-bold tracking-tight'>
<CardContent className='pt-0'>
<p className='text-2xl font-semibold tracking-tight tabular-nums'>
{formatAmount(totals.totalUser)}
</p>
</CardContent>
@@ -294,7 +292,6 @@ export function DetailedWalletBalance({
<Card
key={`${key}-mobile`}
className={cn(
'shadow-none',
detail.error &&
'border-destructive/40 bg-destructive/5'
)}
+2 -5
View File
@@ -4,7 +4,7 @@ import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { Loader2, Copy, Check, SendIcon } from 'lucide-react';
import { Loader2, Copy, Check } from 'lucide-react';
import { toast } from 'sonner';
import { useQueryClient } from '@tanstack/react-query';
import { QRCodeSVG } from 'qrcode.react';
@@ -101,10 +101,7 @@ export function EcashRedeem() {
return (
<Card className='h-full w-full shadow-sm'>
<CardHeader>
<div className='flex items-center space-x-2'>
<SendIcon className='text-primary h-5 w-5' />
<CardTitle>Send eCash</CardTitle>
</div>
<CardTitle>Send eCash</CardTitle>
<CardDescription>
Generate a token to send eCash to someone
</CardDescription>
+1 -4
View File
@@ -172,10 +172,7 @@ export function ApiKeyManager({
return (
<Card>
<CardHeader className='space-y-1'>
<CardTitle className='flex items-center gap-2 text-xl'>
<RefreshCcw className='text-primary h-5 w-5' />
API Key Management
</CardTitle>
<CardTitle className='text-xl'>API Key Management</CardTitle>
<p className='text-muted-foreground text-xs tracking-wide'>
Manage your existing API keys and balances
</p>
@@ -1,7 +1,7 @@
'use client';
import { type JSX, useCallback, useState } from 'react';
import { Copy, KeyRound, RefreshCcw, Trash2 } from 'lucide-react';
import { Copy, RefreshCcw, Trash2 } from 'lucide-react';
import { toast } from 'sonner';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -305,10 +305,7 @@ export function CashuPaymentWorkflow({
return (
<Card>
<CardHeader className='space-y-1'>
<CardTitle className='flex items-center gap-2 text-xl'>
<KeyRound className='text-primary h-5 w-5' />
API key workflow
</CardTitle>
<CardTitle className='text-xl'>API key workflow</CardTitle>
<p className='text-muted-foreground text-xs tracking-wide'>
Sections expand as soon as you interact
</p>
+3 -9
View File
@@ -2,7 +2,7 @@
import { type JSX, useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Bolt, Copy, RefreshCcw, ShieldCheck, Terminal } from 'lucide-react';
import { Bolt, Copy, RefreshCcw, Terminal } from 'lucide-react';
import { toast } from 'sonner';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -193,10 +193,7 @@ export function CheatSheet(): JSX.Element {
<Card>
<CardHeader className='flex flex-row items-start justify-between gap-4'>
<div>
<CardTitle className='flex items-center gap-2 text-lg'>
<ShieldCheck className='text-primary h-4 w-4' />
Node identity
</CardTitle>
<CardTitle className='text-lg'>Node identity</CardTitle>
<p className='text-muted-foreground text-xs tracking-wide'>
/v1/info snapshot
</p>
@@ -341,10 +338,7 @@ export function CheatSheet(): JSX.Element {
<Card>
<CardHeader className='flex flex-row items-center justify-between'>
<CardTitle className='flex items-center gap-2 text-lg'>
<Terminal className='text-primary h-4 w-4' />
Quick docs
</CardTitle>
<CardTitle className='text-lg'>Quick docs</CardTitle>
<span className='text-muted-foreground text-xs tracking-wide'>
curl-ready
</span>
+4 -18
View File
@@ -4,11 +4,7 @@ import { type JSX, useState, useCallback, useEffect } from 'react';
import {
Copy,
RefreshCcw,
ShieldCheck,
History,
Users,
RotateCcw,
KeyRound,
} from 'lucide-react';
import { toast } from 'sonner';
import {
@@ -154,10 +150,7 @@ export function KeyInfoDetails({
<div className='space-y-6'>
<Card>
<CardHeader className='space-y-1'>
<CardTitle className='flex items-center gap-2 text-xl'>
<KeyRound className='text-primary h-5 w-5' />
Key Information
</CardTitle>
<CardTitle className='text-xl'>Key Information</CardTitle>
<CardDescription>
Enter an API key to view its balance, consumption, and child keys.
</CardDescription>
@@ -202,10 +195,7 @@ export function KeyInfoDetails({
<div className='grid gap-4 md:grid-cols-2'>
<Card>
<CardHeader className='pb-2'>
<CardTitle className='flex items-center gap-2 text-lg'>
<ShieldCheck className='text-primary h-5 w-5' />
Status & Identity
</CardTitle>
<CardTitle className='text-lg'>Status & Identity</CardTitle>
</CardHeader>
<CardContent className='space-y-4'>
<div className='flex items-center justify-between'>
@@ -255,10 +245,7 @@ export function KeyInfoDetails({
<Card>
<CardHeader className='pb-2'>
<CardTitle className='flex items-center gap-2 text-lg'>
<History className='text-primary h-5 w-5' />
Consumption
</CardTitle>
<CardTitle className='text-lg'>Consumption</CardTitle>
</CardHeader>
<CardContent className='space-y-4'>
<div className='flex items-center justify-between'>
@@ -313,8 +300,7 @@ export function KeyInfoDetails({
walletInfo.childKeys.length > 0 && (
<Card>
<CardHeader>
<CardTitle className='flex items-center gap-2 text-lg'>
<Users className='text-primary h-5 w-5' />
<CardTitle className='text-lg'>
Child Keys ({walletInfo.childKeys.length})
</CardTitle>
<CardDescription>
@@ -2,7 +2,7 @@
import { type JSX, useCallback, useState } from 'react';
import Image from 'next/image';
import { Copy, Zap, CheckCircle, Loader2 } from 'lucide-react';
import { Copy, CheckCircle, Loader2 } from 'lucide-react';
import { toast } from 'sonner';
import QRCode from 'qrcode';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -514,10 +514,7 @@ export function LightningPaymentWorkflow({
return (
<Card>
<CardHeader className='space-y-1'>
<CardTitle className='flex items-center gap-2 text-xl'>
<Zap className='text-primary h-5 w-5' />
Lightning Payment Workflow
</CardTitle>
<CardTitle className='text-xl'>Lightning Payment Workflow</CardTitle>
<p className='text-muted-foreground text-xs tracking-wide'>
Create and manage API keys using Lightning Network payments
</p>
+1 -3
View File
@@ -25,7 +25,6 @@ import {
Key,
MoreVertical,
RefreshCw,
Users,
} from 'lucide-react';
interface ModelProviderSectionProps {
@@ -133,8 +132,7 @@ export function ModelProviderSection({
aria-label={`Select models for provider ${provider}`}
/>
<div className='min-w-0'>
<CardTitle className='flex items-center gap-1.5 text-base sm:gap-2 sm:text-lg'>
<Users className='h-5 w-5' />
<CardTitle className='flex items-baseline gap-1.5 text-base sm:text-lg'>
<span className='truncate'>{provider}</span>
<span className='text-muted-foreground text-sm font-normal'>
({providerModels.length} models)
+1 -4
View File
@@ -203,10 +203,7 @@ export function ModelTester({ models }: ModelTesterProps) {
return (
<Card className='w-full'>
<CardHeader>
<CardTitle className='flex items-center gap-2'>
<Send className='h-5 w-5' />
Model Credential Tester
</CardTitle>
<CardTitle>Model Credential Tester</CardTitle>
<CardDescription>
Test model functionality by sending chat completion requests through
the secure proxy (resolves CORS and network issues)
+4 -2
View File
@@ -173,9 +173,11 @@ export function ProviderBalance({
onClick={handleTopUpClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className='w-full font-mono sm:w-auto'
className='w-full sm:w-auto'
>
{isHovered ? 'Top Up' : displayValue}
<span className={isHovered ? undefined : 'font-mono'}>
{isHovered ? 'Top Up' : displayValue}
</span>
</Button>
<Dialog open={isTopupDialogOpen} onOpenChange={handleCloseDialog}>
+9 -11
View File
@@ -54,22 +54,20 @@ export function ProviderCard({
const hasDetails = Boolean(provider.api_version) || isExpanded;
return (
<Card size='sm' className='border-border/70 bg-card/55'>
<CardHeader className='pb-2'>
<Card>
<CardHeader>
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
<div className='min-w-0 flex-1'>
<div className='flex flex-col gap-2 sm:flex-row sm:items-center'>
<CardTitle className='truncate text-base sm:text-lg'>
{provider.provider_type}
</CardTitle>
<CardTitle className='truncate'>{provider.provider_type}</CardTitle>
<Badge
variant={provider.enabled ? 'default' : 'secondary'}
className='w-fit sm:ml-2'
className='w-fit'
>
{provider.enabled ? 'Enabled' : 'Disabled'}
</Badge>
</div>
<CardDescription className='mt-1 text-xs break-all sm:text-sm'>
<CardDescription className='break-all'>
{provider.base_url}
</CardDescription>
</div>
@@ -88,7 +86,7 @@ export function ProviderCard({
variant='outline'
size='sm'
onClick={onToggleExpansion}
className='col-span-2 h-9 justify-between px-3 sm:col-auto sm:h-8 sm:justify-center'
className='col-span-2 justify-between sm:col-auto sm:justify-center'
>
<span className='inline-flex items-center gap-1.5'>
<Database className='h-4 w-4' />
@@ -105,7 +103,7 @@ export function ProviderCard({
variant='outline'
size='sm'
onClick={onEditProvider}
className='h-9 justify-center gap-1.5 px-3 sm:h-8'
className='justify-center gap-1.5'
>
<Pencil className='h-4 w-4' />
<span>Edit</span>
@@ -115,7 +113,7 @@ export function ProviderCard({
variant='outline'
size='sm'
onClick={onDeleteProvider}
className='text-destructive hover:text-destructive h-9 justify-center gap-1.5 px-3 sm:h-8'
className='text-destructive hover:text-destructive justify-center gap-1.5'
>
<Trash2 className='h-4 w-4' />
<span>Delete</span>
@@ -125,7 +123,7 @@ export function ProviderCard({
</CardHeader>
{hasDetails ? (
<CardContent className='pt-1'>
<CardContent>
<div className='space-y-3'>
{provider.api_version && (
<div className='flex flex-col gap-1 text-sm sm:flex-row sm:items-center sm:justify-between'>
+1 -1
View File
@@ -23,7 +23,7 @@ const PAGE_META: Record<string, { title: string; description: string }> = {
title: 'System Logs',
description: 'Inspect request and application logs.',
},
'/model': {
'/models': {
title: 'Models',
description: 'Manage model catalog and provider mappings.',
},
+57 -71
View File
@@ -9,7 +9,6 @@ import {
Clock,
DollarSign,
Activity,
type LucideIcon,
} from 'lucide-react';
import { AdminService, TemporaryBalance } from '@/lib/api/services/admin';
import {
@@ -43,39 +42,6 @@ import { cn } from '@/lib/utils';
import type { DisplayUnit } from '@/lib/types/units';
import { formatFromMsat } from '@/lib/currency';
function TemporaryBalanceStat({
icon: Icon,
label,
value,
iconClassName,
}: {
icon: LucideIcon;
label: string;
value: string;
iconClassName: string;
}) {
return (
<Card className='shadow-none'>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>{label}</CardTitle>
<span
className={cn(
'inline-flex size-8 items-center justify-center rounded-full',
iconClassName
)}
>
<Icon className='h-4 w-4' />
</span>
</CardHeader>
<CardContent>
<p className='text-2xl font-bold tracking-tight tabular-nums'>
{value}
</p>
</CardContent>
</Card>
);
}
function getTotals(balances: TemporaryBalance[]) {
let totalBalance = 0;
let totalSpent = 0;
@@ -157,32 +123,32 @@ export function TemporaryBalances({
const rows = data ? buildHierarchicalData(data, filteredData) : [];
return (
<Card className='h-full w-full shadow-sm'>
<Card>
<CardHeader className='pb-4'>
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
<CardTitle className='flex items-center gap-2 text-xl'>
<Key className='h-5 w-5' />
Temporary Balances
</CardTitle>
<div className='flex w-full flex-col gap-2 sm:w-auto sm:flex-row'>
<div className='relative flex-1 sm:flex-initial'>
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
<div className='space-y-1.5'>
<CardTitle>Temporary Balances</CardTitle>
<CardDescription className='max-w-2xl'>
API keys with their current balances and usage statistics
</CardDescription>
</div>
<div className='flex w-full gap-2 sm:w-auto sm:pt-0.5'>
<div className='min-w-0 flex-1 sm:w-72 sm:flex-none'>
<Input
type='text'
placeholder='Search by key or address...'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className='pr-3 pl-8 sm:w-64'
name='temporary_balance_search'
autoComplete='off'
/>
<Key className='text-muted-foreground absolute top-2.5 left-2 h-4 w-4' />
</div>
<Button
variant='ghost'
size='icon'
onClick={() => refetch()}
disabled={isLoading || isFetching}
className='h-8 w-full sm:w-8'
>
<RefreshCw
className={cn(
@@ -194,19 +160,13 @@ export function TemporaryBalances({
</Button>
</div>
</div>
<CardDescription>
API keys with their current balances and usage statistics
</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className='space-y-4'>
<div className='grid gap-3 md:grid-cols-3'>
{Array.from({ length: 3 }).map((_, index) => (
<Card
key={`temp-stat-skeleton-${index}`}
className='shadow-none'
>
<Card key={`temp-stat-skeleton-${index}`}>
<CardHeader className='space-y-2 pb-1'>
<Skeleton className='h-3.5 w-24' />
<Skeleton className='h-3 w-8' />
@@ -236,24 +196,51 @@ export function TemporaryBalances({
) : (
<div className='space-y-6'>
<div className='grid gap-3 md:grid-cols-3'>
<TemporaryBalanceStat
icon={DollarSign}
label='Total Balance'
value={formatBalance(totals.totalBalance)}
iconClassName='text-green-600 dark:text-green-300'
/>
<TemporaryBalanceStat
icon={Activity}
label='Total Spent'
value={formatBalance(totals.totalSpent)}
iconClassName='text-blue-600 dark:text-blue-300'
/>
<TemporaryBalanceStat
icon={Key}
label='Total Requests'
value={totals.totalRequests.toLocaleString()}
iconClassName='text-purple-600 dark:text-purple-300'
/>
<Card>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-muted-foreground text-sm font-medium'>
Total Balance
</CardTitle>
<span className='inline-flex size-8 items-center justify-center'>
<DollarSign className='size-4 text-green-600 dark:text-green-300' />
</span>
</CardHeader>
<CardContent className='pt-0'>
<p className='text-2xl font-semibold tracking-tight tabular-nums'>
{formatBalance(totals.totalBalance)}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-muted-foreground text-sm font-medium'>
Total Spent
</CardTitle>
<span className='inline-flex size-8 items-center justify-center'>
<Activity className='size-4 text-blue-600 dark:text-blue-300' />
</span>
</CardHeader>
<CardContent className='pt-0'>
<p className='text-2xl font-semibold tracking-tight tabular-nums'>
{formatBalance(totals.totalSpent)}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-muted-foreground text-sm font-medium'>
Total Requests
</CardTitle>
<span className='inline-flex size-8 items-center justify-center'>
<Key className='size-4 text-purple-600 dark:text-purple-300' />
</span>
</CardHeader>
<CardContent className='pt-0'>
<p className='text-2xl font-semibold tracking-tight tabular-nums'>
{totals.totalRequests.toLocaleString()}
</p>
</CardContent>
</Card>
</div>
{rows.length > 0 ? (
@@ -343,7 +330,6 @@ export function TemporaryBalances({
<Card
key={`${balance.hashed_key}-${balance.parent_key_hash ?? 'root'}-mobile-${index}`}
className={cn(
'shadow-none',
balance.balance === 0 &&
!balance.isChild &&
'opacity-80',
+6 -8
View File
@@ -114,19 +114,17 @@ export function UsageSummaryCards({ summary }: UsageSummaryCardsProps) {
return (
<div className='grid grid-cols-1 gap-2.5 px-1 min-[380px]:grid-cols-2 sm:gap-4 sm:px-0 xl:grid-cols-4'>
{cards.map((card) => (
<Card key={card.title} className='hover:bg-muted/40 transition-colors'>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-1 sm:pb-2'>
<CardTitle className='text-muted-foreground text-xs font-medium sm:text-sm'>
<Card key={card.title} size='sm'>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-1'>
<CardTitle className='text-muted-foreground text-[11px] font-medium sm:text-sm'>
{card.title}
</CardTitle>
<span
className={`inline-flex size-6 items-center justify-center sm:size-8 ${card.iconClassName}`}
>
<card.icon className='h-3.5 w-3.5 sm:h-4 sm:w-4' />
<span className='inline-flex size-6 items-center justify-center sm:size-7'>
<card.icon className={`size-3.5 sm:size-4 ${card.iconClassName}`} />
</span>
</CardHeader>
<CardContent className='pt-0'>
<div className='text-lg font-semibold tracking-tight break-words sm:text-2xl'>
<div className='text-base font-semibold break-words tabular-nums sm:text-2xl'>
{card.value}
</div>
</CardContent>