Files
chaos-api/web/default/src/features/keys/components/api-keys-cells.tsx
T
t0ng7u d146e45e2f ⚖️ chore(web/default): add reusable copyright header tooling
Add a Bun script to apply and normalize AGPL copyright headers across the default frontend source files.

The script keeps headers idempotent, upgrades existing headers to the 2023-2026 QuantumNous range, and is exposed through `bun run copyright` for future maintenance.
2026-05-09 11:35:07 +08:00

216 lines
6.1 KiB
TypeScript
Vendored

/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useState, useCallback } from 'react'
import { Check, Copy, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { copyToClipboard } from '@/lib/copy-to-clipboard'
import { Button } from '@/components/ui/button'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { StatusBadge } from '@/components/status-badge'
import { type ApiKey } from '../types'
import { useApiKeys } from './api-keys-provider'
export function ApiKeyCell({ apiKey }: { apiKey: ApiKey }) {
const { t } = useTranslation()
const {
resolveRealKey,
resolvedKeys,
loadingKeys,
copiedKeyId,
markKeyCopied,
} = useApiKeys()
const [popoverOpen, setPopoverOpen] = useState(false)
const isLoading = !!loadingKeys[apiKey.id]
const resolvedFullKey = resolvedKeys[apiKey.id]
const isCopied = copiedKeyId === apiKey.id
const maskedKey = `sk-${apiKey.key}`
const handlePopoverOpen = useCallback(
(open: boolean) => {
setPopoverOpen(open)
if (open && !resolvedFullKey) {
resolveRealKey(apiKey.id)
}
},
[resolvedFullKey, resolveRealKey, apiKey.id]
)
const handleCopy = useCallback(async () => {
const realKey = resolvedFullKey || (await resolveRealKey(apiKey.id))
if (realKey) {
const ok = await copyToClipboard(realKey)
if (ok) markKeyCopied(apiKey.id)
}
}, [resolvedFullKey, resolveRealKey, apiKey.id, markKeyCopied])
return (
<div className='flex items-center'>
<Popover open={popoverOpen} onOpenChange={handlePopoverOpen}>
<PopoverTrigger
render={
<Button
variant='ghost'
size='sm'
className='text-muted-foreground h-7 font-mono text-xs'
/>
}
>
{maskedKey}
</PopoverTrigger>
<PopoverContent
className='w-auto max-w-[min(90vw,28rem)]'
align='start'
>
<div className='space-y-2'>
<p className='text-muted-foreground text-xs'>{t('Full API Key')}</p>
{isLoading ? (
<div className='flex items-center gap-2 py-2'>
<Loader2 className='size-3.5 animate-spin' />
<span className='text-muted-foreground text-xs'>
{t('Loading...')}
</span>
</div>
) : (
<input
readOnly
value={resolvedFullKey || maskedKey}
autoFocus
onFocus={(e) => e.target.select()}
className='bg-muted/50 w-full min-w-[280px] rounded-md border px-3 py-2 font-mono text-xs outline-none'
/>
)}
</div>
</PopoverContent>
</Popover>
<Tooltip>
<TooltipTrigger
render={
<Button
variant='ghost'
size='icon'
className='size-7 shrink-0'
onClick={handleCopy}
disabled={isLoading}
/>
}
>
{isLoading ? (
<Loader2 className='size-3.5 animate-spin' />
) : isCopied ? (
<Check className='size-3.5 text-green-600' />
) : (
<Copy className='size-3.5' />
)}
</TooltipTrigger>
<TooltipContent>
{isLoading
? t('Loading...')
: isCopied
? t('Copied!')
: t('Copy API key')}
</TooltipContent>
</Tooltip>
</div>
)
}
export function ModelLimitsCell({ apiKey }: { apiKey: ApiKey }) {
const { t } = useTranslation()
if (!apiKey.model_limits_enabled || !apiKey.model_limits) {
return (
<StatusBadge label={t('Unlimited')} variant='neutral' copyable={false} />
)
}
const models = apiKey.model_limits.split(',').filter(Boolean)
return (
<Tooltip>
<TooltipTrigger render={<span />}>
<StatusBadge
label={t('{{count}} model(s)', { count: models.length })}
variant='neutral'
copyable={false}
/>
</TooltipTrigger>
<TooltipContent side='top' className='max-w-xs'>
<div className='max-h-[200px] space-y-0.5 overflow-y-auto text-xs'>
{models.map((m) => (
<div key={m} className='font-mono'>
{m}
</div>
))}
</div>
</TooltipContent>
</Tooltip>
)
}
export function IpRestrictionsCell({ apiKey }: { apiKey: ApiKey }) {
const { t } = useTranslation()
const allowIps = apiKey.allow_ips?.trim()
if (!allowIps) {
return (
<StatusBadge
label={t('No restriction')}
variant='neutral'
copyable={false}
/>
)
}
const ips = allowIps
.split('\n')
.map((ip) => ip.trim())
.filter(Boolean)
return (
<Tooltip>
<TooltipTrigger render={<span />}>
<StatusBadge
label={t('{{count}} IP(s)', { count: ips.length })}
variant='neutral'
copyable={false}
/>
</TooltipTrigger>
<TooltipContent side='top' className='max-w-xs'>
<div className='max-h-[200px] space-y-0.5 overflow-y-auto text-xs'>
{ips.map((ip) => (
<div key={ip} className='font-mono'>
{ip}
</div>
))}
</div>
</TooltipContent>
</Tooltip>
)
}