♻️ refactor(web): consolidate design-system primitives and responsive data views
This commit is contained in:
@@ -25,7 +25,7 @@ import {
|
||||
IconLinuxDo,
|
||||
IconWeChat,
|
||||
} from '@/assets/brand-icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { useOAuthLogin } from '../hooks/use-oauth-login'
|
||||
@@ -159,7 +159,8 @@ export function OAuthProviders({
|
||||
type='button'
|
||||
disabled={disabled || isLoading || extraDisabled}
|
||||
onClick={onClick}
|
||||
className='h-11 w-full justify-center gap-2 rounded-lg'
|
||||
size='xl'
|
||||
className='w-full justify-center rounded-lg'
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
|
||||
+3
-2
@@ -24,8 +24,9 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Turnstile } from '@/components/turnstile'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { sendPasswordResetEmail } from '@/features/auth/api'
|
||||
import {
|
||||
forgotPasswordFormSchema,
|
||||
@@ -113,6 +113,7 @@ export function ForgotPasswordForm({
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
size='xl'
|
||||
className='mt-2'
|
||||
disabled={isLoading || isActive || !turnstileReady}
|
||||
>
|
||||
|
||||
@@ -24,7 +24,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
FormMessage,
|
||||
FormDescription,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
@@ -204,6 +204,7 @@ export function OtpForm({ className, ...props }: OtpFormProps) {
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
size='xl'
|
||||
className='mt-2 w-full'
|
||||
disabled={!isFormValid || isLoading}
|
||||
>
|
||||
@@ -215,8 +216,7 @@ export function OtpForm({ className, ...props }: OtpFormProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='link'
|
||||
size='sm'
|
||||
className='text-primary h-auto p-0'
|
||||
className='text-primary h-auto p-0 sm:h-auto'
|
||||
onClick={handleToggleMode}
|
||||
>
|
||||
{useBackupCode ? t('Use authenticator code') : t('Use backup code')}
|
||||
@@ -225,8 +225,7 @@ export function OtpForm({ className, ...props }: OtpFormProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='link'
|
||||
size='sm'
|
||||
className='text-primary h-auto p-0'
|
||||
className='text-primary h-auto p-0 sm:h-auto'
|
||||
onClick={handleBackToLogin}
|
||||
>
|
||||
{t('Back to login')}
|
||||
|
||||
@@ -22,9 +22,9 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useCountdown } from '@/hooks/use-countdown'
|
||||
import { api } from '@/lib/api'
|
||||
@@ -169,6 +169,7 @@ export function ResetPasswordConfirm({
|
||||
)}
|
||||
|
||||
<Button
|
||||
size='xl'
|
||||
className='w-full'
|
||||
onClick={
|
||||
newPassword
|
||||
|
||||
Vendored
+8
-3
@@ -20,10 +20,15 @@ import { ShieldCheck, KeyRound, Loader2 } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@/components/design-system/tabs'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
|
||||
import type {
|
||||
SecureVerificationState,
|
||||
|
||||
@@ -25,10 +25,11 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { PasswordInput } from '@/components/password-input'
|
||||
import { Turnstile } from '@/components/turnstile'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -37,7 +38,6 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { login, wechatLoginByCode } from '@/features/auth/api'
|
||||
import { LegalConsent } from '@/features/auth/components/legal-consent'
|
||||
@@ -292,7 +292,8 @@ export function UserAuthForm({
|
||||
variant='outline'
|
||||
disabled={passkeyButtonDisabled}
|
||||
onClick={handlePasskeyLogin}
|
||||
className='h-11 w-full justify-center gap-2 rounded-lg'
|
||||
size='xl'
|
||||
className='w-full justify-center rounded-lg'
|
||||
>
|
||||
{isPasskeyLoading ? (
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
@@ -375,7 +376,8 @@ export function UserAuthForm({
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type='submit'
|
||||
className='mt-2 w-full justify-center gap-2'
|
||||
size='xl'
|
||||
className='mt-2 w-full justify-center'
|
||||
disabled={isLoading || (requiresLegalConsent && !agreedToLegal)}
|
||||
>
|
||||
{isLoading ? <Loader2 className='animate-spin' /> : <LogIn />}
|
||||
|
||||
@@ -24,10 +24,11 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { PasswordInput } from '@/components/password-input'
|
||||
import { Turnstile } from '@/components/turnstile'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -36,7 +37,6 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { register, wechatLoginByCode } from '@/features/auth/api'
|
||||
import { LegalConsent } from '@/features/auth/components/legal-consent'
|
||||
@@ -355,7 +355,8 @@ export function SignUpForm({
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type='submit'
|
||||
className='mt-2 w-full justify-center gap-2'
|
||||
size='xl'
|
||||
className='mt-2 w-full justify-center'
|
||||
disabled={
|
||||
isLoading ||
|
||||
(requiresLegalConsent && !agreedToLegal) ||
|
||||
|
||||
+1
-1
@@ -376,7 +376,7 @@ export async function manageMultiKeys(
|
||||
export async function getMultiKeyStatus(
|
||||
channelId: number,
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
pageSize = 20,
|
||||
status?: number
|
||||
): Promise<MultiKeyStatusResponse> {
|
||||
return manageMultiKeys({
|
||||
|
||||
+100
-103
@@ -17,19 +17,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { flexRender, type Row } from '@tanstack/react-table'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { GroupBadge } from '@/components/group-badge'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
DataTableCardDetails,
|
||||
DataTableCardField,
|
||||
} from '@/components/data-table'
|
||||
|
||||
import { CHANNEL_STATUS } from '../constants'
|
||||
import { isTagAggregateRow, parseGroupsList } from '../lib'
|
||||
import { isTagAggregateRow } from '../lib'
|
||||
import type { Channel } from '../types'
|
||||
import { ChannelRowActionsLayoutContext } from './channel-row-actions-context'
|
||||
import { useChannels } from './channels-provider'
|
||||
|
||||
const SENSITIVE_MASK = '••••'
|
||||
|
||||
/**
|
||||
* Bespoke channel card for the card view. Reuses every column's existing cell
|
||||
@@ -37,7 +34,7 @@ const SENSITIVE_MASK = '••••'
|
||||
* preserved: row selection, provider/multi-key/IO.NET type badge, id,
|
||||
* name/remark + warning icons, status (with tooltips), groups, inline
|
||||
* priority/weight spinners, balance refresh, response/test times, tag
|
||||
* expand-collapse, and the per-row (or per-tag) actions menu.
|
||||
* expand-collapse, models, and the per-row (or per-tag) actions menu.
|
||||
*/
|
||||
function ChannelCardComponent({
|
||||
row,
|
||||
@@ -47,46 +44,40 @@ function ChannelCardComponent({
|
||||
isSelected: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { sensitiveVisible } = useChannels()
|
||||
const isTagRow = isTagAggregateRow(row.original)
|
||||
const cells = row.getAllCells()
|
||||
const cells = row.getVisibleCells()
|
||||
const visibleColumnIds = new Set(cells.map((cell) => cell.column.id))
|
||||
|
||||
const renderCell = (id: string) => {
|
||||
const cell = cells.find((c) => c.column.id === id)
|
||||
const cell = cells.find((candidate) => candidate.column.id === id)
|
||||
if (!cell || !cell.column.columnDef.cell) {
|
||||
return null
|
||||
}
|
||||
return flexRender(cell.column.columnDef.cell, cell.getContext())
|
||||
}
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
balance: t('Used / Remaining'),
|
||||
response_time: t('Response'),
|
||||
test_time: t('Last Tested'),
|
||||
}
|
||||
|
||||
const groups = parseGroupsList(row.original.group ?? '')
|
||||
|
||||
const selectCell = renderCell('select')
|
||||
const typeCell = renderCell('type')
|
||||
const idCell = renderCell('id')
|
||||
const nameCell = renderCell('name')
|
||||
const statusCell = renderCell('status')
|
||||
const actionsCell = renderCell('actions')
|
||||
const priorityCell = renderCell('priority')
|
||||
const weightCell = renderCell('weight')
|
||||
const balanceCell = renderCell('balance')
|
||||
const modelsCell = renderCell('models')
|
||||
const groupsCell = renderCell('group')
|
||||
const tagCell = renderCell('tag')
|
||||
const responseCell = renderCell('response_time')
|
||||
const testCell = renderCell('test_time')
|
||||
|
||||
const labelClass = 'text-muted-foreground text-xs font-medium select-none'
|
||||
|
||||
// In card view the enable/disable state is already conveyed by the inline
|
||||
// power toggle, so the plain "Enabled"/"Disabled" badge is redundant. Keep
|
||||
// only the informative states (e.g. auto-disabled, unknown) and tag rows.
|
||||
const showStatusBadge =
|
||||
isTagRow ||
|
||||
(row.original.status !== CHANNEL_STATUS.ENABLED &&
|
||||
row.original.status !== CHANNEL_STATUS.MANUAL_DISABLED)
|
||||
const emptyValue = <span className='text-muted-foreground'>-</span>
|
||||
const detailsCount = [
|
||||
visibleColumnIds.has('group'),
|
||||
!isTagRow && visibleColumnIds.has('tag'),
|
||||
visibleColumnIds.has('priority'),
|
||||
visibleColumnIds.has('weight'),
|
||||
].filter(Boolean).length
|
||||
|
||||
return (
|
||||
<ChannelRowActionsLayoutContext.Provider value='card'>
|
||||
@@ -94,93 +85,99 @@ function ChannelCardComponent({
|
||||
data-state={isSelected ? 'selected' : undefined}
|
||||
className='flex flex-col gap-3'
|
||||
>
|
||||
{/* Row 1: selection + type, with status badge + actions menu */}
|
||||
<div className='flex items-center justify-between gap-2'>
|
||||
{/* Provider identity, status, selection, and every row action remain
|
||||
immediately available. The wrapping layout avoids mobile clipping. */}
|
||||
<div className='flex flex-wrap items-start justify-between gap-2'>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-2'>
|
||||
{!isTagRow && selectCell && (
|
||||
<span className='shrink-0'>{selectCell}</span>
|
||||
)}
|
||||
<div className='min-w-0 overflow-hidden'>{typeCell}</div>
|
||||
{visibleColumnIds.has('type') && (
|
||||
<div className='min-w-0 flex-1'>{typeCell}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex shrink-0 items-center gap-1.5'>
|
||||
{showStatusBadge && statusCell}
|
||||
<div className='flex flex-wrap items-center justify-end gap-1.5'>
|
||||
{visibleColumnIds.has('status') && statusCell}
|
||||
{actionsCell}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body: left column (id/name + balance) paired with a right-aligned
|
||||
column (priority/weight + response/test time). */}
|
||||
<div className='flex items-start justify-between gap-3'>
|
||||
{/* Left column */}
|
||||
<div className='flex min-w-0 flex-1 flex-col gap-3 overflow-hidden'>
|
||||
<div className='min-w-0 text-sm'>
|
||||
{!isTagRow && (
|
||||
<div className={labelClass}>
|
||||
#{sensitiveVisible ? row.original.id : SENSITIVE_MASK}
|
||||
</div>
|
||||
)}
|
||||
{nameCell}
|
||||
</div>
|
||||
<div className='min-w-0'>
|
||||
<div className={cn('mb-1', labelClass)}>
|
||||
{fieldLabels.balance}
|
||||
</div>
|
||||
<div className='min-w-0 overflow-hidden text-sm'>
|
||||
{balanceCell ?? (
|
||||
<span className='text-muted-foreground'>-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right column (sits on the right, content left-aligned). A single
|
||||
grid with content-sized columns keeps Priority/Weight and
|
||||
Response/Last Tested aligned without wasting horizontal space. */}
|
||||
<div className='grid shrink-0 grid-cols-[auto_auto] items-center gap-x-3 gap-y-1'>
|
||||
<span className={labelClass}>{t('Priority')}</span>
|
||||
<span className={labelClass}>{t('Weight')}</span>
|
||||
<div className='flex justify-start'>{priorityCell}</div>
|
||||
<div className='flex justify-start'>{weightCell}</div>
|
||||
<span className={cn('mt-2', labelClass)}>
|
||||
{fieldLabels.response_time}
|
||||
</span>
|
||||
<span className={cn('mt-2', labelClass)}>
|
||||
{fieldLabels.test_time}
|
||||
</span>
|
||||
<div className='overflow-hidden text-sm'>
|
||||
{responseCell ?? <span className='text-muted-foreground'>-</span>}
|
||||
</div>
|
||||
<div className='overflow-hidden text-sm'>
|
||||
{testCell ?? <span className='text-muted-foreground'>-</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last row: groups span the full width, showing every group (no label) */}
|
||||
<div className='min-w-0'>
|
||||
{groups.length > 0 ? (
|
||||
<div className='flex flex-wrap gap-1'>
|
||||
{groups.map((g) => (
|
||||
<GroupBadge
|
||||
key={g}
|
||||
group={g}
|
||||
label={sensitiveVisible ? undefined : SENSITIVE_MASK}
|
||||
size='sm'
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className='text-muted-foreground text-sm'>-</span>
|
||||
<div className='grid grid-cols-2 gap-x-3 gap-y-2'>
|
||||
{visibleColumnIds.has('name') && (
|
||||
<DataTableCardField
|
||||
label={isTagRow ? t('Tag') : t('Name')}
|
||||
span={2}
|
||||
contentMode='wrap'
|
||||
>
|
||||
{nameCell ?? emptyValue}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{!isTagRow && visibleColumnIds.has('id') && (
|
||||
<DataTableCardField label={t('ID')} contentMode='full'>
|
||||
{idCell ?? emptyValue}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('balance') && (
|
||||
<DataTableCardField
|
||||
label={t('Used / Remaining')}
|
||||
span={2}
|
||||
contentMode='full'
|
||||
>
|
||||
{balanceCell ?? emptyValue}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{!isTagRow && visibleColumnIds.has('models') && (
|
||||
<DataTableCardField
|
||||
label={t('Models')}
|
||||
span={2}
|
||||
contentMode='summary'
|
||||
>
|
||||
{modelsCell ?? emptyValue}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('response_time') && (
|
||||
<DataTableCardField label={t('Response')} contentMode='full'>
|
||||
{responseCell ?? emptyValue}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{!isTagRow && visibleColumnIds.has('test_time') && (
|
||||
<DataTableCardField label={t('Last Tested')} contentMode='full'>
|
||||
{testCell ?? emptyValue}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{detailsCount > 0 && (
|
||||
<DataTableCardDetails count={detailsCount}>
|
||||
{visibleColumnIds.has('group') && (
|
||||
<DataTableCardField
|
||||
label={t('Groups')}
|
||||
span={2}
|
||||
contentMode='summary'
|
||||
>
|
||||
{groupsCell ?? emptyValue}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{!isTagRow && visibleColumnIds.has('tag') && (
|
||||
<DataTableCardField label={t('Tag')} span={2} contentMode='wrap'>
|
||||
{tagCell ?? emptyValue}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('priority') && (
|
||||
<DataTableCardField label={t('Priority')} contentMode='full'>
|
||||
{priorityCell ?? emptyValue}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('weight') && (
|
||||
<DataTableCardField label={t('Weight')} contentMode='full'>
|
||||
{weightCell ?? emptyValue}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
</DataTableCardDetails>
|
||||
)}
|
||||
</div>
|
||||
</ChannelRowActionsLayoutContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Memoized so each card only re-renders when its own react-table row reference
|
||||
* changes, instead of every card re-rendering whenever the parent table state
|
||||
* (filters, pagination, sensitive toggle, etc.) updates.
|
||||
*/
|
||||
export const ChannelCard = memo(ChannelCardComponent)
|
||||
export const ChannelCard = ChannelCardComponent
|
||||
|
||||
+132
-108
@@ -33,12 +33,12 @@ import { toast } from 'sonner'
|
||||
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { BadgeListCell } from '@/components/data-table'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { GroupBadge } from '@/components/group-badge'
|
||||
import { ProviderBadge } from '@/components/provider-badge'
|
||||
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
|
||||
import { TableId } from '@/components/table-id'
|
||||
import { TruncatedText } from '@/components/truncated-text'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -128,10 +128,9 @@ function UpstreamUpdateTags({ channel }: { channel: Channel }) {
|
||||
<div className='flex items-center gap-0.5'>
|
||||
{addCount > 0 && (
|
||||
<StatusBadge
|
||||
label={`+${addCount}`}
|
||||
variant='success'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
render={<button type='button' />}
|
||||
className='cursor-pointer'
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -143,14 +142,15 @@ function UpstreamUpdateTags({ channel }: { channel: Channel }) {
|
||||
'add'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{`+${addCount}`}
|
||||
</StatusBadge>
|
||||
)}
|
||||
{removeCount > 0 && (
|
||||
<StatusBadge
|
||||
label={`-${removeCount}`}
|
||||
variant='danger'
|
||||
variant='destructive'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
render={<button type='button' />}
|
||||
className='cursor-pointer'
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -162,7 +162,9 @@ function UpstreamUpdateTags({ channel }: { channel: Channel }) {
|
||||
'remove'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{`-${removeCount}`}
|
||||
</StatusBadge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -362,18 +364,11 @@ function BalanceCell({ channel }: { channel: Channel }) {
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<StatusBadge
|
||||
label={
|
||||
sensitiveVisible
|
||||
? `${t('Used:')} ${usedDisplay}`
|
||||
: maskedUsedLabel
|
||||
}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
showDot={false}
|
||||
className='cursor-help'
|
||||
/>
|
||||
<StatusBadge variant='neutral' size='sm' className='cursor-help'>
|
||||
{sensitiveVisible
|
||||
? `${t('Used:')} ${usedDisplay}`
|
||||
: maskedUsedLabel}
|
||||
</StatusBadge>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
@@ -435,18 +430,19 @@ function BalanceCell({ channel }: { channel: Channel }) {
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className='flex items-center gap-1'>
|
||||
<div
|
||||
className={
|
||||
layout === 'card'
|
||||
? 'flex flex-wrap items-center gap-1'
|
||||
: 'flex items-center gap-1'
|
||||
}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<StatusBadge
|
||||
label={sensitiveVisible ? usedDisplay : SENSITIVE_MASK}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
showDot={false}
|
||||
className='cursor-help'
|
||||
/>
|
||||
<StatusBadge variant='neutral' size='sm' className='cursor-help'>
|
||||
{sensitiveVisible ? usedDisplay : SENSITIVE_MASK}
|
||||
</StatusBadge>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
@@ -457,14 +453,14 @@ function BalanceCell({ channel }: { channel: Channel }) {
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<StatusBadge
|
||||
label={remainingBadgeLabel}
|
||||
variant={remainingBadgeVariant}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
showDot={false}
|
||||
render={<button type='button' />}
|
||||
className='cursor-pointer'
|
||||
onClick={handleClickUpdate}
|
||||
/>
|
||||
>
|
||||
{remainingBadgeLabel}
|
||||
</StatusBadge>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
@@ -570,7 +566,11 @@ export function useChannelsColumns(
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: t('ID'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 20,
|
||||
contentMode: 'full',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const id = row.getValue('id') as number
|
||||
return <TableId value={sensitiveVisible ? id : SENSITIVE_MASK} />
|
||||
@@ -581,7 +581,11 @@ export function useChannelsColumns(
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: t('Name'),
|
||||
meta: { mobileTitle: true },
|
||||
meta: {
|
||||
cardRole: 'title',
|
||||
cardSpan: 2,
|
||||
contentMode: 'wrap',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isTagRow = isTagAggregateRow(row.original)
|
||||
const name = row.getValue('name') as string
|
||||
@@ -596,8 +600,7 @@ export function useChannelsColumns(
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-6 w-6 p-0'
|
||||
size='icon-sm'
|
||||
onClick={row.getToggleExpandedHandler()}
|
||||
>
|
||||
{row.getIsExpanded() ? (
|
||||
@@ -608,12 +611,9 @@ export function useChannelsColumns(
|
||||
</Button>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<span className='font-semibold'>Tag:{tag}</span>
|
||||
<StatusBadge
|
||||
label={`${childrenCount} channels`}
|
||||
variant='blue'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral' size='sm'>
|
||||
{`${childrenCount} channels`}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -693,17 +693,20 @@ export function useChannelsColumns(
|
||||
{
|
||||
accessorKey: 'type',
|
||||
header: t('Type'),
|
||||
meta: {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 10,
|
||||
cardSpan: 2,
|
||||
contentMode: 'wrap',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isTagRow = isTagAggregateRow(row.original)
|
||||
|
||||
if (isTagRow) {
|
||||
return (
|
||||
<StatusBadge
|
||||
label={t('Tag Aggregate')}
|
||||
variant='blue'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral' size='sm'>
|
||||
{t('Tag Aggregate')}
|
||||
</StatusBadge>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -755,11 +758,7 @@ export function useChannelsColumns(
|
||||
>
|
||||
<ProviderBadge
|
||||
iconKey={`${iconName}.Color`}
|
||||
iconSize={18}
|
||||
label={typeName}
|
||||
colorText={false}
|
||||
copyable={false}
|
||||
showDot={false}
|
||||
className='max-w-full min-w-0 overflow-hidden'
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
@@ -771,8 +770,11 @@ export function useChannelsColumns(
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span
|
||||
className='flex cursor-pointer items-center gap-1.5 text-xs font-medium'
|
||||
<StatusBadge
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
render={<button type='button' />}
|
||||
className='cursor-pointer'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (!deploymentId) {
|
||||
@@ -784,13 +786,7 @@ export function useChannelsColumns(
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StatusBadge
|
||||
label='IO.NET'
|
||||
variant='purple'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='cursor-pointer'
|
||||
/>
|
||||
IO.NET
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top'>
|
||||
<div className='max-w-xs space-y-1'>
|
||||
@@ -827,7 +823,7 @@ export function useChannelsColumns(
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: t('Status'),
|
||||
meta: { mobileBadge: true },
|
||||
meta: { cardRole: 'badge', contentMode: 'wrap' },
|
||||
cell: ({ row }) => {
|
||||
const isTagRow = isTagAggregateRow(row.original)
|
||||
const status = row.getValue('status') as number
|
||||
@@ -840,21 +836,15 @@ export function useChannelsColumns(
|
||||
|
||||
if (hasEnabled) {
|
||||
return (
|
||||
<StatusBadge
|
||||
label={`Active (${childrenCount})`}
|
||||
variant='success'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='success' size='sm'>
|
||||
{`Active (${childrenCount})`}
|
||||
</StatusBadge>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<StatusBadge
|
||||
label={`Inactive (${childrenCount})`}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral' size='sm'>
|
||||
{`Inactive (${childrenCount})`}
|
||||
</StatusBadge>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -899,12 +889,9 @@ export function useChannelsColumns(
|
||||
<TooltipProvider delay={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span />}>
|
||||
<StatusBadge
|
||||
label={label}
|
||||
variant={config.variant}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant={config.variant} size='sm'>
|
||||
{label}
|
||||
</StatusBadge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top' className='max-w-xs'>
|
||||
<div className='space-y-1 text-xs'>
|
||||
@@ -927,12 +914,9 @@ export function useChannelsColumns(
|
||||
}
|
||||
|
||||
return (
|
||||
<StatusBadge
|
||||
label={label}
|
||||
variant={config.variant}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant={config.variant} size='sm'>
|
||||
{label}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
@@ -956,7 +940,12 @@ export function useChannelsColumns(
|
||||
{
|
||||
accessorKey: 'models',
|
||||
header: t('Models'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 40,
|
||||
cardSpan: 2,
|
||||
contentMode: 'summary',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const models = row.getValue('models') as string
|
||||
const modelArray = parseModelsList(models)
|
||||
@@ -965,11 +954,12 @@ export function useChannelsColumns(
|
||||
items={modelArray.map((model) => (
|
||||
<StatusBadge
|
||||
key={model}
|
||||
label={model}
|
||||
autoColor={model}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
className='font-mono'
|
||||
/>
|
||||
title={model}
|
||||
>
|
||||
{model}
|
||||
</StatusBadge>
|
||||
))}
|
||||
/>
|
||||
)
|
||||
@@ -982,7 +972,12 @@ export function useChannelsColumns(
|
||||
{
|
||||
accessorKey: 'group',
|
||||
header: t('Groups'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 10,
|
||||
cardSpan: 2,
|
||||
contentMode: 'summary',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const group = row.getValue('group') as string
|
||||
const groupArray = parseGroupsList(group)
|
||||
@@ -994,6 +989,7 @@ export function useChannelsColumns(
|
||||
group={g}
|
||||
label={sensitiveVisible ? undefined : SENSITIVE_MASK}
|
||||
size='sm'
|
||||
title={sensitiveVisible ? g : undefined}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
@@ -1015,14 +1011,23 @@ export function useChannelsColumns(
|
||||
{
|
||||
accessorKey: 'tag',
|
||||
header: t('Tag'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 20,
|
||||
cardSpan: 2,
|
||||
contentMode: 'wrap',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const tag = row.getValue('tag') as string | null
|
||||
if (!tag) {
|
||||
return <span className='text-muted-foreground text-xs'>-</span>
|
||||
}
|
||||
|
||||
return <StatusBadge label={tag} autoColor={tag} size='sm' />
|
||||
return (
|
||||
<StatusBadge variant='neutral' size='sm' title={tag}>
|
||||
{tag}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
size: 120,
|
||||
enableSorting: false,
|
||||
@@ -1032,7 +1037,11 @@ export function useChannelsColumns(
|
||||
{
|
||||
accessorKey: 'priority',
|
||||
header: t('Priority'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 30,
|
||||
contentMode: 'full',
|
||||
},
|
||||
cell: ({ row }) => <PriorityCell channel={row.original} />,
|
||||
size: 100,
|
||||
},
|
||||
@@ -1041,7 +1050,11 @@ export function useChannelsColumns(
|
||||
{
|
||||
accessorKey: 'weight',
|
||||
header: t('Weight'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 40,
|
||||
contentMode: 'full',
|
||||
},
|
||||
cell: ({ row }) => <WeightCell channel={row.original} />,
|
||||
size: 90,
|
||||
enableSorting: false,
|
||||
@@ -1051,6 +1064,12 @@ export function useChannelsColumns(
|
||||
{
|
||||
accessorKey: 'balance',
|
||||
header: t('Used / Remaining'),
|
||||
meta: {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 30,
|
||||
cardSpan: 2,
|
||||
contentMode: 'full',
|
||||
},
|
||||
cell: ({ row }) => <BalanceCell channel={row.original} />,
|
||||
size: 180,
|
||||
},
|
||||
@@ -1059,18 +1078,19 @@ export function useChannelsColumns(
|
||||
{
|
||||
accessorKey: 'response_time',
|
||||
header: t('Response'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 50,
|
||||
contentMode: 'full',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const responseTime = row.getValue('response_time') as number
|
||||
const config = getResponseTimeConfig(responseTime)
|
||||
|
||||
return (
|
||||
<StatusBadge
|
||||
label={formatResponseTime(responseTime, t)}
|
||||
variant={config.variant}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant={config.variant} size='sm'>
|
||||
{formatResponseTime(responseTime, t)}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
size: 110,
|
||||
@@ -1080,7 +1100,11 @@ export function useChannelsColumns(
|
||||
{
|
||||
accessorKey: 'test_time',
|
||||
header: t('Last Tested'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 60,
|
||||
contentMode: 'full',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const testTime = row.getValue('test_time') as number
|
||||
|
||||
@@ -1099,12 +1123,12 @@ export function useChannelsColumns(
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<StatusBadge
|
||||
label={timeText}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='cursor-pointer'
|
||||
/>
|
||||
>
|
||||
{timeText}
|
||||
</StatusBadge>
|
||||
}
|
||||
/>
|
||||
<TooltipContent side='top'>
|
||||
|
||||
@@ -34,7 +34,7 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -156,7 +156,6 @@ export function ChannelsPrimaryButtons() {
|
||||
setCurrentRow(null)
|
||||
setOpen('create-channel')
|
||||
}}
|
||||
size='sm'
|
||||
disabled={!canEditSensitive}
|
||||
>
|
||||
<Plus className='h-4 w-4' />
|
||||
@@ -173,7 +172,7 @@ export function ChannelsPrimaryButtons() {
|
||||
|
||||
{/* More Actions */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger render={<Button variant='outline' size='sm' />}>
|
||||
<DropdownMenuTrigger render={<Button variant='outline' />}>
|
||||
<MoreHorizontal className='h-4 w-4' />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-56'>
|
||||
|
||||
@@ -35,8 +35,8 @@ import {
|
||||
useDebouncedColumnFilter,
|
||||
useDataTable,
|
||||
} from '@/components/data-table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -114,7 +114,8 @@ export function ChannelsTable() {
|
||||
navigate: route.useNavigate(),
|
||||
pagination: {
|
||||
defaultPage: 1,
|
||||
defaultPageSize: isMobile ? 10 : DEFAULT_PAGE_SIZE,
|
||||
defaultPageSize: DEFAULT_PAGE_SIZE,
|
||||
pageSizeStorageKey: 'channels:page-size:v1',
|
||||
},
|
||||
globalFilter: { enabled: true, key: 'filter' },
|
||||
columnFilters: [
|
||||
@@ -312,10 +313,6 @@ export function ChannelsTable() {
|
||||
columns,
|
||||
totalCount,
|
||||
sorting,
|
||||
initialColumnVisibility: {
|
||||
models: false,
|
||||
tag: false,
|
||||
},
|
||||
columnVisibilityStorageKey: CHANNELS_COLUMN_VISIBILITY_STORAGE_KEY,
|
||||
columnSizingStorageKey: isMobile
|
||||
? false
|
||||
@@ -409,6 +406,7 @@ export function ChannelsTable() {
|
||||
<DataTablePage
|
||||
table={table}
|
||||
columns={columns}
|
||||
tableLabel={t('Channels')}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
emptyTitle={t('No Channels Found')}
|
||||
@@ -426,12 +424,14 @@ export function ChannelsTable() {
|
||||
toolbarProps={{
|
||||
searchPlaceholder: t('Filter by name, ID, or key...'),
|
||||
searchDebounceMs: 500,
|
||||
hasAdditionalFilters: Boolean(modelFilterInput.trim()),
|
||||
onReset: () => {
|
||||
resetModelFilterInput()
|
||||
},
|
||||
additionalSearch: (
|
||||
<Input
|
||||
placeholder={t('Filter by model...')}
|
||||
aria-label={t('Filter by model...')}
|
||||
value={modelFilterInput}
|
||||
onChange={onModelFilterInputChange}
|
||||
onCompositionStart={onModelFilterCompositionStart}
|
||||
@@ -468,7 +468,7 @@ export function ChannelsTable() {
|
||||
size='icon'
|
||||
onClick={() => setSensitiveVisible(!sensitiveVisible)}
|
||||
aria-label={sensitiveVisible ? t('Hide') : t('Show')}
|
||||
className='text-muted-foreground hover:text-foreground size-8'
|
||||
className='text-muted-foreground hover:text-foreground'
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -23,9 +23,9 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -116,7 +116,6 @@ export function DataTableBulkActions<TData>({
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={handleEnableAll}
|
||||
className='size-8'
|
||||
aria-label={t('Enable selected channels')}
|
||||
title={t('Enable selected channels')}
|
||||
/>
|
||||
@@ -137,7 +136,6 @@ export function DataTableBulkActions<TData>({
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={handleDisableAll}
|
||||
className='size-8'
|
||||
aria-label={t('Disable selected channels')}
|
||||
title={t('Disable selected channels')}
|
||||
/>
|
||||
@@ -158,7 +156,6 @@ export function DataTableBulkActions<TData>({
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={() => setShowTagDialog(true)}
|
||||
className='size-8'
|
||||
aria-label={t('Set tag for selected channels')}
|
||||
title={t('Set tag for selected channels')}
|
||||
/>
|
||||
@@ -186,7 +183,6 @@ export function DataTableBulkActions<TData>({
|
||||
}}
|
||||
aria-disabled={!canEditSensitive}
|
||||
className={cn(
|
||||
'size-8',
|
||||
!canEditSensitive && 'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
aria-label={t('Delete selected channels')}
|
||||
|
||||
@@ -38,7 +38,7 @@ import { useContext, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -255,8 +255,9 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
size='icon-sm'
|
||||
variant='ghost'
|
||||
className='data-popup-open:bg-muted flex h-8 w-8 p-0'
|
||||
className='data-popup-open:bg-muted'
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ import { Power, PowerOff, Pencil, Edit } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { DataTableRowActionMenu } from '@/components/data-table/core/row-action-menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
|
||||
+8
-22
@@ -21,10 +21,8 @@ import { type ReactNode, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -32,7 +30,9 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
} from '@/components/design-system/select'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
@@ -316,7 +316,6 @@ export function AdvancedCustomEditorDialog({
|
||||
<Button
|
||||
type='button'
|
||||
variant={editMode === 'visual' ? 'default' : 'outline'}
|
||||
size='sm'
|
||||
onClick={switchToVisualMode}
|
||||
>
|
||||
{t('Visual')}
|
||||
@@ -324,7 +323,6 @@ export function AdvancedCustomEditorDialog({
|
||||
<Button
|
||||
type='button'
|
||||
variant={editMode === 'json' ? 'default' : 'outline'}
|
||||
size='sm'
|
||||
onClick={switchToJsonMode}
|
||||
>
|
||||
{t('JSON Text')}
|
||||
@@ -343,7 +341,7 @@ export function AdvancedCustomEditorDialog({
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='h-8 max-w-full min-w-[260px] flex-1 sm:w-[320px]'>
|
||||
<SelectTrigger className='max-w-full min-w-[260px] flex-1 sm:w-[320px]'>
|
||||
<SelectValue className='min-w-0 truncate'>
|
||||
{t(templateLabel)}
|
||||
</SelectValue>
|
||||
@@ -370,7 +368,6 @@ export function AdvancedCustomEditorDialog({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => applyTemplate('fill')}
|
||||
>
|
||||
{t('Fill Template')}
|
||||
@@ -378,7 +375,6 @@ export function AdvancedCustomEditorDialog({
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => applyTemplate('append')}
|
||||
>
|
||||
{t('Append Template')}
|
||||
@@ -389,12 +385,7 @@ export function AdvancedCustomEditorDialog({
|
||||
{editMode === 'visual' ? (
|
||||
<div className='flex flex-col gap-4 p-4 lg:gap-3'>
|
||||
<div className='flex justify-end border-y py-4 lg:py-2'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={addRoute}
|
||||
>
|
||||
<Button type='button' variant='outline' onClick={addRoute}>
|
||||
<Plus className='mr-2 h-4 w-4' />
|
||||
{t('Add route')}
|
||||
</Button>
|
||||
@@ -443,12 +434,7 @@ export function AdvancedCustomEditorDialog({
|
||||
) : (
|
||||
<div className='p-4'>
|
||||
<div className='mb-2 flex items-center gap-2'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={formatJson}
|
||||
>
|
||||
<Button type='button' variant='outline' onClick={formatJson}>
|
||||
{t('Format')}
|
||||
</Button>
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
|
||||
+1
-1
@@ -22,8 +22,8 @@ import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { formatCurrencyFromUSD } from '@/lib/currency'
|
||||
import { formatTimestampToDate } from '@/lib/format'
|
||||
|
||||
|
||||
+19
-33
@@ -50,6 +50,16 @@ import {
|
||||
DataTableView,
|
||||
useDataTable,
|
||||
} from '@/components/data-table'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/design-system/select'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import {
|
||||
sideDrawerContentClassName,
|
||||
@@ -58,18 +68,8 @@ import {
|
||||
sideDrawerHeaderClassName,
|
||||
} from '@/components/drawer-layout'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -353,7 +353,7 @@ function ChannelTestDialogContent({
|
||||
useState<FailureDetailsState | null>(null)
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 30,
|
||||
pageSize: 20,
|
||||
})
|
||||
const endpointSelectItems = useMemo(
|
||||
() =>
|
||||
@@ -412,7 +412,7 @@ function ChannelTestDialogContent({
|
||||
setIsDeleteFailedDialogOpen(false)
|
||||
setIsDeletingFailed(false)
|
||||
setFailureDetails(null)
|
||||
setPagination({ pageIndex: 0, pageSize: 30 })
|
||||
setPagination({ pageIndex: 0, pageSize: 20 })
|
||||
}, [])
|
||||
|
||||
const streamDisabled = STREAM_INCOMPATIBLE_ENDPOINTS.has(endpointType)
|
||||
@@ -876,12 +876,9 @@ function ChannelTestDialogContent({
|
||||
{model}
|
||||
</span>
|
||||
{isDefault && (
|
||||
<StatusBadge
|
||||
label={t('Default')}
|
||||
variant='info'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='info' size='sm'>
|
||||
{t('Default')}
|
||||
</StatusBadge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -1062,7 +1059,6 @@ function ChannelTestDialogContent({
|
||||
{isBatchTesting ? (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleStopBatchTest}
|
||||
disabled={isBatchStopRequested}
|
||||
>
|
||||
@@ -1073,7 +1069,6 @@ function ChannelTestDialogContent({
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={() => handleBatchTest(filteredModels)}
|
||||
disabled={isAnyTesting || filteredModels.length === 0}
|
||||
>
|
||||
@@ -1082,7 +1077,6 @@ function ChannelTestDialogContent({
|
||||
{successModels.length > 0 && (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleSelectSuccessfulModels}
|
||||
>
|
||||
<CheckCircle2 data-icon='inline-start' />
|
||||
@@ -1094,7 +1088,6 @@ function ChannelTestDialogContent({
|
||||
{failedModels.length > 0 && (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => setIsDeleteFailedDialogOpen(true)}
|
||||
>
|
||||
<Trash2 data-icon='inline-start' />
|
||||
@@ -1190,14 +1183,12 @@ function TestStatusCell({ result }: { result?: TestResult }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!result || result.status === 'idle') {
|
||||
return (
|
||||
<StatusBadge label={t('Not tested')} variant='neutral' copyable={false} />
|
||||
)
|
||||
return <StatusBadge variant='neutral'>{t('Not tested')}</StatusBadge>
|
||||
}
|
||||
|
||||
if (result.status === 'testing') {
|
||||
return (
|
||||
<StatusBadge variant='info' copyable={false}>
|
||||
<StatusBadge variant='info'>
|
||||
<Loader2 className='size-3.5 shrink-0 animate-spin' />
|
||||
<span className='min-w-0 truncate leading-normal'>
|
||||
{t('Testing...')}
|
||||
@@ -1207,12 +1198,10 @@ function TestStatusCell({ result }: { result?: TestResult }) {
|
||||
}
|
||||
|
||||
if (result.status === 'success') {
|
||||
return (
|
||||
<StatusBadge label={t('Success')} variant='success' copyable={false} />
|
||||
)
|
||||
return <StatusBadge variant='success'>{t('Success')}</StatusBadge>
|
||||
}
|
||||
|
||||
return <StatusBadge label={t('Failed')} variant='danger' copyable={false} />
|
||||
return <StatusBadge variant='destructive'>{t('Failed')}</StatusBadge>
|
||||
}
|
||||
|
||||
function TestResultCell({
|
||||
@@ -1290,7 +1279,6 @@ function FailureResultContent({
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='h-7 w-fit px-2 text-xs'
|
||||
onClick={() =>
|
||||
window.open('/system-settings/billing/model-pricing', '_blank')
|
||||
}
|
||||
@@ -1303,7 +1291,6 @@ function FailureResultContent({
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-7 w-fit px-2 text-xs'
|
||||
aria-haspopup='dialog'
|
||||
onClick={() => onOpenDetails({ model, summary, details })}
|
||||
>
|
||||
@@ -1407,7 +1394,6 @@ function TestModelsBulkActions({ table }: { table: TanStackTable<ModelRow> }) {
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={handleCopySelected}
|
||||
disabled={selectedModels.length === 0}
|
||||
/>
|
||||
|
||||
+36
-57
@@ -29,14 +29,10 @@ import { type ReactNode, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import {
|
||||
StatusBadge,
|
||||
textColorMap,
|
||||
type StatusBadgeProps,
|
||||
} from '@/components/status-badge'
|
||||
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
@@ -332,8 +328,8 @@ const PLAN_TYPE_BADGE: Record<
|
||||
> = {
|
||||
enterprise: { label: 'Enterprise', variant: 'success' },
|
||||
team: { label: 'Team', variant: 'info' },
|
||||
pro: { label: 'Pro', variant: 'blue' },
|
||||
plus: { label: 'Plus', variant: 'purple' },
|
||||
pro: { label: 'Pro', variant: 'neutral' },
|
||||
plus: { label: 'Plus', variant: 'neutral' },
|
||||
free: { label: 'Free', variant: 'warning' },
|
||||
}
|
||||
|
||||
@@ -376,7 +372,7 @@ function windowLabel(windowData?: CodexRateLimitWindow | null) {
|
||||
const percent = clampPercent(windowData?.used_percent)
|
||||
let variant: StatusBadgeProps['variant'] = 'info'
|
||||
if (percent >= 95) {
|
||||
variant = 'danger'
|
||||
variant = 'destructive'
|
||||
} else if (percent >= 80) {
|
||||
variant = 'warning'
|
||||
}
|
||||
@@ -388,27 +384,28 @@ function getUsageStatusBadge(
|
||||
t: (key: string) => string
|
||||
) {
|
||||
if (!rateLimit || Object.keys(rateLimit).length === 0) {
|
||||
return (
|
||||
<StatusBadge label={t('Pending')} variant='neutral' copyable={false} />
|
||||
)
|
||||
return <StatusBadge variant='neutral'>{t('Pending')}</StatusBadge>
|
||||
}
|
||||
if (rateLimit.allowed && !rateLimit.limit_reached) {
|
||||
return (
|
||||
<StatusBadge label={t('Available')} variant='success' copyable={false} />
|
||||
)
|
||||
return <StatusBadge variant='success'>{t('Available')}</StatusBadge>
|
||||
}
|
||||
return <StatusBadge label={t('Limited')} variant='danger' copyable={false} />
|
||||
return <StatusBadge variant='destructive'>{t('Limited')}</StatusBadge>
|
||||
}
|
||||
|
||||
function formatLabelValue(label: string, value: string) {
|
||||
return label.endsWith(':') ? `${label}${value}` : `${label} ${value}`
|
||||
}
|
||||
|
||||
// Single source of truth for variant → text color lives in status-badge.
|
||||
const percentTextClassName: Record<
|
||||
NonNullable<StatusBadgeProps['variant']>,
|
||||
string
|
||||
> = textColorMap
|
||||
> = {
|
||||
neutral: 'text-muted-foreground',
|
||||
info: 'text-status-info',
|
||||
success: 'text-status-success',
|
||||
warning: 'text-status-warning',
|
||||
destructive: 'text-status-destructive',
|
||||
}
|
||||
|
||||
type RateLimitWindowProps = {
|
||||
title: string
|
||||
@@ -648,11 +645,9 @@ function ResetCreditItem(props: { credit: CodexResetCredit; index: number }) {
|
||||
<div className='min-w-0 text-sm font-medium break-words'>
|
||||
{title}
|
||||
</div>
|
||||
<StatusBadge
|
||||
label={t(statusBadge.label)}
|
||||
variant={statusBadge.variant}
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant={statusBadge.variant}>
|
||||
{t(statusBadge.label)}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
{props.credit.description ? (
|
||||
<div className='text-muted-foreground mt-1 text-xs leading-5'>
|
||||
@@ -801,7 +796,6 @@ function ResetCreditsPanel(props: {
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={props.onRefresh}
|
||||
disabled={props.isLoading}
|
||||
>
|
||||
@@ -822,7 +816,6 @@ function ResetCreditsPanel(props: {
|
||||
<Button
|
||||
type='button'
|
||||
variant={canReset ? 'destructive' : 'outline'}
|
||||
size='sm'
|
||||
onClick={props.onRequestReset}
|
||||
disabled={!canReset || props.isLoading || props.isResetting}
|
||||
className='shrink-0'
|
||||
@@ -1079,7 +1072,6 @@ export function CodexUsageDialog({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={onRefresh}
|
||||
disabled={Boolean(isRefreshing)}
|
||||
>
|
||||
@@ -1091,35 +1083,25 @@ export function CodexUsageDialog({
|
||||
</CardHeader>
|
||||
<CardContent className='p-4 pt-0'>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<StatusBadge
|
||||
label={accountBadge.label}
|
||||
variant={accountBadge.variant}
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant={accountBadge.variant}>
|
||||
{accountBadge.label}
|
||||
</StatusBadge>
|
||||
{getUsageStatusBadge(rateLimit, t)}
|
||||
<StatusBadge
|
||||
label={`HTTP ${response?.upstream_status ?? '-'}`}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge
|
||||
label={formatLabelValue(t('Reset count:'), resetCreditsText)}
|
||||
variant={Number(resetCredits) > 0 ? 'blue' : 'neutral'}
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral'>
|
||||
{`HTTP ${response?.upstream_status ?? '-'}`}
|
||||
</StatusBadge>
|
||||
<StatusBadge variant='neutral'>
|
||||
{formatLabelValue(t('Reset count:'), resetCreditsText)}
|
||||
</StatusBadge>
|
||||
{payload?.credits?.overage_limit_reached ? (
|
||||
<StatusBadge
|
||||
label={t('Overage limited')}
|
||||
variant='danger'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='destructive'>
|
||||
{t('Overage limited')}
|
||||
</StatusBadge>
|
||||
) : null}
|
||||
{payload?.spend_control?.reached ? (
|
||||
<StatusBadge
|
||||
label={t('Spend limited')}
|
||||
variant='danger'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='destructive'>
|
||||
{t('Spend limited')}
|
||||
</StatusBadge>
|
||||
) : null}
|
||||
</div>
|
||||
<div className='mt-4 grid grid-cols-1 gap-3 md:grid-cols-2'>
|
||||
@@ -1158,11 +1140,9 @@ export function CodexUsageDialog({
|
||||
<div className='text-sm font-semibold'>
|
||||
{t('Reset Credits')}
|
||||
</div>
|
||||
<StatusBadge
|
||||
label={`${t('Available')} ${resetCreditsText}`}
|
||||
variant={Number(resetCredits) > 0 ? 'blue' : 'neutral'}
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral'>
|
||||
{`${t('Available')} ${resetCreditsText}`}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<div className='text-muted-foreground mt-1 text-xs leading-5'>
|
||||
{t('View issued reset credits, grant dates, and expiration.')}
|
||||
@@ -1262,7 +1242,6 @@ export function CodexUsageDialog({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => copyToClipboard(rawJsonText)}
|
||||
disabled={!rawJsonText}
|
||||
>
|
||||
|
||||
+2
-2
@@ -21,10 +21,10 @@ import { Loader2 } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
import { handleCopyChannel } from '../../lib'
|
||||
|
||||
@@ -17,18 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { Loader2, X } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { GroupBadge } from '@/components/group-badge'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -36,7 +31,12 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
} from '@/components/design-system/select'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { GroupBadge } from '@/components/group-badge'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
@@ -285,15 +285,16 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) {
|
||||
<div className='flex min-h-[60px] flex-wrap gap-2 rounded-md border p-3'>
|
||||
{selectedModels.length > 0 ? (
|
||||
selectedModels.map((model) => (
|
||||
<StatusBadge
|
||||
<Badge
|
||||
key={model}
|
||||
variant='neutral'
|
||||
className='cursor-pointer transition-opacity hover:opacity-70'
|
||||
copyable={false}
|
||||
variant='secondary'
|
||||
render={<button type='button' />}
|
||||
onClick={() => handleRemoveModel(model)}
|
||||
aria-label={`${t('Remove')}: ${model}`}
|
||||
>
|
||||
{model} ×
|
||||
</StatusBadge>
|
||||
{model}
|
||||
<X data-icon='inline-end' aria-hidden='true' />
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className='text-muted-foreground text-sm'>
|
||||
@@ -304,12 +305,10 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) {
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<Select<string>
|
||||
items={[
|
||||
...availableModels.map((model) => ({
|
||||
value: model,
|
||||
label: model,
|
||||
})),
|
||||
]}
|
||||
items={availableModels.map((model) => ({
|
||||
value: model,
|
||||
label: model,
|
||||
}))}
|
||||
onValueChange={(value) => {
|
||||
if (value === null) return
|
||||
if (!selectedModels.includes(value)) {
|
||||
@@ -382,7 +381,6 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
setModelMapping(
|
||||
JSON.stringify(
|
||||
@@ -398,7 +396,6 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => setModelMapping(JSON.stringify({}, null, 2))}
|
||||
>
|
||||
{t('Clear Mapping')}
|
||||
@@ -406,7 +403,6 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => setModelMapping('')}
|
||||
>
|
||||
{t('No Change')}
|
||||
|
||||
+8
-3
@@ -22,17 +22,22 @@ import { useState, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@/components/design-system/tabs'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
|
||||
Vendored
+2
-2
@@ -27,8 +27,8 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
} from '@/components/design-system/alert-dialog'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
|
||||
export type MissingModelsAction = 'cancel' | 'submit' | 'add'
|
||||
|
||||
|
||||
+22
-43
@@ -24,9 +24,7 @@ import { toast } from 'sonner'
|
||||
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { StaticDataTable } from '@/components/data-table'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -34,7 +32,9 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
} from '@/components/design-system/select'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
ADMIN_PERMISSION_ACTIONS,
|
||||
@@ -88,7 +88,7 @@ export function MultiKeyManageDialog({
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [keys, setKeys] = useState<KeyStatus[]>([])
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [totalPages, setTotalPages] = useState(0)
|
||||
const [enabledCount, setEnabledCount] = useState(0)
|
||||
@@ -131,7 +131,7 @@ export function MultiKeyManageDialog({
|
||||
setKeys(response.data.keys || [])
|
||||
setTotal(response.data.total || 0)
|
||||
setCurrentPage(response.data.page || 1)
|
||||
setPageSize(response.data.page_size || 10)
|
||||
setPageSize(response.data.page_size || 20)
|
||||
setTotalPages(response.data.total_pages || 0)
|
||||
setEnabledCount(response.data.enabled_count || 0)
|
||||
setManualDisabledCount(response.data.manual_disabled_count || 0)
|
||||
@@ -218,14 +218,7 @@ export function MultiKeyManageDialog({
|
||||
|
||||
const renderStatusBadge = (status: number) => {
|
||||
const config = getMultiKeyStatusConfig(status)
|
||||
return (
|
||||
<StatusBadge
|
||||
label={t(config.label)}
|
||||
variant={config.variant}
|
||||
showDot
|
||||
copyable={false}
|
||||
/>
|
||||
)
|
||||
return <StatusBadge variant={config.variant}>{t(config.label)}</StatusBadge>
|
||||
}
|
||||
|
||||
const formatKeyTimestamp = (timestamp?: number) => {
|
||||
@@ -243,21 +236,13 @@ export function MultiKeyManageDialog({
|
||||
title={
|
||||
<>
|
||||
{t('Multi-Key Management')}
|
||||
<StatusBadge
|
||||
label={currentRow.name}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral'>{currentRow.name}</StatusBadge>
|
||||
{currentRow.channel_info?.multi_key_mode && (
|
||||
<StatusBadge
|
||||
label={
|
||||
currentRow.channel_info.multi_key_mode === 'random'
|
||||
? t('Random')
|
||||
: t('Polling')
|
||||
}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral'>
|
||||
{currentRow.channel_info.multi_key_mode === 'random'
|
||||
? t('Random')
|
||||
: t('Polling')}
|
||||
</StatusBadge>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
@@ -294,12 +279,10 @@ export function MultiKeyManageDialog({
|
||||
{/* Toolbar */}
|
||||
<div className='flex shrink-0 items-center justify-between'>
|
||||
<Select
|
||||
items={[
|
||||
...MULTI_KEY_FILTER_OPTIONS.map((option) => ({
|
||||
value: option.value,
|
||||
label: t(option.label),
|
||||
})),
|
||||
]}
|
||||
items={MULTI_KEY_FILTER_OPTIONS.map((option) => ({
|
||||
value: option.value,
|
||||
label: t(option.label),
|
||||
}))}
|
||||
value={statusFilter === null ? 'all' : statusFilter.toString()}
|
||||
onValueChange={(v) => v !== null && handleStatusFilterChange(v)}
|
||||
>
|
||||
@@ -320,7 +303,6 @@ export function MultiKeyManageDialog({
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => loadKeyStatus()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
@@ -330,7 +312,6 @@ export function MultiKeyManageDialog({
|
||||
{manualDisabledCount + autoDisabledCount > 0 && (
|
||||
<Button
|
||||
variant='default'
|
||||
size='sm'
|
||||
onClick={() => setConfirmAction({ type: 'enable-all' })}
|
||||
>
|
||||
<Power className='mr-2 h-4 w-4' />
|
||||
@@ -341,7 +322,6 @@ export function MultiKeyManageDialog({
|
||||
{enabledCount > 0 && (
|
||||
<Button
|
||||
variant='destructive'
|
||||
size='sm'
|
||||
onClick={() => setConfirmAction({ type: 'disable-all' })}
|
||||
>
|
||||
<PowerOff className='mr-2 h-4 w-4' />
|
||||
@@ -352,7 +332,6 @@ export function MultiKeyManageDialog({
|
||||
{autoDisabledCount > 0 && (
|
||||
<Button
|
||||
variant='destructive'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
if (!canEditSensitive) return
|
||||
setConfirmAction({ type: 'delete-disabled' })
|
||||
@@ -378,15 +357,17 @@ export function MultiKeyManageDialog({
|
||||
|
||||
{/* Table */}
|
||||
<div className='min-h-0 flex-1 overflow-auto rounded-md border'>
|
||||
{isLoading ? (
|
||||
{isLoading && (
|
||||
<div className='flex items-center justify-center py-12'>
|
||||
<Loader2 className='text-muted-foreground h-8 w-8 animate-spin' />
|
||||
</div>
|
||||
) : keys.length === 0 ? (
|
||||
)}
|
||||
{!isLoading && keys.length === 0 && (
|
||||
<div className='text-muted-foreground py-12 text-center'>
|
||||
{t('No keys found')}
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
{!isLoading && keys.length > 0 && (
|
||||
<StaticDataTable
|
||||
className='rounded-none border-0'
|
||||
tableClassName='min-w-[800px]'
|
||||
@@ -450,7 +431,6 @@ export function MultiKeyManageDialog({
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1 || isLoading}
|
||||
>
|
||||
@@ -458,7 +438,6 @@ export function MultiKeyManageDialog({
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages || isLoading}
|
||||
>
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
|
||||
import type { MultiKeyConfirmAction } from '../../types'
|
||||
|
||||
|
||||
+8
-9
@@ -22,7 +22,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -32,10 +31,11 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
} from '@/components/design-system/alert-dialog'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
@@ -484,14 +484,13 @@ export function OllamaModelsDialog({
|
||||
</div>
|
||||
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button variant='outline' size='sm' onClick={selectAllFiltered}>
|
||||
<Button variant='outline' onClick={selectAllFiltered}>
|
||||
{t('Select all (filtered)')}
|
||||
</Button>
|
||||
<Button variant='outline' size='sm' onClick={clearSelection}>
|
||||
<Button variant='outline' onClick={clearSelection}>
|
||||
{t('Clear selection')}
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={() => void applySelection('append')}
|
||||
disabled={!selected.length}
|
||||
>
|
||||
@@ -499,7 +498,6 @@ export function OllamaModelsDialog({
|
||||
</Button>
|
||||
<Button
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
onClick={() => void applySelection('replace')}
|
||||
disabled={!selected.length}
|
||||
>
|
||||
@@ -547,7 +545,8 @@ export function OllamaModelsDialog({
|
||||
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
size='icon-sm'
|
||||
aria-label={t('Delete')}
|
||||
className='text-destructive hover:text-destructive'
|
||||
onClick={() => {
|
||||
setDeleteTarget(m.id)
|
||||
|
||||
+40
-87
@@ -36,16 +36,8 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -53,7 +45,15 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
} from '@/components/design-system/select'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -1725,7 +1725,6 @@ export function ParamOverrideEditorDialog(
|
||||
<Button
|
||||
type='button'
|
||||
variant={editMode === 'visual' ? 'default' : 'outline'}
|
||||
size='sm'
|
||||
onClick={switchToVisualMode}
|
||||
>
|
||||
{t('Visual')}
|
||||
@@ -1733,7 +1732,6 @@ export function ParamOverrideEditorDialog(
|
||||
<Button
|
||||
type='button'
|
||||
variant={editMode === 'json' ? 'default' : 'outline'}
|
||||
size='sm'
|
||||
onClick={switchToJsonMode}
|
||||
>
|
||||
{t('JSON Text')}
|
||||
@@ -1756,7 +1754,7 @@ export function ParamOverrideEditorDialog(
|
||||
setTemplatePresetKey(v || 'operations_default')
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='h-8 w-[220px]'>
|
||||
<SelectTrigger className='w-[220px]'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
@@ -1772,7 +1770,6 @@ export function ParamOverrideEditorDialog(
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => fillTemplate('fill')}
|
||||
>
|
||||
{t('Fill Template')}
|
||||
@@ -1780,17 +1777,11 @@ export function ParamOverrideEditorDialog(
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => fillTemplate('append')}
|
||||
>
|
||||
{t('Append Template')}
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={resetEditorState}
|
||||
>
|
||||
<Button type='button' variant='ghost' onClick={resetEditorState}>
|
||||
{t('Reset')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1830,7 +1821,7 @@ export function ParamOverrideEditorDialog(
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
size='icon-sm'
|
||||
onClick={addOperation}
|
||||
>
|
||||
<Plus className='h-4 w-4' />
|
||||
@@ -1860,7 +1851,7 @@ export function ParamOverrideEditorDialog(
|
||||
value={operationSearch}
|
||||
onChange={(e) => setOperationSearch(e.target.value)}
|
||||
placeholder={t('Search rules...')}
|
||||
className='h-8 pl-8 text-xs'
|
||||
className='pl-8 text-xs'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2012,12 +2003,7 @@ export function ParamOverrideEditorDialog(
|
||||
/* JSON mode */
|
||||
<div className='p-4'>
|
||||
<div className='mb-2 flex items-center gap-2'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={formatJson}
|
||||
>
|
||||
<Button type='button' variant='outline' onClick={formatJson}>
|
||||
{t('Format')}
|
||||
</Button>
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
@@ -2121,7 +2107,6 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => ruleEditorProps.duplicateOperation(operation.id)}
|
||||
>
|
||||
<Copy className='mr-1 h-3.5 w-3.5' />
|
||||
@@ -2130,7 +2115,6 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='text-destructive hover:text-destructive'
|
||||
onClick={() => ruleEditorProps.removeOperation(operation.id)}
|
||||
>
|
||||
@@ -2159,7 +2143,7 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='h-9'>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
@@ -2186,7 +2170,7 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
||||
})
|
||||
}
|
||||
placeholder={getModePathPlaceholder(mode)}
|
||||
className='h-9'
|
||||
className=''
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -2220,7 +2204,7 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
||||
'e.g. Clean tool parameters to avoid upstream validation errors'
|
||||
)}
|
||||
maxLength={180}
|
||||
className='h-9'
|
||||
className=''
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2251,8 +2235,7 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='text-muted-foreground h-auto px-1.5 py-0.5 text-xs'
|
||||
className='text-muted-foreground h-auto px-1.5 py-0.5 text-xs sm:h-auto'
|
||||
onClick={() => {
|
||||
try {
|
||||
const parsed = JSON.parse(operation.value_text)
|
||||
@@ -2322,7 +2305,7 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
||||
})
|
||||
}
|
||||
placeholder={getModeFromPlaceholder(mode)}
|
||||
className='h-9'
|
||||
className=''
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -2339,7 +2322,7 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
||||
})
|
||||
}
|
||||
placeholder={getModeToPlaceholder(mode)}
|
||||
className='h-9'
|
||||
className=''
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -2364,7 +2347,7 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='h-7 w-[120px] text-xs'>
|
||||
<SelectTrigger className='w-[120px]'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
@@ -2381,8 +2364,6 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-7 text-xs'
|
||||
onClick={ruleEditorProps.expandAllConditions}
|
||||
>
|
||||
<ChevronDown className='mr-1 h-3 w-3' />
|
||||
@@ -2391,8 +2372,6 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-7 text-xs'
|
||||
onClick={ruleEditorProps.collapseAllConditions}
|
||||
>
|
||||
<ChevronUp className='mr-1 h-3 w-3' />
|
||||
@@ -2403,8 +2382,6 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='h-7 text-xs'
|
||||
onClick={() => ruleEditorProps.addCondition(operation.id)}
|
||||
>
|
||||
<Plus className='mr-1 h-3 w-3' />
|
||||
@@ -2498,8 +2475,7 @@ function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='text-destructive hover:text-destructive h-7 text-xs'
|
||||
className='text-destructive hover:text-destructive'
|
||||
onClick={() =>
|
||||
conditionEditorProps.removeCondition(
|
||||
conditionEditorProps.operationId,
|
||||
@@ -2524,7 +2500,7 @@ function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
|
||||
)
|
||||
}
|
||||
placeholder='model'
|
||||
className='h-8 text-xs'
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
@@ -2546,7 +2522,7 @@ function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='h-8 text-xs'>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
@@ -2574,7 +2550,7 @@ function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
|
||||
)
|
||||
}
|
||||
placeholder='gpt'
|
||||
className='h-8 text-xs'
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2641,8 +2617,6 @@ function ReturnErrorEditor(returnErrorEditorProps: ReturnErrorEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant={draft.simpleMode ? 'default' : 'outline'}
|
||||
size='sm'
|
||||
className='h-7 text-xs'
|
||||
onClick={() =>
|
||||
returnErrorEditorProps.updateDraft(
|
||||
returnErrorEditorProps.operationId,
|
||||
@@ -2655,8 +2629,6 @@ function ReturnErrorEditor(returnErrorEditorProps: ReturnErrorEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant={draft.simpleMode ? 'outline' : 'default'}
|
||||
size='sm'
|
||||
className='h-7 text-xs'
|
||||
onClick={() =>
|
||||
returnErrorEditorProps.updateDraft(
|
||||
returnErrorEditorProps.operationId,
|
||||
@@ -2707,7 +2679,7 @@ function ReturnErrorEditor(returnErrorEditorProps: ReturnErrorEditorProps) {
|
||||
)
|
||||
}
|
||||
placeholder='400'
|
||||
className='h-8 text-xs'
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
@@ -2723,7 +2695,7 @@ function ReturnErrorEditor(returnErrorEditorProps: ReturnErrorEditorProps) {
|
||||
)
|
||||
}
|
||||
placeholder='forced_bad_request'
|
||||
className='h-8 text-xs'
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
@@ -2739,7 +2711,7 @@ function ReturnErrorEditor(returnErrorEditorProps: ReturnErrorEditorProps) {
|
||||
)
|
||||
}
|
||||
placeholder='invalid_request_error'
|
||||
className='h-8 text-xs'
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2750,8 +2722,6 @@ function ReturnErrorEditor(returnErrorEditorProps: ReturnErrorEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant={draft.skipRetry ? 'default' : 'outline'}
|
||||
size='sm'
|
||||
className='h-7 text-xs'
|
||||
onClick={() =>
|
||||
returnErrorEditorProps.updateDraft(
|
||||
returnErrorEditorProps.operationId,
|
||||
@@ -2764,8 +2734,6 @@ function ReturnErrorEditor(returnErrorEditorProps: ReturnErrorEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant={draft.skipRetry ? 'outline' : 'default'}
|
||||
size='sm'
|
||||
className='h-7 text-xs'
|
||||
onClick={() =>
|
||||
returnErrorEditorProps.updateDraft(
|
||||
returnErrorEditorProps.operationId,
|
||||
@@ -2801,8 +2769,6 @@ function ReturnErrorEditor(returnErrorEditorProps: ReturnErrorEditorProps) {
|
||||
key={preset.code}
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='h-6 text-xs'
|
||||
onClick={() =>
|
||||
returnErrorEditorProps.updateDraft(
|
||||
returnErrorEditorProps.operationId,
|
||||
@@ -2859,8 +2825,6 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant={draft.simpleMode ? 'default' : 'outline'}
|
||||
size='sm'
|
||||
className='h-7 text-xs'
|
||||
onClick={() =>
|
||||
pruneObjectsEditorProps.updateDraft(
|
||||
pruneObjectsEditorProps.operationId,
|
||||
@@ -2873,8 +2837,6 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant={draft.simpleMode ? 'outline' : 'default'}
|
||||
size='sm'
|
||||
className='h-7 text-xs'
|
||||
onClick={() =>
|
||||
pruneObjectsEditorProps.updateDraft(
|
||||
pruneObjectsEditorProps.operationId,
|
||||
@@ -2898,7 +2860,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
||||
)
|
||||
}
|
||||
placeholder='redacted_thinking'
|
||||
className='h-8 text-xs'
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2924,7 +2886,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='h-8 text-xs'>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
@@ -2945,8 +2907,6 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant={draft.recursive ? 'default' : 'outline'}
|
||||
size='sm'
|
||||
className='h-8 text-xs'
|
||||
onClick={() =>
|
||||
pruneObjectsEditorProps.updateDraft(
|
||||
pruneObjectsEditorProps.operationId,
|
||||
@@ -2959,8 +2919,6 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant={draft.recursive ? 'outline' : 'default'}
|
||||
size='sm'
|
||||
className='h-8 text-xs'
|
||||
onClick={() =>
|
||||
pruneObjectsEditorProps.updateDraft(
|
||||
pruneObjectsEditorProps.operationId,
|
||||
@@ -2982,8 +2940,6 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='h-7 text-xs'
|
||||
onClick={() =>
|
||||
pruneObjectsEditorProps.addRule(
|
||||
pruneObjectsEditorProps.operationId
|
||||
@@ -3014,8 +2970,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='text-destructive hover:text-destructive h-6 text-xs'
|
||||
className='text-destructive hover:text-destructive'
|
||||
onClick={() =>
|
||||
pruneObjectsEditorProps.removeRule(
|
||||
pruneObjectsEditorProps.operationId,
|
||||
@@ -3042,7 +2997,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
||||
)
|
||||
}
|
||||
placeholder='type'
|
||||
className='h-7 text-xs'
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-0.5'>
|
||||
@@ -3066,7 +3021,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='h-7 text-xs'>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
@@ -3094,7 +3049,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
||||
)
|
||||
}
|
||||
placeholder='redacted_thinking'
|
||||
className='h-7 text-xs'
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3181,7 +3136,7 @@ function SyncFieldsEditor(syncFieldsEditorProps: SyncFieldsEditorProps) {
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='h-8 w-[110px] text-xs'>
|
||||
<SelectTrigger className='w-[110px]'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
@@ -3208,7 +3163,7 @@ function SyncFieldsEditor(syncFieldsEditorProps: SyncFieldsEditorProps) {
|
||||
)
|
||||
}
|
||||
placeholder='session_id'
|
||||
className='h-8 text-xs'
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3236,7 +3191,7 @@ function SyncFieldsEditor(syncFieldsEditorProps: SyncFieldsEditorProps) {
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='h-8 w-[110px] text-xs'>
|
||||
<SelectTrigger className='w-[110px]'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
@@ -3263,7 +3218,7 @@ function SyncFieldsEditor(syncFieldsEditorProps: SyncFieldsEditorProps) {
|
||||
)
|
||||
}
|
||||
placeholder='prompt_cache_key'
|
||||
className='h-8 text-xs'
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3285,8 +3240,6 @@ function SyncFieldsEditor(syncFieldsEditorProps: SyncFieldsEditorProps) {
|
||||
key={preset.label}
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='h-6 text-xs'
|
||||
onClick={() =>
|
||||
syncFieldsEditorProps.updateOperation(
|
||||
syncFieldsEditorProps.operationId,
|
||||
|
||||
+2
-2
@@ -20,10 +20,10 @@ import { AlertTriangle } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
interface StatusCodeRiskDialogProps {
|
||||
|
||||
+2
-2
@@ -22,11 +22,11 @@ import { useState, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { MultiSelect } from '@/components/multi-select'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
+14
-9
@@ -21,13 +21,18 @@ import { useState, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@/components/design-system/tabs'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
|
||||
interface UpstreamUpdateDialogProps {
|
||||
open: boolean
|
||||
@@ -97,8 +102,8 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
|
||||
const handleConfirm = () => {
|
||||
const hasAdd = props.addModels.length > 0
|
||||
const hasRemove = props.removeModels.length > 0
|
||||
const selectedAddArr = Array.from(selectedAdd)
|
||||
const selectedRemoveArr = Array.from(selectedRemove)
|
||||
const selectedAddArr = [...selectedAdd]
|
||||
const selectedRemoveArr = [...selectedRemove]
|
||||
const anyAdd = selectedAddArr.length > 0
|
||||
const anyRemove = selectedRemoveArr.length > 0
|
||||
|
||||
@@ -152,13 +157,13 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
|
||||
<TabsList className='grid w-full grid-cols-2'>
|
||||
<TabsTrigger value='add' className='gap-1'>
|
||||
{t('Add Models')}
|
||||
<StatusBadge variant='neutral' className='ml-1' copyable={false}>
|
||||
<StatusBadge variant='neutral' className='ml-1'>
|
||||
{selectedAdd.size}/{props.addModels.length}
|
||||
</StatusBadge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value='remove' className='gap-1'>
|
||||
{t('Remove Models')}
|
||||
<StatusBadge variant='neutral' className='ml-1' copyable={false}>
|
||||
<StatusBadge variant='neutral' className='ml-1'>
|
||||
{selectedRemove.size}/{props.removeModels.length}
|
||||
</StatusBadge>
|
||||
</TabsTrigger>
|
||||
@@ -282,8 +287,8 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
|
||||
handleConfirm={() => {
|
||||
setPartialConfirmOpen(false)
|
||||
props.onConfirm({
|
||||
addModels: Array.from(selectedAdd),
|
||||
removeModels: Array.from(selectedRemove),
|
||||
addModels: [...selectedAdd],
|
||||
removeModels: [...selectedRemove],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
+12
-38
@@ -55,6 +55,17 @@ import { type SubmitErrorHandler, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Combobox } from '@/components/design-system/combobox'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/design-system/select'
|
||||
import {
|
||||
sideDrawerContentClassName,
|
||||
sideDrawerFooterClassName,
|
||||
@@ -67,8 +78,6 @@ import { JsonEditor } from '@/components/json-editor'
|
||||
import { MultiSelect } from '@/components/multi-select'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Combobox } from '@/components/ui/combobox'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -78,15 +87,6 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
Sheet,
|
||||
@@ -1847,7 +1847,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='shrink-0'
|
||||
onClick={pasteConnectionInfoFromClipboard}
|
||||
>
|
||||
@@ -1878,7 +1877,6 @@ export function ChannelMutateDrawer({
|
||||
<span className='flex shrink-0 gap-2'>
|
||||
<Button
|
||||
type='button'
|
||||
size='sm'
|
||||
onClick={() => applyConnectionInfo(clipboardConnectionInfo)}
|
||||
>
|
||||
{t('Fill in')}
|
||||
@@ -1886,7 +1884,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => setClipboardConnectionInfo(null)}
|
||||
>
|
||||
{t('Ignore')}
|
||||
@@ -2806,7 +2803,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
setAdvancedCustomEditorOpen(true)
|
||||
}
|
||||
@@ -2843,10 +2839,7 @@ export function ChannelMutateDrawer({
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger
|
||||
size='sm'
|
||||
className='w-full sm:w-56'
|
||||
>
|
||||
<SelectTrigger className='w-full sm:w-56'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
@@ -2960,7 +2953,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleDeduplicateKeys}
|
||||
className='w-fit'
|
||||
>
|
||||
@@ -2987,7 +2979,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleRevealKey}
|
||||
disabled={
|
||||
isChannelKeyLoading ||
|
||||
@@ -3005,7 +2996,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={async () => {
|
||||
if (channelKey) {
|
||||
await copyToClipboard(
|
||||
@@ -3049,7 +3039,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleRefreshCodexCredential}
|
||||
disabled={
|
||||
sensitiveLocked ||
|
||||
@@ -3258,7 +3247,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
const hiddenTargets = new Set(
|
||||
modelMappingGuardrail.exposedTargetModels
|
||||
@@ -3298,7 +3286,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleFillRelatedModels}
|
||||
disabled={!basicModels.length}
|
||||
>
|
||||
@@ -3311,7 +3298,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleFillAllModels}
|
||||
disabled={!allModelsList.length}
|
||||
>
|
||||
@@ -3326,7 +3312,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleFetchModels}
|
||||
disabled={!isEditing && !canEditSensitive}
|
||||
>
|
||||
@@ -3348,7 +3333,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleCopyModels}
|
||||
disabled={currentModelsArray.length === 0}
|
||||
>
|
||||
@@ -3361,7 +3345,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={handleClearModels}
|
||||
disabled={currentModelsArray.length === 0}
|
||||
>
|
||||
@@ -3382,7 +3365,6 @@ export function ChannelMutateDrawer({
|
||||
key={group.id}
|
||||
type='button'
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
handleAddPrefillGroup(group)
|
||||
}
|
||||
@@ -3515,7 +3497,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
updateModels([
|
||||
...currentModelsArray,
|
||||
@@ -3825,7 +3806,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
setParamOverrideEditorOpen(true)
|
||||
}
|
||||
@@ -3836,7 +3816,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
field.onChange(
|
||||
JSON.stringify(
|
||||
@@ -3869,7 +3848,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => field.onChange('')}
|
||||
>
|
||||
{t('Clear')}
|
||||
@@ -3913,7 +3891,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
field.onChange(
|
||||
JSON.stringify(
|
||||
@@ -3936,7 +3913,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
field.onChange(
|
||||
JSON.stringify(
|
||||
@@ -3952,7 +3928,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
@@ -3971,7 +3946,6 @@ export function ChannelMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => field.onChange('')}
|
||||
>
|
||||
{t('Clear')}
|
||||
|
||||
@@ -20,10 +20,15 @@ import { Code, Plus, Table, Trash2 } from 'lucide-react'
|
||||
import { useEffect, useId, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@/components/design-system/tabs'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -237,8 +242,7 @@ export function ModelMappingEditor(props: ModelMappingEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='link'
|
||||
size='sm'
|
||||
className='h-auto p-0'
|
||||
className='h-auto p-0 sm:h-auto'
|
||||
onClick={handleFillTemplate}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
@@ -299,7 +303,6 @@ export function ModelMappingEditor(props: ModelMappingEditorProps) {
|
||||
size='icon'
|
||||
onClick={() => handleDeleteRow(row.id)}
|
||||
disabled={props.disabled}
|
||||
className='h-10 w-10'
|
||||
aria-label={t('Delete mapping')}
|
||||
>
|
||||
<Trash2 className='h-4 w-4' aria-hidden='true' />
|
||||
@@ -317,7 +320,6 @@ export function ModelMappingEditor(props: ModelMappingEditorProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleAddRow}
|
||||
disabled={props.disabled}
|
||||
className='w-full'
|
||||
|
||||
+3
-3
@@ -138,7 +138,7 @@ export const CHANNEL_STATUS_CONFIG = {
|
||||
label: 'Enabled',
|
||||
},
|
||||
[CHANNEL_STATUS.MANUAL_DISABLED]: {
|
||||
variant: 'danger' as const,
|
||||
variant: 'destructive' as const,
|
||||
label: 'Disabled',
|
||||
},
|
||||
[CHANNEL_STATUS.AUTO_DISABLED]: {
|
||||
@@ -173,7 +173,7 @@ export const MULTI_KEY_STATUS_CONFIG = {
|
||||
label: 'Manual Disabled',
|
||||
},
|
||||
[MULTI_KEY_STATUS.AUTO_DISABLED]: {
|
||||
variant: 'danger' as const,
|
||||
variant: 'destructive' as const,
|
||||
label: 'Auto Disabled',
|
||||
},
|
||||
}
|
||||
@@ -324,7 +324,7 @@ export const RESPONSE_TIME_CONFIG = {
|
||||
EXCELLENT: { variant: 'success' as const, label: 'Excellent' },
|
||||
GOOD: { variant: 'success' as const, label: 'Good' },
|
||||
FAIR: { variant: 'warning' as const, label: 'Fair' },
|
||||
POOR: { variant: 'danger' as const, label: 'Poor' },
|
||||
POOR: { variant: 'destructive' as const, label: 'Poor' },
|
||||
UNKNOWN: { variant: 'neutral' as const, label: 'Not tested' },
|
||||
} as const
|
||||
|
||||
|
||||
+2
-2
@@ -340,12 +340,12 @@ export function formatBalance(balance: number | null | undefined): string {
|
||||
*/
|
||||
export function getBalanceVariant(
|
||||
balance: number
|
||||
): 'success' | 'warning' | 'danger' | 'neutral' {
|
||||
): 'success' | 'warning' | 'destructive' | 'neutral' {
|
||||
if (balance === 0) {
|
||||
return 'neutral'
|
||||
}
|
||||
if (balance < 1) {
|
||||
return 'danger'
|
||||
return 'destructive'
|
||||
}
|
||||
if (balance < 10) {
|
||||
return 'warning'
|
||||
|
||||
@@ -41,6 +41,8 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/design-system/tabs'
|
||||
import { Toggle } from '@/components/design-system/toggle'
|
||||
import { MultiSelect } from '@/components/multi-select'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import {
|
||||
@@ -51,8 +53,6 @@ import {
|
||||
EmptyTitle,
|
||||
} from '@/components/ui/empty'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Toggle } from '@/components/ui/toggle'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
|
||||
@@ -20,8 +20,7 @@ import { Filter, X } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -30,7 +29,8 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from '@/components/ui/command'
|
||||
} from '@/components/design-system/command'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -108,7 +108,6 @@ export function FlowNodeFilterControl(props: FlowNodeFilterControlProps) {
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
aria-label={t('Filter by node')}
|
||||
/>
|
||||
}
|
||||
@@ -116,9 +115,7 @@ export function FlowNodeFilterControl(props: FlowNodeFilterControlProps) {
|
||||
<Filter data-icon='inline-start' aria-hidden='true' />
|
||||
{selectedCount > 0 ? t('Selected nodes') : t('All nodes')}
|
||||
{selectedCount > 0 && (
|
||||
<Badge variant='secondary' className='rounded-sm px-1'>
|
||||
{selectedCount}
|
||||
</Badge>
|
||||
<Badge variant='secondary'>{selectedCount}</Badge>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
@@ -201,14 +198,14 @@ export function FlowNodeFilterControl(props: FlowNodeFilterControlProps) {
|
||||
<Badge
|
||||
key={flowNodeFilterKey(option)}
|
||||
variant='secondary'
|
||||
className='max-w-[14rem] rounded-sm pr-1'
|
||||
className='max-w-[14rem] pr-1'
|
||||
>
|
||||
<span className='truncate'>
|
||||
{t(props.stageLabels[option.kind])}: {option.label}
|
||||
</span>
|
||||
<button
|
||||
type='button'
|
||||
className='hover:bg-muted-foreground/15 flex size-4 shrink-0 items-center justify-center rounded-sm'
|
||||
className='hover:bg-muted-foreground/15 focus-visible:ring-ring flex size-4 shrink-0 items-center justify-center rounded-sm outline-none focus-visible:ring-2'
|
||||
aria-label={t('Remove node filter')}
|
||||
onClick={() =>
|
||||
props.onRemoveNode({ kind: option.kind, id: option.id })
|
||||
|
||||
+5
-5
@@ -20,9 +20,7 @@ import { Save, Settings2 } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -30,7 +28,9 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
} from '@/components/design-system/select'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
CONSUMPTION_DISTRIBUTION_CHART_OPTIONS,
|
||||
MODEL_ANALYTICS_CHART_OPTIONS,
|
||||
@@ -71,7 +71,7 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) {
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
trigger={
|
||||
<Button variant='outline' size='sm'>
|
||||
<Button variant='outline'>
|
||||
<Settings2 className='mr-2 h-4 w-4' />
|
||||
{t('Preferences')}
|
||||
</Button>
|
||||
|
||||
+7
-8
@@ -21,11 +21,8 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { DateTimePicker } from '@/components/datetime-picker'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -33,7 +30,10 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
} from '@/components/design-system/select'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import {
|
||||
TIME_GRANULARITY_OPTIONS,
|
||||
TIME_RANGE_PRESETS,
|
||||
@@ -171,7 +171,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
trigger={
|
||||
<Button variant='outline' size='sm'>
|
||||
<Button variant='outline'>
|
||||
<Filter className='mr-2 h-4 w-4' />
|
||||
{t('Filter')}
|
||||
</Button>
|
||||
@@ -210,7 +210,6 @@ export function ModelsFilter(props: ModelsFilterProps) {
|
||||
<Button
|
||||
key={range.days}
|
||||
type='button'
|
||||
size='sm'
|
||||
variant={selectedRange === range.days ? 'default' : 'outline'}
|
||||
onClick={() => handleQuickRange(range.days)}
|
||||
className={cn(
|
||||
|
||||
+23
-22
@@ -21,13 +21,14 @@ import { Gauge, HeartPulse, Timer } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { StatusBadge, type StatusVariant } from '@/components/status-badge'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { getPerfMetricsSummary } from '@/features/performance-metrics/api'
|
||||
import {
|
||||
formatLatency,
|
||||
formatThroughput,
|
||||
formatUptimePct,
|
||||
getSuccessRateDotClass,
|
||||
getSuccessRateLevel,
|
||||
getSuccessRateTextClass,
|
||||
} from '@/features/performance-metrics/lib/format'
|
||||
import type { PerfModelSummary } from '@/features/performance-metrics/types'
|
||||
@@ -35,6 +36,11 @@ import { cn } from '@/lib/utils'
|
||||
|
||||
const PERFORMANCE_WINDOW_HOURS = 24
|
||||
const TOP_MODEL_LIMIT = 6
|
||||
const METRIC_SKELETON_KEYS = [
|
||||
'success-rate-skeleton',
|
||||
'latency-skeleton',
|
||||
'throughput-skeleton',
|
||||
]
|
||||
|
||||
type WeightedMetric = 'avg_latency_ms' | 'avg_tps' | 'success_rate'
|
||||
|
||||
@@ -60,7 +66,7 @@ function simpleAverage(
|
||||
count++
|
||||
}
|
||||
|
||||
return count > 0 ? total / count : NaN
|
||||
return count > 0 ? total / count : Number.NaN
|
||||
}
|
||||
|
||||
function buildPerformanceSummary(rows: PerfModelSummary[]): PerformanceSummary {
|
||||
@@ -128,8 +134,8 @@ export function PerformanceOverview() {
|
||||
{/* 3 KPI inline metrics */}
|
||||
{loading ? (
|
||||
<div className='flex flex-wrap items-center gap-x-5 gap-y-2'>
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className='flex items-center gap-1.5'>
|
||||
{METRIC_SKELETON_KEYS.map((key) => (
|
||||
<div key={key} className='flex items-center gap-1.5'>
|
||||
<Skeleton className='h-3 w-14' />
|
||||
<Skeleton className='h-4 w-16' />
|
||||
</div>
|
||||
@@ -201,27 +207,22 @@ function InlineMetric(props: {
|
||||
|
||||
function ModelBadge(props: { model: PerfModelSummary }) {
|
||||
const model = props.model
|
||||
const level = getSuccessRateLevel(model.success_rate)
|
||||
let variant: StatusVariant = 'neutral'
|
||||
if (level === 'excellent' || level === 'good') {
|
||||
variant = 'success'
|
||||
} else if (level === 'warning') {
|
||||
variant = 'warning'
|
||||
} else if (level === 'critical') {
|
||||
variant = 'destructive'
|
||||
}
|
||||
|
||||
return (
|
||||
<span className='bg-muted/50 inline-flex items-center gap-1.5 rounded-full px-2.5 py-1'>
|
||||
<span className='max-w-[10rem] truncate font-mono text-xs'>
|
||||
{model.model_name}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'size-1.5 rounded-full',
|
||||
getSuccessRateDotClass(model.success_rate)
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs font-semibold tabular-nums',
|
||||
getSuccessRateTextClass(model.success_rate)
|
||||
)}
|
||||
>
|
||||
<StatusBadge variant={variant}>
|
||||
<span className='mr-1 max-w-[10rem] truncate'>{model.model_name}</span>
|
||||
<span className='tabular-nums'>
|
||||
{formatUptimePct(model.success_rate)}
|
||||
</span>
|
||||
</span>
|
||||
</StatusBadge>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,12 +20,9 @@ import { Zap, ExternalLink, Gauge } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { CopyButton } from '@/components/copy-button'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
getLatencyColorClass,
|
||||
openExternalSpeedTest,
|
||||
} from '@/features/dashboard/lib/api-info'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { StatusBadge, type StatusVariant } from '@/components/status-badge'
|
||||
import { openExternalSpeedTest } from '@/features/dashboard/lib/api-info'
|
||||
import type { ApiInfoItem, PingStatus } from '@/features/dashboard/types'
|
||||
import { getBgColorClass } from '@/lib/colors'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -40,6 +37,12 @@ export function ApiInfoItemComponent(props: ApiInfoItemProps) {
|
||||
const { t } = useTranslation()
|
||||
const item = props.item
|
||||
const status = props.status
|
||||
let latencyVariant: StatusVariant = 'success'
|
||||
if (status.latency !== null && status.latency >= 500) {
|
||||
latencyVariant = 'destructive'
|
||||
} else if (status.latency !== null && status.latency >= 200) {
|
||||
latencyVariant = 'warning'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='group hover:bg-muted/40 flex items-center justify-between gap-2 px-3 py-2.5 transition-colors sm:gap-3 sm:px-5 sm:py-3'>
|
||||
@@ -69,36 +72,25 @@ export function ApiInfoItemComponent(props: ApiInfoItemProps) {
|
||||
<div className='flex shrink-0 items-center gap-2'>
|
||||
<div className='flex items-center'>
|
||||
{status.testing && (
|
||||
<StatusBadge
|
||||
label={t('Testing...')}
|
||||
variant='warning'
|
||||
className='animate-pulse'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='warning'>{t('Testing...')}</StatusBadge>
|
||||
)}
|
||||
{status.latency !== null && !status.testing && (
|
||||
<StatusBadge
|
||||
variant='success'
|
||||
label={`${status.latency}${t('ms')}`}
|
||||
className={cn(
|
||||
'font-mono font-medium',
|
||||
getLatencyColorClass(status.latency)
|
||||
)}
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant={latencyVariant}>
|
||||
{status.latency}
|
||||
{t('ms')}
|
||||
</StatusBadge>
|
||||
)}
|
||||
{status.error && (
|
||||
<StatusBadge label={t('N/A')} variant='neutral' copyable={false} />
|
||||
<StatusBadge variant='neutral'>{t('N/A')}</StatusBadge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-0.5'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
size='icon-sm'
|
||||
onClick={() => props.onTest(item.url)}
|
||||
disabled={status.testing}
|
||||
className='size-7 p-0'
|
||||
title={t('Test Latency')}
|
||||
>
|
||||
<Zap
|
||||
@@ -108,9 +100,9 @@ export function ApiInfoItemComponent(props: ApiInfoItemProps) {
|
||||
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
size='icon-sm'
|
||||
onClick={() => openExternalSpeedTest(item.url)}
|
||||
className='hidden size-7 p-0 sm:inline-flex'
|
||||
className='hidden sm:inline-flex'
|
||||
title={t('External Speed Test')}
|
||||
>
|
||||
<Gauge className='size-3.5' />
|
||||
@@ -119,8 +111,7 @@ export function ApiInfoItemComponent(props: ApiInfoItemProps) {
|
||||
<CopyButton
|
||||
value={item.url}
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='size-7 p-0'
|
||||
size='icon-sm'
|
||||
iconClassName='size-3.5'
|
||||
tooltip={t('Copy URL')}
|
||||
aria-label={t('Copy URL')}
|
||||
@@ -128,8 +119,8 @@ export function ApiInfoItemComponent(props: ApiInfoItemProps) {
|
||||
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='hidden size-7 p-0 sm:inline-flex'
|
||||
size='icon-sm'
|
||||
className='hidden sm:inline-flex'
|
||||
title={t('Open in New Tab')}
|
||||
render={<a href={item.url} target='_blank' rel='noreferrer' />}
|
||||
>
|
||||
|
||||
+1
-681
@@ -16,47 +16,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import {
|
||||
ArrowRight,
|
||||
BookOpen,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Circle,
|
||||
Copy,
|
||||
CreditCard,
|
||||
FileText,
|
||||
KeyRound,
|
||||
ListChecks,
|
||||
RadioTower,
|
||||
ShieldCheck,
|
||||
TerminalSquare,
|
||||
Timer,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import {
|
||||
CardStaggerContainer,
|
||||
CardStaggerItem,
|
||||
} from '@/components/page-transition'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { fetchTokenKey, getApiKeys } from '@/features/keys/api'
|
||||
import type { ApiKey } from '@/features/keys/types'
|
||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||
import { getUserModels } from '@/lib/api'
|
||||
import { ROLE } from '@/lib/roles'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
|
||||
import {
|
||||
useApiInfo,
|
||||
useDashboardContentVisibility,
|
||||
} from '../../hooks/use-status-data'
|
||||
import { useDashboardContentVisibility } from '../../hooks/use-status-data'
|
||||
import { AnnouncementsPanel } from './announcements-panel'
|
||||
import { ApiInfoPanel } from './api-info-panel'
|
||||
import { FAQPanel } from './faq-panel'
|
||||
@@ -64,670 +32,22 @@ import { PerformanceHealthPanel } from './performance-health-panel'
|
||||
import { SummaryCards } from './summary-cards'
|
||||
import { UptimePanel } from './uptime-panel'
|
||||
|
||||
const SETUP_GUIDE_VISIBILITY_STORAGE_KEY =
|
||||
'dashboard_overview_setup_guide_expanded'
|
||||
|
||||
const SETUP_GUIDE_CODE_PATTERN = [
|
||||
'const request = await client.responses.create({',
|
||||
" model: 'gpt-4.1-mini',",
|
||||
" input: 'Start routing traffic',",
|
||||
'})',
|
||||
'',
|
||||
'if (request.output_text) {',
|
||||
' console.log(request.output_text)',
|
||||
'}',
|
||||
].join('\n')
|
||||
|
||||
type DashboardActionPath =
|
||||
| '/keys'
|
||||
| '/wallet'
|
||||
| '/playground'
|
||||
| '/channels'
|
||||
| '/usage-logs'
|
||||
| '/pricing'
|
||||
|
||||
interface StartStep {
|
||||
title: string
|
||||
description: string
|
||||
to: DashboardActionPath
|
||||
icon: LucideIcon
|
||||
completed: boolean
|
||||
}
|
||||
|
||||
interface QuickAction {
|
||||
title: string
|
||||
description: string
|
||||
to: DashboardActionPath
|
||||
icon: LucideIcon
|
||||
adminOnly?: boolean
|
||||
}
|
||||
|
||||
interface RequestExample {
|
||||
endpoint: string
|
||||
model: string
|
||||
keyName: string
|
||||
keyId?: number
|
||||
displayKey: string
|
||||
ready: boolean
|
||||
}
|
||||
|
||||
interface HeroSignal {
|
||||
label: string
|
||||
value: string
|
||||
icon: LucideIcon
|
||||
}
|
||||
|
||||
function getSavedSetupGuideExpanded(): boolean | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
const saved = window.localStorage.getItem(SETUP_GUIDE_VISIBILITY_STORAGE_KEY)
|
||||
if (saved === 'expanded') return true
|
||||
if (saved === 'collapsed') return false
|
||||
return null
|
||||
}
|
||||
|
||||
function saveSetupGuideExpanded(expanded: boolean): void {
|
||||
if (typeof window === 'undefined') return
|
||||
window.localStorage.setItem(
|
||||
SETUP_GUIDE_VISIBILITY_STORAGE_KEY,
|
||||
expanded ? 'expanded' : 'collapsed'
|
||||
)
|
||||
}
|
||||
|
||||
function getCurrentOrigin(): string {
|
||||
if (typeof window === 'undefined') return ''
|
||||
return window.location.origin
|
||||
}
|
||||
|
||||
function normalizeEndpoint(sourceUrl?: string): string {
|
||||
const fallback = `${getCurrentOrigin()}/v1/chat/completions`
|
||||
const trimmed = sourceUrl?.trim()
|
||||
if (!trimmed) return fallback
|
||||
|
||||
const withoutTrailingSlash = trimmed.replace(/\/+$/, '')
|
||||
if (withoutTrailingSlash.endsWith('/v1/chat/completions')) {
|
||||
return withoutTrailingSlash
|
||||
}
|
||||
if (withoutTrailingSlash.endsWith('/v1')) {
|
||||
return `${withoutTrailingSlash}/chat/completions`
|
||||
}
|
||||
return `${withoutTrailingSlash}/v1/chat/completions`
|
||||
}
|
||||
|
||||
function getPreferredKey(keys: ApiKey[]): ApiKey | null {
|
||||
return keys.find((item) => item.status === 1) ?? keys[0] ?? null
|
||||
}
|
||||
|
||||
function formatDisplayKey(key?: string): string {
|
||||
if (!key) return 'sk-...'
|
||||
if (key.length <= 14) return key
|
||||
return `${key.slice(0, 7)}...${key.slice(-4)}`
|
||||
}
|
||||
|
||||
function buildCurlCommand(args: {
|
||||
endpoint: string
|
||||
apiKey: string
|
||||
model: string
|
||||
}): string {
|
||||
return [
|
||||
`curl ${args.endpoint} \\`,
|
||||
' -H "Content-Type: application/json" \\',
|
||||
` -H "Authorization: Bearer ${args.apiKey}" \\`,
|
||||
` -d '{"model":"${args.model}","messages":[{"role":"user","content":"Say hello in one sentence."}]}'`,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function SetupGuideBackdrop(props: { compact?: boolean }) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_48%_120%_at_78%_0%,color-mix(in_oklch,var(--primary)_8%,transparent)_0%,transparent_62%),linear-gradient(112deg,color-mix(in_oklch,var(--card)_98%,var(--primary)_2%)_0%,color-mix(in_oklch,var(--card)_94%,var(--muted)_6%)_48%,color-mix(in_oklch,var(--background)_92%,var(--accent)_8%)_100%)] dark:opacity-65',
|
||||
props.compact
|
||||
? '[mask-image:linear-gradient(90deg,black_0%,black_48%,transparent_74%)] opacity-55'
|
||||
: 'opacity-85'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'text-foreground/5 dark:text-foreground/8 pointer-events-none absolute inset-y-0 right-0 hidden overflow-hidden font-mono sm:block',
|
||||
props.compact ? 'w-1/2 opacity-45' : 'w-[58%] opacity-75'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
>
|
||||
<pre
|
||||
className={cn(
|
||||
'absolute right-3 [mask-image:linear-gradient(90deg,transparent_0%,black_30%,black_82%,transparent_100%)] text-right tracking-[0.38em] whitespace-pre',
|
||||
props.compact
|
||||
? '-top-6 text-[9px] leading-4'
|
||||
: 'top-1 text-xs leading-5'
|
||||
)}
|
||||
>
|
||||
{SETUP_GUIDE_CODE_PATTERN}
|
||||
</pre>
|
||||
</div>
|
||||
<div
|
||||
className='from-background/35 to-background/70 dark:from-background/20 dark:to-background/80 pointer-events-none absolute inset-0 bg-linear-to-b via-transparent'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function StartStepItem(props: {
|
||||
step: StartStep
|
||||
index: number
|
||||
isLast: boolean
|
||||
}) {
|
||||
const Icon = props.step.icon
|
||||
const StatusIcon = props.step.completed ? Check : Circle
|
||||
|
||||
return (
|
||||
<li className='relative flex gap-3 pb-2.5 last:pb-0'>
|
||||
{!props.isLast && (
|
||||
<span
|
||||
className='bg-border absolute top-9 bottom-0 left-4 w-px'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
'bg-background relative z-10 flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs',
|
||||
props.step.completed && 'border-success/30 bg-success/10'
|
||||
)}
|
||||
>
|
||||
<StatusIcon
|
||||
className={props.step.completed ? 'text-success size-4' : 'size-4'}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</span>
|
||||
|
||||
<Link
|
||||
to={props.step.to}
|
||||
className='bg-background/70 hover:bg-muted/50 focus-visible:ring-ring flex min-w-0 flex-1 items-center justify-between gap-3 rounded-xl border px-3 py-2.5 text-left shadow-xs transition-colors outline-none focus-visible:ring-2'
|
||||
>
|
||||
<span className='flex min-w-0 items-start gap-2.5'>
|
||||
<span className='bg-muted mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-lg'>
|
||||
<Icon className='size-3.5' aria-hidden='true' />
|
||||
</span>
|
||||
<span className='flex min-w-0 flex-col gap-0.5'>
|
||||
<span className='flex items-center gap-2 text-sm font-medium'>
|
||||
<span className='text-muted-foreground text-xs tabular-nums'>
|
||||
{props.index + 1}.
|
||||
</span>
|
||||
<span className='truncate'>{props.step.title}</span>
|
||||
</span>
|
||||
<span className='text-muted-foreground line-clamp-1 text-xs'>
|
||||
{props.step.description}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<ArrowRight
|
||||
className='text-muted-foreground size-4 shrink-0'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function RequestPreview(props: {
|
||||
example: RequestExample
|
||||
signals: HeroSignal[]
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [isCopying, setIsCopying] = useState(false)
|
||||
const { copyToClipboard } = useCopyToClipboard({ notify: false })
|
||||
const previewCurl = buildCurlCommand({
|
||||
endpoint: props.example.endpoint,
|
||||
apiKey: props.example.displayKey,
|
||||
model: props.example.model,
|
||||
})
|
||||
const previewLines = previewCurl.split('\n')
|
||||
const handleCopyRequest = async () => {
|
||||
if (!props.example.keyId || isCopying) return
|
||||
|
||||
setIsCopying(true)
|
||||
try {
|
||||
const result = await fetchTokenKey(props.example.keyId)
|
||||
const key = result.success && result.data?.key ? result.data.key : ''
|
||||
if (!key) {
|
||||
toast.error(result.message || t('Failed to copy to clipboard'))
|
||||
return
|
||||
}
|
||||
|
||||
const realCurl = buildCurlCommand({
|
||||
endpoint: props.example.endpoint,
|
||||
apiKey: `sk-${key}`,
|
||||
model: props.example.model,
|
||||
})
|
||||
const copied = await copyToClipboard(realCurl)
|
||||
if (copied) {
|
||||
toast.success(t('Copied to clipboard'))
|
||||
} else {
|
||||
toast.error(t('Failed to copy to clipboard'))
|
||||
}
|
||||
} finally {
|
||||
setIsCopying(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='bg-background relative overflow-hidden rounded-2xl border p-3'>
|
||||
<div className='flex items-center justify-between gap-3 border-b pb-3'>
|
||||
<div className='flex min-w-0 items-center gap-2'>
|
||||
<span className='bg-muted flex size-8 shrink-0 items-center justify-center rounded-lg'>
|
||||
<TerminalSquare className='size-4' aria-hidden='true' />
|
||||
</span>
|
||||
<div className='min-w-0'>
|
||||
<div className='truncate text-sm font-medium'>
|
||||
{t('First API request')}
|
||||
</div>
|
||||
<div className='text-muted-foreground truncate text-xs'>
|
||||
{props.example.ready
|
||||
? props.example.keyName
|
||||
: t('Create an API key to unlock the real request')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{props.example.ready ? (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='h-7 gap-1.5 px-2 text-xs'
|
||||
disabled={isCopying}
|
||||
onClick={handleCopyRequest}
|
||||
aria-label={t('Copy ready-to-run curl')}
|
||||
>
|
||||
<Copy data-icon='inline-start' />
|
||||
{isCopying ? t('Loading') : t('Copy')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button size='sm' variant='outline' render={<Link to='/keys' />}>
|
||||
{t('Create API Key')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='bg-foreground/[0.035] my-3 rounded-xl p-3 font-mono text-xs'>
|
||||
<div className='mb-2 flex items-center gap-1.5'>
|
||||
<span className='bg-destructive size-2 rounded-full' />
|
||||
<span className='bg-warning size-2 rounded-full' />
|
||||
<span className='bg-success size-2 rounded-full' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-1 overflow-hidden'>
|
||||
{previewLines.map((line, index) => (
|
||||
<code
|
||||
key={`${line}-${index}`}
|
||||
className='text-muted-foreground truncate'
|
||||
title={line}
|
||||
>
|
||||
{line}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-2'>
|
||||
{props.signals.map((signal) => {
|
||||
const Icon = signal.icon
|
||||
|
||||
return (
|
||||
<div
|
||||
key={signal.label}
|
||||
className='bg-muted/40 flex items-center justify-between gap-3 rounded-xl px-3 py-2'
|
||||
>
|
||||
<span className='flex min-w-0 items-center gap-2'>
|
||||
<Icon
|
||||
className='text-muted-foreground size-3.5 shrink-0'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<span className='truncate text-xs font-medium'>
|
||||
{signal.label}
|
||||
</span>
|
||||
</span>
|
||||
<span className='text-muted-foreground shrink-0 text-xs'>
|
||||
{signal.value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function QuickActionItem(props: { action: QuickAction }) {
|
||||
const Icon = props.action.icon
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant='outline'
|
||||
className='h-auto justify-start rounded-xl px-3 py-3 text-left'
|
||||
render={<Link to={props.action.to} />}
|
||||
>
|
||||
<span className='bg-muted flex size-9 shrink-0 items-center justify-center rounded-lg'>
|
||||
<Icon className='size-4' aria-hidden='true' />
|
||||
</span>
|
||||
<span className='flex min-w-0 flex-1 flex-col gap-0.5'>
|
||||
<span className='truncate text-sm font-medium'>
|
||||
{props.action.title}
|
||||
</span>
|
||||
<span className='text-muted-foreground line-clamp-2 text-xs leading-relaxed'>
|
||||
{props.action.description}
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function CompactQuickAction(props: { action: QuickAction }) {
|
||||
const Icon = props.action.icon
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='bg-background/70 h-8 min-w-24 gap-1.5 px-2.5'
|
||||
render={<Link to={props.action.to} />}
|
||||
>
|
||||
<Icon data-icon='inline-start' />
|
||||
<span>{props.action.title}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function OverviewDashboard() {
|
||||
const { t } = useTranslation()
|
||||
const user = useAuthStore((state) => state.auth.user)
|
||||
const { items: apiInfoItems } = useApiInfo()
|
||||
const {
|
||||
apiInfo: showApiInfoPanel,
|
||||
announcements: showAnnouncementsPanel,
|
||||
faq: showFAQPanel,
|
||||
uptimeKuma: showUptimePanel,
|
||||
} = useDashboardContentVisibility()
|
||||
const [manualSetupGuideExpanded, setManualSetupGuideExpanded] = useState<
|
||||
boolean | null
|
||||
>(() => getSavedSetupGuideExpanded())
|
||||
|
||||
const requestCount = Number(user?.request_count ?? 0)
|
||||
const remainQuota = Number(user?.quota ?? 0)
|
||||
const usedQuota = Number(user?.used_quota ?? 0)
|
||||
const isAdmin = Boolean(user?.role && user.role >= ROLE.ADMIN)
|
||||
|
||||
const apiKeysQuery = useQuery({
|
||||
queryKey: ['dashboard', 'overview', 'api-keys'],
|
||||
queryFn: async () => {
|
||||
const result = await getApiKeys({ p: 1, size: 10 })
|
||||
return result.success ? (result.data?.items ?? []) : []
|
||||
},
|
||||
staleTime: 60 * 1000,
|
||||
})
|
||||
|
||||
const modelsQuery = useQuery({
|
||||
queryKey: ['dashboard', 'overview', 'user-models'],
|
||||
queryFn: async () => {
|
||||
const result = await getUserModels()
|
||||
return result.success ? (result.data ?? []) : []
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
|
||||
const preferredKey = useMemo(
|
||||
() => getPreferredKey(apiKeysQuery.data ?? []),
|
||||
[apiKeysQuery.data]
|
||||
)
|
||||
|
||||
const startSteps = useMemo<StartStep[]>(
|
||||
() => [
|
||||
{
|
||||
title: t('Create API Key'),
|
||||
description: t('Create a key for your app or service'),
|
||||
to: '/keys',
|
||||
icon: KeyRound,
|
||||
completed: Boolean(preferredKey),
|
||||
},
|
||||
{
|
||||
title: t('Add credits'),
|
||||
description: t('Keep enough balance before production traffic'),
|
||||
to: '/wallet',
|
||||
icon: CreditCard,
|
||||
completed: remainQuota > 0 || usedQuota > 0,
|
||||
},
|
||||
{
|
||||
title: t('Send a request'),
|
||||
description: t('Verify routing with Playground or your client'),
|
||||
to: '/playground',
|
||||
icon: TerminalSquare,
|
||||
completed: requestCount > 0,
|
||||
},
|
||||
],
|
||||
[preferredKey, remainQuota, requestCount, t, usedQuota]
|
||||
)
|
||||
|
||||
const quickActions = useMemo<QuickAction[]>(
|
||||
() => [
|
||||
{
|
||||
title: t('API Keys'),
|
||||
description: t('Create a key for your app or service'),
|
||||
to: '/keys',
|
||||
icon: KeyRound,
|
||||
},
|
||||
{
|
||||
title: t('Channels'),
|
||||
description: t('Configure upstream providers and routing.'),
|
||||
to: '/channels',
|
||||
icon: RadioTower,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
title: t('Usage Logs'),
|
||||
description: t('Inspect requests, errors, and billing details'),
|
||||
to: '/usage-logs',
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
title: t('Pricing'),
|
||||
description: t('Review model rates before scaling traffic'),
|
||||
to: '/pricing',
|
||||
icon: BookOpen,
|
||||
},
|
||||
],
|
||||
[t]
|
||||
)
|
||||
|
||||
const visibleQuickActions = useMemo(
|
||||
() => quickActions.filter((action) => !action.adminOnly || isAdmin),
|
||||
[isAdmin, quickActions]
|
||||
)
|
||||
|
||||
const heroSignals = useMemo<HeroSignal[]>(
|
||||
() => [
|
||||
{
|
||||
label: t('Route active'),
|
||||
value: apiInfoItems.length > 0 ? t('Online') : t('Current domain'),
|
||||
icon: RadioTower,
|
||||
},
|
||||
{
|
||||
label: t('Auth configured'),
|
||||
value: preferredKey ? t('Secured') : t('Needs API key'),
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
{
|
||||
label: t('Model selected'),
|
||||
value: modelsQuery.data?.[0] ?? t('Loading'),
|
||||
icon: Timer,
|
||||
},
|
||||
],
|
||||
[apiInfoItems.length, modelsQuery.data, preferredKey, t]
|
||||
)
|
||||
|
||||
const requestExample = useMemo<RequestExample>(() => {
|
||||
const endpoint = normalizeEndpoint(apiInfoItems[0]?.url)
|
||||
const model = modelsQuery.data?.[0] ?? 'gpt-4o-mini'
|
||||
const keyName = preferredKey?.name ?? t('No API key yet')
|
||||
const ready = Boolean(preferredKey?.id && model)
|
||||
|
||||
return {
|
||||
endpoint,
|
||||
model,
|
||||
keyName,
|
||||
keyId: preferredKey?.id,
|
||||
displayKey: preferredKey
|
||||
? formatDisplayKey(`sk-${preferredKey.key}`)
|
||||
: 'sk-...',
|
||||
ready,
|
||||
}
|
||||
}, [apiInfoItems, modelsQuery.data, preferredKey, t])
|
||||
|
||||
const completedStepCount = startSteps.filter((step) => step.completed).length
|
||||
const setupComplete = completedStepCount === startSteps.length
|
||||
const setupStatusReady = apiKeysQuery.isFetched && Boolean(user)
|
||||
const setupGuideExpanded =
|
||||
manualSetupGuideExpanded ?? (setupStatusReady && !setupComplete)
|
||||
const showLeftContentPanels =
|
||||
isAdmin || showApiInfoPanel || showAnnouncementsPanel || showFAQPanel
|
||||
const showContentPanels = showLeftContentPanels || showUptimePanel
|
||||
|
||||
const handleSetupGuideToggle = () => {
|
||||
const nextExpanded = !setupGuideExpanded
|
||||
setManualSetupGuideExpanded(nextExpanded)
|
||||
saveSetupGuideExpanded(nextExpanded)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{setupGuideExpanded ? (
|
||||
<CardStaggerContainer className='grid items-stretch gap-4 xl:grid-cols-[minmax(0,1fr)_22rem]'>
|
||||
<CardStaggerItem className='bg-card h-full overflow-hidden rounded-2xl border shadow-xs'>
|
||||
<div className='relative h-full overflow-hidden p-4 sm:p-5'>
|
||||
<SetupGuideBackdrop />
|
||||
<div className='relative grid gap-5 lg:grid-cols-[minmax(0,1fr)_21rem]'>
|
||||
<div className='flex min-w-0 flex-col gap-5'>
|
||||
<div className='flex flex-wrap items-start justify-between gap-3'>
|
||||
<div className='flex max-w-2xl flex-col gap-1'>
|
||||
<div className='text-muted-foreground flex items-center gap-2 text-xs font-medium tracking-wider uppercase'>
|
||||
<ListChecks className='size-3.5' aria-hidden='true' />
|
||||
{t('Get started')}
|
||||
</div>
|
||||
<h3 className='text-xl font-semibold tracking-tight sm:text-2xl'>
|
||||
{t('Build on your API gateway in minutes')}
|
||||
</h3>
|
||||
<p className='text-muted-foreground max-w-xl text-sm leading-relaxed'>
|
||||
{t(
|
||||
'A focused home for keys, balance, routing, and service health.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleSetupGuideToggle}
|
||||
>
|
||||
<ChevronUp data-icon='inline-start' />
|
||||
{t('Hide setup guide')}
|
||||
</Button>
|
||||
<Button size='sm' render={<Link to='/keys' />}>
|
||||
<KeyRound data-icon='inline-start' />
|
||||
{t('Create API Key')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ol className='bg-background/45 rounded-2xl border p-2 backdrop-blur'>
|
||||
{startSteps.map((step, index) => (
|
||||
<StartStepItem
|
||||
key={step.title}
|
||||
step={step}
|
||||
index={index}
|
||||
isLast={index === startSteps.length - 1}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<RequestPreview
|
||||
example={requestExample}
|
||||
signals={heroSignals}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardStaggerItem>
|
||||
|
||||
<CardStaggerItem className='bg-card h-full rounded-2xl border p-4 shadow-xs sm:p-5'>
|
||||
<div className='flex h-full flex-col gap-4'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
|
||||
{t('Recommended actions')}
|
||||
</div>
|
||||
<h3 className='text-lg font-semibold tracking-tight'>
|
||||
{t('Keep the platform ready')}
|
||||
</h3>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
{visibleQuickActions.map((action) => (
|
||||
<QuickActionItem key={action.title} action={action} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardStaggerItem>
|
||||
</CardStaggerContainer>
|
||||
) : (
|
||||
<CardStaggerContainer>
|
||||
<CardStaggerItem className='bg-card overflow-hidden rounded-2xl border shadow-xs'>
|
||||
<div className='relative overflow-hidden px-4 py-3 sm:px-5'>
|
||||
<SetupGuideBackdrop compact />
|
||||
<div className='relative flex flex-wrap items-center justify-between gap-3'>
|
||||
<div className='flex min-w-0 items-center gap-3'>
|
||||
<span className='bg-background/70 flex size-9 shrink-0 items-center justify-center rounded-xl border shadow-xs'>
|
||||
<Check className='text-success size-4' aria-hidden='true' />
|
||||
</span>
|
||||
<div className='min-w-0'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<h3 className='truncate text-sm font-semibold'>
|
||||
{setupComplete
|
||||
? t('Setup guide complete')
|
||||
: t('Setup guide')}
|
||||
</h3>
|
||||
<span className='text-muted-foreground bg-background/60 rounded-md border px-2 py-0.5 text-xs'>
|
||||
{t('Setup progress: {{completed}}/{{total}}', {
|
||||
completed: completedStepCount,
|
||||
total: startSteps.length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<p className='text-muted-foreground line-clamp-1 text-xs'>
|
||||
{setupComplete
|
||||
? t(
|
||||
'Your setup guide is collapsed so usage stays in focus.'
|
||||
)
|
||||
: t('Setup guide is collapsed. Expand it anytime.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
{visibleQuickActions.map((action) => (
|
||||
<CompactQuickAction key={action.title} action={action} />
|
||||
))}
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='bg-background/70 h-8 min-w-28'
|
||||
onClick={handleSetupGuideToggle}
|
||||
>
|
||||
<ChevronDown data-icon='inline-start' />
|
||||
{t('Show setup guide')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardStaggerItem>
|
||||
</CardStaggerContainer>
|
||||
)}
|
||||
|
||||
<SummaryCards />
|
||||
|
||||
{showContentPanels && (
|
||||
|
||||
@@ -22,8 +22,8 @@ import { ArrowRight, Flame, ShieldCheck, TrendingDown } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { StaggerContainer, StaggerItem } from '@/components/page-transition'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { getUserQuotaDates } from '@/features/dashboard/api'
|
||||
import { useSummaryCardsConfig } from '@/features/dashboard/hooks/use-dashboard-config'
|
||||
import type { QuotaDataItem } from '@/features/dashboard/types'
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Activity, RotateCw } from 'lucide-react'
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { getUptimeStatus } from '@/features/dashboard/api'
|
||||
import type {
|
||||
@@ -110,10 +110,9 @@ export function UptimePanel() {
|
||||
headerActions={
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
size='icon-sm'
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className='size-7 p-0'
|
||||
>
|
||||
<RotateCw
|
||||
className={cn('size-3.5', refreshing && 'animate-spin')}
|
||||
|
||||
@@ -22,8 +22,8 @@ import { Users, Loader2 } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/design-system/tabs'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { useTheme } from '@/context/theme-provider'
|
||||
import { getUserQuotaDataByUsers } from '@/features/dashboard/api'
|
||||
import {
|
||||
|
||||
+4
-4
@@ -21,11 +21,11 @@ import { Eye, EyeOff } from 'lucide-react'
|
||||
import { useState, useCallback, useMemo, lazy, Suspense } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/design-system/tabs'
|
||||
import { SectionPageLayout } from '@/components/layout'
|
||||
import { FadeIn } from '@/components/page-transition'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -265,7 +265,7 @@ export function Dashboard() {
|
||||
? t('Hide sensitive data')
|
||||
: t('Show sensitive data')
|
||||
}
|
||||
className='text-muted-foreground hover:text-foreground size-8'
|
||||
className='text-muted-foreground hover:text-foreground'
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -298,7 +298,7 @@ export function Dashboard() {
|
||||
<div className='flex flex-wrap items-center justify-between gap-1.5 sm:gap-2'>
|
||||
{showSectionTabs ? (
|
||||
<Tabs value={activeSection} onValueChange={handleSectionChange}>
|
||||
<TabsList className='max-w-full flex-wrap justify-start group-data-horizontal/tabs:h-auto'>
|
||||
<TabsList className='max-w-full flex-wrap justify-start group-data-horizontal/tabs:h-auto sm:group-data-horizontal/tabs:h-auto'>
|
||||
{visibleSections.map((section) => (
|
||||
<TabsTrigger key={section} value={section}>
|
||||
{t(SECTION_META[section].titleKey)}
|
||||
|
||||
@@ -1,56 +1,13 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'node:test'
|
||||
|
||||
import type { FlowUserFilterOption } from '../types'
|
||||
import {
|
||||
compactFlowSelectionLabel,
|
||||
flowDisplayState,
|
||||
requireSuccessfulFlowRows,
|
||||
visibleFlowUsers,
|
||||
} from './flow-selection'
|
||||
|
||||
const users: FlowUserFilterOption[] = [
|
||||
{
|
||||
value: 'user:1',
|
||||
label: 'dry',
|
||||
valueLabel: '100',
|
||||
valueRaw: 100,
|
||||
color: '#1664ff',
|
||||
},
|
||||
{
|
||||
value: 'user:2',
|
||||
label: 'jrc',
|
||||
valueLabel: '70',
|
||||
valueRaw: 70,
|
||||
color: '#1ac6ff',
|
||||
},
|
||||
]
|
||||
|
||||
describe('dashboard flow selection helpers', () => {
|
||||
test('limits user chips to currently visible users', () => {
|
||||
assert.deepEqual(
|
||||
visibleFlowUsers(users, []).map((user) => user.value),
|
||||
['user:1', 'user:2']
|
||||
)
|
||||
assert.deepEqual(
|
||||
visibleFlowUsers(users, ['user:2']).map((user) => user.value),
|
||||
['user:2']
|
||||
)
|
||||
})
|
||||
|
||||
test('filters visible users without mutating the source options', () => {
|
||||
const visible = visibleFlowUsers(users, ['user:1'])
|
||||
|
||||
assert.deepEqual(
|
||||
visible.map((user) => user.value),
|
||||
['user:1']
|
||||
)
|
||||
assert.deepEqual(
|
||||
users.map((user) => user.value),
|
||||
['user:1', 'user:2']
|
||||
)
|
||||
})
|
||||
|
||||
test('formats compact selected counts for flow multiselect summaries', () => {
|
||||
assert.equal(compactFlowSelectionLabel(0), '*')
|
||||
assert.equal(compactFlowSelectionLabel(1), '1')
|
||||
|
||||
+1
-13
@@ -16,10 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import type {
|
||||
FlowQuotaDataItem,
|
||||
FlowUserFilterOption,
|
||||
} from '@/features/dashboard/types'
|
||||
import type { FlowQuotaDataItem } from '@/features/dashboard/types'
|
||||
|
||||
export type FlowDisplayState = 'loading' | 'error' | 'empty' | 'chart'
|
||||
|
||||
@@ -55,12 +52,3 @@ export function flowDisplayState(options: {
|
||||
export function compactFlowSelectionLabel(count: number): string {
|
||||
return count > 0 ? String(count) : '*'
|
||||
}
|
||||
|
||||
export function visibleFlowUsers(
|
||||
users: FlowUserFilterOption[],
|
||||
selectedUsers: string[]
|
||||
): FlowUserFilterOption[] {
|
||||
if (selectedUsers.length === 0) return users
|
||||
const selected = new Set(selectedUsers)
|
||||
return users.filter((user) => selected.has(user.value))
|
||||
}
|
||||
|
||||
+4
-6
@@ -2,11 +2,7 @@ import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'node:test'
|
||||
|
||||
import type { FlowQuotaDataItem } from '../types'
|
||||
import {
|
||||
buildDashboardFlowData,
|
||||
buildFlowFilterOptions,
|
||||
buildFlowSankeySpec,
|
||||
} from './flow'
|
||||
import { buildDashboardFlowData, buildFlowSankeySpec } from './flow'
|
||||
|
||||
const rows: FlowQuotaDataItem[] = [
|
||||
{
|
||||
@@ -276,7 +272,9 @@ describe('dashboard flow data', () => {
|
||||
})
|
||||
|
||||
test('builds user filter options with stable values', () => {
|
||||
const options = buildFlowFilterOptions(rows, 'quota')
|
||||
const options = buildDashboardFlowData(rows, 'quota', {
|
||||
role: 'admin',
|
||||
}).filterOptions
|
||||
|
||||
assert.deepEqual(
|
||||
options.users.map((user) => [user.value, user.label, user.valueLabel]),
|
||||
|
||||
-22
@@ -947,28 +947,6 @@ function buildNodeFilterOptions(
|
||||
)
|
||||
}
|
||||
|
||||
export function buildFlowFilterOptions(
|
||||
rows: FlowQuotaDataItem[],
|
||||
metric: FlowMetric = 'quota',
|
||||
palette?: readonly string[],
|
||||
role: FlowRole = DEFAULT_FLOW_ROLE,
|
||||
visibleStages?: FlowNodeKind[],
|
||||
selectedNodes?: readonly FlowNodeFilter[]
|
||||
): FlowFilterOptions {
|
||||
return {
|
||||
users: buildUserFilterOptions(rows, metric, palette),
|
||||
nodes: buildNodeFilterOptions(
|
||||
rows,
|
||||
metric,
|
||||
role,
|
||||
visibleStages,
|
||||
palette,
|
||||
EMPTY_FLOW_PATH_CONTEXT,
|
||||
selectedNodes
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDashboardFlowData(
|
||||
rows: FlowQuotaDataItem[],
|
||||
metric: FlowMetric = 'quota',
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { useNavigate, useRouter } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
|
||||
export function ForbiddenError() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { useNavigate, useRouter } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const FEEDBACK_URL = 'https://github.com/QuantumNous/new-api/issues'
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
|
||||
export function MaintenanceError() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { useNavigate, useRouter } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
|
||||
export function NotFoundError() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { useNavigate, useRouter } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
|
||||
export function UnauthorisedError() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Link } from '@tanstack/react-router'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
|
||||
interface HeroButtonsProps {
|
||||
isAuthenticated: boolean
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ArrowRight } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { AnimateInView } from '@/components/animate-in-view'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
|
||||
interface CTAProps {
|
||||
className?: string
|
||||
|
||||
+11
-6
@@ -21,7 +21,7 @@ import { Link } from '@tanstack/react-router'
|
||||
import { ArrowRight, BookOpen } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { useStatus } from '@/hooks/use-status'
|
||||
|
||||
import { HeroTerminalDemo } from '../hero-terminal-demo'
|
||||
@@ -57,7 +57,8 @@ export function Hero(props: HeroProps) {
|
||||
return (
|
||||
<Button
|
||||
variant='outline'
|
||||
className='group border-border/50 hover:border-border hover:bg-muted/50 inline-flex h-11 items-center gap-1.5 rounded-lg px-5 text-sm font-medium'
|
||||
size='xl'
|
||||
className='group border-border/50 hover:border-border hover:bg-muted/50 gap-1.5 rounded-lg text-sm'
|
||||
render={
|
||||
<a href={docsUrl} target='_blank' rel='noopener noreferrer' />
|
||||
}
|
||||
@@ -70,7 +71,8 @@ export function Hero(props: HeroProps) {
|
||||
return (
|
||||
<Button
|
||||
variant='outline'
|
||||
className='group border-border/50 hover:border-border hover:bg-muted/50 inline-flex h-11 items-center gap-1.5 rounded-lg px-5 text-sm font-medium'
|
||||
size='xl'
|
||||
className='group border-border/50 hover:border-border hover:bg-muted/50 gap-1.5 rounded-lg text-sm'
|
||||
render={<Link to={docsUrl} />}
|
||||
>
|
||||
<BookOpen className='text-muted-foreground/80 group-hover:text-foreground size-4 transition-colors duration-200' />
|
||||
@@ -140,7 +142,8 @@ export function Hero(props: HeroProps) {
|
||||
{props.isAuthenticated ? (
|
||||
<>
|
||||
<Button
|
||||
className='group h-11 rounded-lg px-5 text-sm font-medium'
|
||||
size='xl'
|
||||
className='group rounded-lg text-sm'
|
||||
render={<Link to='/dashboard' />}
|
||||
>
|
||||
{t('Go to Dashboard')}
|
||||
@@ -151,7 +154,8 @@ export function Hero(props: HeroProps) {
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
className='group h-11 rounded-lg px-5 text-sm font-medium'
|
||||
size='xl'
|
||||
className='group rounded-lg text-sm'
|
||||
render={<Link to='/sign-up' />}
|
||||
>
|
||||
{t('Get Started')}
|
||||
@@ -159,7 +163,8 @@ export function Hero(props: HeroProps) {
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='border-border/50 hover:border-border hover:bg-muted/50 h-11 rounded-lg px-5 text-sm font-medium'
|
||||
size='xl'
|
||||
className='border-border/50 hover:border-border hover:bg-muted/50 rounded-lg text-sm'
|
||||
render={<Link to='/pricing' />}
|
||||
>
|
||||
{t('View Pricing')}
|
||||
|
||||
Vendored
+1
-1
@@ -35,7 +35,7 @@ import type {
|
||||
export async function getApiKeys(
|
||||
params: GetApiKeysParams = {}
|
||||
): Promise<GetApiKeysResponse> {
|
||||
const { p = 1, size = 10 } = params
|
||||
const { p = 1, size = 20 } = params
|
||||
const res = await api.get(`/api/token/?p=${p}&size=${size}`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
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 { flexRender, type Row } from '@tanstack/react-table'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
DataTableCardDetails,
|
||||
DataTableCardField,
|
||||
} from '@/components/data-table'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { formatQuota } from '@/lib/format'
|
||||
|
||||
import type { ApiKey } from '../types'
|
||||
|
||||
function renderApiKeyCell(row: Row<ApiKey>, columnId: string) {
|
||||
const cell = row
|
||||
.getVisibleCells()
|
||||
.find((candidate) => candidate.column.id === columnId)
|
||||
if (!cell) return null
|
||||
return flexRender(cell.column.columnDef.cell, cell.getContext())
|
||||
}
|
||||
|
||||
function ApiKeyModels(props: { apiKey: ApiKey }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!props.apiKey.model_limits_enabled || !props.apiKey.model_limits) {
|
||||
return <StatusBadge variant='neutral'>{t('Unlimited')}</StatusBadge>
|
||||
}
|
||||
|
||||
const models = props.apiKey.model_limits
|
||||
.split(',')
|
||||
.map((model) => model.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
return (
|
||||
<span className='font-mono text-xs whitespace-pre-wrap'>
|
||||
{models.join(', ')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function ApiKeyIpRestrictions(props: { apiKey: ApiKey }) {
|
||||
const { t } = useTranslation()
|
||||
const allowIps = props.apiKey.allow_ips?.trim()
|
||||
|
||||
if (!allowIps) {
|
||||
return <StatusBadge variant='neutral'>{t('No restriction')}</StatusBadge>
|
||||
}
|
||||
|
||||
const ips = allowIps
|
||||
.split('\n')
|
||||
.map((ip) => ip.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
return (
|
||||
<span className='font-mono text-xs whitespace-pre-wrap'>
|
||||
{ips.join('\n')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function ApiKeyCard(props: { row: Row<ApiKey> }) {
|
||||
const { t } = useTranslation()
|
||||
const apiKey = props.row.original
|
||||
const totalQuota = apiKey.used_quota + apiKey.remain_quota
|
||||
const visibleColumnIds = new Set(
|
||||
props.row.getVisibleCells().map((cell) => cell.column.id)
|
||||
)
|
||||
const detailsCount = [
|
||||
'status',
|
||||
'group',
|
||||
'model_limits',
|
||||
'allow_ips',
|
||||
'created_time',
|
||||
'accessed_time',
|
||||
'expired_time',
|
||||
'actions',
|
||||
].filter((columnId) => visibleColumnIds.has(columnId)).length
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='grid grid-cols-2 gap-x-3 gap-y-2'>
|
||||
{visibleColumnIds.has('name') && (
|
||||
<DataTableCardField
|
||||
label={t('Name')}
|
||||
span={2}
|
||||
contentMode='wrap'
|
||||
valueClassName='font-medium'
|
||||
>
|
||||
{renderApiKeyCell(props.row, 'name')}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('key') && (
|
||||
<DataTableCardField label={t('API Key')} span={2} contentMode='full'>
|
||||
{renderApiKeyCell(props.row, 'key')}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('quota') && (
|
||||
<DataTableCardField label={t('Quota')} span={2} contentMode='full'>
|
||||
{apiKey.unlimited_quota ? (
|
||||
<StatusBadge variant='neutral'>{t('Unlimited')}</StatusBadge>
|
||||
) : (
|
||||
<span className='font-medium tabular-nums'>
|
||||
{formatQuota(apiKey.remain_quota)}
|
||||
<span className='text-muted-foreground font-normal'>
|
||||
{' / '}
|
||||
{formatQuota(totalQuota)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{detailsCount > 0 && (
|
||||
<DataTableCardDetails count={detailsCount}>
|
||||
{visibleColumnIds.has('status') && (
|
||||
<DataTableCardField label={t('Status')} contentMode='full'>
|
||||
{renderApiKeyCell(props.row, 'status')}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('group') && (
|
||||
<DataTableCardField label={t('Group')} contentMode='full'>
|
||||
{renderApiKeyCell(props.row, 'group')}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('model_limits') && (
|
||||
<DataTableCardField label={t('Models')} span={2} contentMode='full'>
|
||||
<ApiKeyModels apiKey={apiKey} />
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('allow_ips') && (
|
||||
<DataTableCardField
|
||||
label={t('IP Restriction')}
|
||||
span={2}
|
||||
contentMode='full'
|
||||
>
|
||||
<ApiKeyIpRestrictions apiKey={apiKey} />
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('created_time') && (
|
||||
<DataTableCardField label={t('Created')} contentMode='full'>
|
||||
{renderApiKeyCell(props.row, 'created_time')}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('accessed_time') && (
|
||||
<DataTableCardField label={t('Last Used')} contentMode='full'>
|
||||
{renderApiKeyCell(props.row, 'accessed_time')}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('expired_time') && (
|
||||
<DataTableCardField
|
||||
label={t('Expires')}
|
||||
span={2}
|
||||
contentMode='full'
|
||||
>
|
||||
{renderApiKeyCell(props.row, 'expired_time')}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
{visibleColumnIds.has('actions') && (
|
||||
<DataTableCardField
|
||||
label={t('Operations')}
|
||||
span={2}
|
||||
contentMode='full'
|
||||
>
|
||||
{renderApiKeyCell(props.row, 'actions')}
|
||||
</DataTableCardField>
|
||||
)}
|
||||
</DataTableCardDetails>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -20,8 +20,7 @@ import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -29,7 +28,8 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
} from '@/components/design-system/command'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -139,7 +139,7 @@ export function ApiKeyGroupCombobox({
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
className='border-input bg-muted/40 hover:bg-muted/55 hover:text-foreground active:bg-background data-popup-open:border-ring data-popup-open:bg-background data-popup-open:ring-ring/20 h-auto min-h-14 w-full justify-between gap-2 rounded-lg px-3 py-2 text-start shadow-none transition-[background-color,border-color,box-shadow] duration-150 data-popup-open:ring-[3px] sm:min-h-20 sm:gap-3 sm:px-4 sm:py-3'
|
||||
className='border-input bg-muted/40 hover:bg-muted/55 hover:text-foreground active:bg-background data-popup-open:border-ring data-popup-open:bg-background data-popup-open:ring-ring/20 h-auto min-h-14 w-full justify-between gap-2 rounded-lg px-3 py-2 text-start shadow-none transition-[background-color,border-color,box-shadow] duration-150 data-popup-open:ring-[3px] sm:h-auto sm:min-h-20 sm:gap-3 sm:px-4 sm:py-3'
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
+12
-24
@@ -22,8 +22,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { BadgeCell } from '@/components/data-table'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -99,7 +99,7 @@ export function ApiKeyCell({ apiKey }: { apiKey: ApiKey }) {
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='text-muted-foreground h-7 max-w-full min-w-0 justify-start truncate px-0 font-mono text-xs hover:bg-transparent aria-expanded:bg-transparent'
|
||||
className='text-muted-foreground max-w-full min-w-0 justify-start truncate px-0 font-mono text-xs hover:bg-transparent aria-expanded:bg-transparent'
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -135,8 +135,8 @@ export function ApiKeyCell({ apiKey }: { apiKey: ApiKey }) {
|
||||
render={
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='size-7 shrink-0'
|
||||
size='icon-sm'
|
||||
className='shrink-0'
|
||||
onClick={handleCopy}
|
||||
onFocus={() => {
|
||||
if (!resolvedFullKey) void resolveRealKey(apiKey.id)
|
||||
@@ -160,9 +160,7 @@ 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} />
|
||||
)
|
||||
return <StatusBadge variant='neutral'>{t('Unlimited')}</StatusBadge>
|
||||
}
|
||||
|
||||
const models = apiKey.model_limits.split(',').filter(Boolean)
|
||||
@@ -170,11 +168,9 @@ export function ModelLimitsCell({ apiKey }: { apiKey: ApiKey }) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<BadgeCell />}>
|
||||
<StatusBadge
|
||||
label={t('{{count}} model(s)', { count: models.length })}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral'>
|
||||
{t('{{count}} model(s)', { count: models.length })}
|
||||
</StatusBadge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top' className='max-w-xs'>
|
||||
<div className='max-h-[200px] space-y-0.5 overflow-y-auto text-xs'>
|
||||
@@ -194,13 +190,7 @@ export function IpRestrictionsCell({ apiKey }: { apiKey: ApiKey }) {
|
||||
const allowIps = apiKey.allow_ips?.trim()
|
||||
|
||||
if (!allowIps) {
|
||||
return (
|
||||
<StatusBadge
|
||||
label={t('No restriction')}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
/>
|
||||
)
|
||||
return <StatusBadge variant='neutral'>{t('No restriction')}</StatusBadge>
|
||||
}
|
||||
|
||||
const ips = allowIps
|
||||
@@ -211,11 +201,9 @@ export function IpRestrictionsCell({ apiKey }: { apiKey: ApiKey }) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<BadgeCell />}>
|
||||
<StatusBadge
|
||||
label={t('{{count}} IP(s)', { count: ips.length })}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral'>
|
||||
{t('{{count}} IP(s)', { count: ips.length })}
|
||||
</StatusBadge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top' className='max-w-xs'>
|
||||
<div className='max-h-[200px] space-y-0.5 overflow-y-auto text-xs'>
|
||||
|
||||
+72
-56
@@ -16,7 +16,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -30,7 +29,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { getUserGroups } from '@/lib/api'
|
||||
import { useGroupRatios } from '@/hooks/use-group-ratios'
|
||||
import { formatQuota, formatTimestampToDate } from '@/lib/format'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -44,32 +43,13 @@ import {
|
||||
import { DataTableRowActions } from './data-table-row-actions'
|
||||
|
||||
function getQuotaProgressColor(percentage: number): string {
|
||||
if (percentage <= 10)
|
||||
if (percentage <= 10) {
|
||||
return '[&_[data-slot=progress-indicator]]:bg-destructive'
|
||||
}
|
||||
if (percentage <= 30) return '[&_[data-slot=progress-indicator]]:bg-warning'
|
||||
return '[&_[data-slot=progress-indicator]]:bg-success'
|
||||
}
|
||||
|
||||
function useGroupRatios(): Record<string, number> {
|
||||
const { data } = useQuery({
|
||||
queryKey: ['user-groups'],
|
||||
queryFn: getUserGroups,
|
||||
staleTime: 0,
|
||||
select: (res) => {
|
||||
if (!res.success || !res.data) return {}
|
||||
const ratios: Record<string, number> = {}
|
||||
for (const [group, info] of Object.entries(res.data)) {
|
||||
if (typeof info.ratio === 'number') {
|
||||
ratios[group] = info.ratio
|
||||
}
|
||||
}
|
||||
return ratios
|
||||
},
|
||||
})
|
||||
|
||||
return data ?? {}
|
||||
}
|
||||
|
||||
export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
const { t } = useTranslation()
|
||||
const groupRatios = useGroupRatios()
|
||||
@@ -96,6 +76,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 40,
|
||||
meta: { cardRole: 'hidden' },
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
@@ -104,7 +85,11 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
<span className='font-medium'>{row.getValue('name')}</span>
|
||||
),
|
||||
size: 180,
|
||||
meta: { mobileTitle: true },
|
||||
meta: {
|
||||
cardRole: 'title',
|
||||
cardSpan: 2,
|
||||
contentMode: 'wrap',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
@@ -113,16 +98,18 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
const statusConfig = API_KEY_STATUSES[row.getValue('status') as number]
|
||||
if (!statusConfig) return null
|
||||
return (
|
||||
<StatusBadge
|
||||
label={t(statusConfig.label)}
|
||||
variant={statusConfig.variant}
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant={statusConfig.variant}>
|
||||
{t(statusConfig.label)}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
filterFn: (row, id, value) => value.includes(String(row.getValue(id))),
|
||||
size: 120,
|
||||
meta: { mobileBadge: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 10,
|
||||
contentMode: 'full',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'key',
|
||||
@@ -131,6 +118,12 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
cell: ({ row }) => <ApiKeyCell apiKey={row.original} />,
|
||||
enableSorting: false,
|
||||
size: 260,
|
||||
meta: {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 10,
|
||||
cardSpan: 2,
|
||||
contentMode: 'full',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'quota',
|
||||
@@ -139,13 +132,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
cell: ({ row }) => {
|
||||
const apiKey = row.original
|
||||
if (apiKey.unlimited_quota) {
|
||||
return (
|
||||
<StatusBadge
|
||||
label={t('Unlimited')}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
/>
|
||||
)
|
||||
return <StatusBadge variant='neutral'>{t('Unlimited')}</StatusBadge>
|
||||
}
|
||||
|
||||
const used = apiKey.used_quota
|
||||
@@ -187,6 +174,12 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
)
|
||||
},
|
||||
size: 170,
|
||||
meta: {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 20,
|
||||
cardSpan: 2,
|
||||
contentMode: 'full',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'group',
|
||||
@@ -204,11 +197,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
>
|
||||
<GroupBadge group='auto' />
|
||||
{apiKey.cross_group_retry && (
|
||||
<StatusBadge
|
||||
label={t('Cross-group')}
|
||||
variant='info'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='info'>{t('Cross-group')}</StatusBadge>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -231,7 +220,12 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
)
|
||||
},
|
||||
size: 160,
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 20,
|
||||
cardSpan: 2,
|
||||
contentMode: 'full',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'model_limits',
|
||||
@@ -240,7 +234,12 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
cell: ({ row }) => <ModelLimitsCell apiKey={row.original} />,
|
||||
enableSorting: false,
|
||||
size: 160,
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 30,
|
||||
cardSpan: 2,
|
||||
contentMode: 'full',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'allow_ips',
|
||||
@@ -249,7 +248,12 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
cell: ({ row }) => <IpRestrictionsCell apiKey={row.original} />,
|
||||
enableSorting: false,
|
||||
size: 160,
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 40,
|
||||
cardSpan: 2,
|
||||
contentMode: 'full',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_time',
|
||||
@@ -260,7 +264,11 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
</span>
|
||||
),
|
||||
size: 180,
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 50,
|
||||
contentMode: 'full',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'accessed_time',
|
||||
@@ -277,7 +285,11 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
)
|
||||
},
|
||||
size: 180,
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 60,
|
||||
contentMode: 'full',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'expired_time',
|
||||
@@ -285,13 +297,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
cell: ({ row }) => {
|
||||
const expiredTime = row.getValue('expired_time') as number
|
||||
if (expiredTime === -1) {
|
||||
return (
|
||||
<StatusBadge
|
||||
label={t('Never')}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
/>
|
||||
)
|
||||
return <StatusBadge variant='neutral'>{t('Never')}</StatusBadge>
|
||||
}
|
||||
const isExpired = expiredTime * 1000 < Date.now()
|
||||
return (
|
||||
@@ -306,13 +312,23 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
)
|
||||
},
|
||||
size: 180,
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 70,
|
||||
contentMode: 'full',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => t('Actions'),
|
||||
cell: ({ row }) => <DataTableRowActions row={row} />,
|
||||
meta: { pinned: 'right' as const },
|
||||
meta: {
|
||||
pinned: 'right' as const,
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 80,
|
||||
cardSpan: 2,
|
||||
contentMode: 'full',
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
} from '@/components/design-system/alert-dialog'
|
||||
|
||||
import { deleteApiKey } from '../api'
|
||||
import { ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants'
|
||||
|
||||
@@ -25,6 +25,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { DateTimePicker } from '@/components/datetime-picker'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
SideDrawerSection,
|
||||
SideDrawerSectionHeader,
|
||||
@@ -35,7 +37,6 @@ import {
|
||||
sideDrawerSwitchItemClassName,
|
||||
} from '@/components/drawer-layout'
|
||||
import { MultiSelect } from '@/components/multi-select'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
@@ -50,7 +51,6 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
@@ -364,8 +364,6 @@ export function ApiKeysMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='px-2 text-xs sm:px-3 sm:text-sm'
|
||||
onClick={() => handleSetExpiry(0, 0, 0)}
|
||||
>
|
||||
{t('Never')}
|
||||
@@ -373,8 +371,6 @@ export function ApiKeysMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='px-2 text-xs sm:px-3 sm:text-sm'
|
||||
onClick={() => handleSetExpiry(1, 0, 0)}
|
||||
>
|
||||
{t('1 Month')}
|
||||
@@ -382,8 +378,6 @@ export function ApiKeysMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='px-2 text-xs sm:px-3 sm:text-sm'
|
||||
onClick={() => handleSetExpiry(0, 1, 0)}
|
||||
>
|
||||
{t('1 Day')}
|
||||
@@ -391,8 +385,6 @@ export function ApiKeysMutateDrawer({
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='px-2 text-xs sm:px-3 sm:text-sm'
|
||||
onClick={() => handleSetExpiry(0, 0, 1)}
|
||||
>
|
||||
{t('1 Hour')}
|
||||
|
||||
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { Plus } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
|
||||
import { useApiKeys } from './api-keys-provider'
|
||||
|
||||
@@ -28,7 +28,7 @@ export function ApiKeysPrimaryButtons() {
|
||||
const { setOpen } = useApiKeys()
|
||||
return (
|
||||
<div className='flex gap-2'>
|
||||
<Button size='sm' onClick={() => setOpen('create')}>
|
||||
<Button onClick={() => setOpen('create')}>
|
||||
<Plus className='h-4 w-4' />
|
||||
{t('Create API Key')}
|
||||
</Button>
|
||||
|
||||
+26
-136
@@ -18,8 +18,6 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getRouteApi } from '@tanstack/react-router'
|
||||
import { type Table as TanstackTable } from '@tanstack/react-table'
|
||||
import { Database } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
@@ -27,36 +25,24 @@ import {
|
||||
DISABLED_ROW_DESKTOP,
|
||||
DISABLED_ROW_MOBILE,
|
||||
DataTablePage,
|
||||
MobileCardList,
|
||||
useDebouncedColumnFilter,
|
||||
useDataTable,
|
||||
} from '@/components/data-table'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from '@/components/ui/empty'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { useTableUrlState } from '@/hooks/use-table-url-state'
|
||||
import { formatQuota } from '@/lib/format'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { getApiKeys, searchApiKeys } from '../api'
|
||||
import {
|
||||
API_KEY_STATUS,
|
||||
API_KEY_STATUS_OPTIONS,
|
||||
API_KEY_STATUSES,
|
||||
ERROR_MESSAGES,
|
||||
} from '../constants'
|
||||
import { type ApiKey } from '../types'
|
||||
import { ApiKeyCell } from './api-keys-cells'
|
||||
import type { ApiKey } from '../types'
|
||||
import { ApiKeyCard } from './api-key-card'
|
||||
import { useApiKeysColumns } from './api-keys-columns'
|
||||
import { useApiKeys } from './api-keys-provider'
|
||||
import { DataTableBulkActions } from './data-table-bulk-actions'
|
||||
import { DataTableRowActions } from './data-table-row-actions'
|
||||
|
||||
const route = getRouteApi('/_authenticated/keys/')
|
||||
const API_KEYS_COLUMN_VISIBILITY_STORAGE_KEY = 'api-keys:column-visibility'
|
||||
@@ -65,122 +51,6 @@ function isDisabledApiKeyRow(apiKey: ApiKey) {
|
||||
return apiKey.status !== API_KEY_STATUS.ENABLED
|
||||
}
|
||||
|
||||
function ApiKeysMobileSkeleton() {
|
||||
return (
|
||||
<div className='divide-border overflow-hidden rounded-lg border'>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='space-y-2 border-b px-3 py-2.5 last:border-b-0'
|
||||
>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Skeleton className='h-4 w-32' />
|
||||
<Skeleton className='h-5 w-16 rounded-md' />
|
||||
</div>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<Skeleton className='h-7 w-44' />
|
||||
<Skeleton className='h-8 w-16' />
|
||||
</div>
|
||||
<Skeleton className='h-3 w-28' />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ApiKeysMobileList({
|
||||
table,
|
||||
isLoading,
|
||||
}: {
|
||||
table: TanstackTable<ApiKey>
|
||||
isLoading: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const rows = table.getRowModel().rows
|
||||
|
||||
if (isLoading) return <ApiKeysMobileSkeleton />
|
||||
|
||||
if (!rows.length) {
|
||||
return (
|
||||
<div className='rounded-lg border p-8'>
|
||||
<Empty className='border-none p-0'>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant='icon'>
|
||||
<Database className='size-6' />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>{t('No API Keys Found')}</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
{t(
|
||||
'No API keys available. Create your first API key to get started.'
|
||||
)}
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='divide-border overflow-hidden rounded-lg border'>
|
||||
{rows.map((row) => {
|
||||
const apiKey = row.original
|
||||
const statusConfig = API_KEY_STATUSES[apiKey.status]
|
||||
const total = apiKey.used_quota + apiKey.remain_quota
|
||||
|
||||
return (
|
||||
<div
|
||||
key={row.id}
|
||||
className={cn(
|
||||
'bg-card space-y-2.5 border-b px-3 py-2.5 last:border-b-0',
|
||||
isDisabledApiKeyRow(apiKey) && DISABLED_ROW_MOBILE
|
||||
)}
|
||||
>
|
||||
<div className='flex items-start justify-between gap-3'>
|
||||
<div className='min-w-0'>
|
||||
<div className='truncate text-sm font-semibold'>
|
||||
{apiKey.name}
|
||||
</div>
|
||||
<div className='text-muted-foreground text-xs'>
|
||||
{t('API Key')}
|
||||
</div>
|
||||
</div>
|
||||
{statusConfig && (
|
||||
<StatusBadge
|
||||
label={t(statusConfig.label)}
|
||||
variant={statusConfig.variant}
|
||||
copyable={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex min-w-0 items-center justify-between gap-2'>
|
||||
<div className='min-w-0 flex-1 [&_button:first-child]:max-w-full [&_button:first-child]:truncate [&_button:first-child]:px-0'>
|
||||
<ApiKeyCell apiKey={apiKey} />
|
||||
</div>
|
||||
<DataTableRowActions row={row} />
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between gap-2 text-xs'>
|
||||
<span className='text-muted-foreground'>{t('Quota')}</span>
|
||||
{apiKey.unlimited_quota ? (
|
||||
<span className='font-medium'>{t('Unlimited')}</span>
|
||||
) : (
|
||||
<span className='font-medium tabular-nums'>
|
||||
{formatQuota(apiKey.remain_quota)}
|
||||
<span className='text-muted-foreground font-normal'>
|
||||
{' / '}
|
||||
{formatQuota(total)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ApiKeysTable() {
|
||||
const { t } = useTranslation()
|
||||
const { refreshTrigger } = useApiKeys()
|
||||
@@ -197,7 +67,11 @@ export function ApiKeysTable() {
|
||||
} = useTableUrlState({
|
||||
search: route.useSearch(),
|
||||
navigate: route.useNavigate(),
|
||||
pagination: { defaultPage: 1, defaultPageSize: 20 },
|
||||
pagination: {
|
||||
defaultPage: 1,
|
||||
defaultPageSize: 20,
|
||||
pageSizeStorageKey: 'api-keys:page-size:v1',
|
||||
},
|
||||
globalFilter: { enabled: true, key: 'filter' },
|
||||
columnFilters: [
|
||||
{ columnId: 'status', searchKey: 'status', type: 'array' },
|
||||
@@ -283,6 +157,7 @@ export function ApiKeysTable() {
|
||||
<DataTablePage
|
||||
table={table}
|
||||
columns={columns}
|
||||
tableLabel={t('API Keys')}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
emptyTitle={t('No API Keys Found')}
|
||||
@@ -293,6 +168,8 @@ export function ApiKeysTable() {
|
||||
applyHeaderSize
|
||||
toolbarProps={{
|
||||
searchPlaceholder: t('Filter by name...'),
|
||||
hasAdditionalFilters: Boolean(tokenFilterInput.trim()),
|
||||
onReset: () => setTokenFilterInput(''),
|
||||
additionalSearch: (
|
||||
<Input
|
||||
placeholder={t('Filter by API key...')}
|
||||
@@ -311,7 +188,20 @@ export function ApiKeysTable() {
|
||||
},
|
||||
],
|
||||
}}
|
||||
mobile={<ApiKeysMobileList table={table} isLoading={isLoading} />}
|
||||
mobile={
|
||||
<MobileCardList
|
||||
table={table}
|
||||
isLoading={isLoading}
|
||||
emptyTitle={t('No API Keys Found')}
|
||||
emptyDescription={t(
|
||||
'No API keys available. Create your first API key to get started.'
|
||||
)}
|
||||
renderCard={(row) => <ApiKeyCard row={row} />}
|
||||
getRowClassName={(row) =>
|
||||
isDisabledApiKeyRow(row.original) ? DISABLED_ROW_MOBILE : undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
getRowClassName={(row) =>
|
||||
isDisabledApiKeyRow(row.original) ? DISABLED_ROW_DESKTOP : undefined
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -89,7 +89,6 @@ export function DataTableBulkActions<TData>({
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
className='size-8'
|
||||
onClick={handleBatchCopy}
|
||||
disabled={isCopying}
|
||||
aria-label={t('Copy selected keys')}
|
||||
@@ -114,7 +113,6 @@ export function DataTableBulkActions<TData>({
|
||||
variant='destructive'
|
||||
size='icon'
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className='size-8'
|
||||
aria-label={t('Delete selected API keys')}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { DataTableRowActionMenu } from '@/components/data-table/core/row-action-menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
|
||||
@@ -21,9 +21,9 @@ import { useState, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { ComboboxInput } from '@/components/design-system/combobox-input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ComboboxInput } from '@/components/ui/combobox-input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { getUserModels } from '@/lib/api'
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { type StatusBadgeProps } from '@/components/status-badge'
|
||||
import type { StatusBadgeProps } from '@/components/status-badge'
|
||||
|
||||
// ============================================================================
|
||||
// API Key Status Configuration
|
||||
@@ -54,7 +54,7 @@ export const API_KEY_STATUSES: Record<
|
||||
},
|
||||
[API_KEY_STATUS.EXHAUSTED]: {
|
||||
label: 'Exhausted',
|
||||
variant: 'danger',
|
||||
variant: 'destructive',
|
||||
value: API_KEY_STATUS.EXHAUSTED,
|
||||
},
|
||||
} as const
|
||||
|
||||
+1
-1
@@ -20,9 +20,9 @@ import { useQuery } from '@tanstack/react-query'
|
||||
import { FileWarning } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { PublicLayout } from '@/components/layout'
|
||||
import { RichContent } from '@/components/rich-content'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { isHttpUrl, isLikelyHtml } from '@/lib/content-format'
|
||||
|
||||
@@ -24,8 +24,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -103,7 +103,6 @@ export function DataTableBulkActions<TData>({
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={handleEnableAll}
|
||||
className='size-8'
|
||||
aria-label={t('Enable selected models')}
|
||||
title={t('Enable selected models')}
|
||||
/>
|
||||
@@ -124,7 +123,6 @@ export function DataTableBulkActions<TData>({
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={handleDisableAll}
|
||||
className='size-8'
|
||||
aria-label={t('Disable selected models')}
|
||||
title={t('Disable selected models')}
|
||||
/>
|
||||
@@ -145,7 +143,6 @@ export function DataTableBulkActions<TData>({
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={handleCopyNames}
|
||||
className='size-8'
|
||||
aria-label={t('Copy model names')}
|
||||
title={t('Copy model names')}
|
||||
/>
|
||||
@@ -166,7 +163,6 @@ export function DataTableBulkActions<TData>({
|
||||
variant='destructive'
|
||||
size='icon'
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className='size-8'
|
||||
aria-label={t('Delete selected models')}
|
||||
title={t('Delete selected models')}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { DataTableRowActionMenu } from '@/components/data-table/core/row-action-menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuShortcut,
|
||||
|
||||
@@ -29,8 +29,8 @@ import {
|
||||
import type { ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type LoadingPhase = 'idle' | 'settings' | 'connection' | 'done'
|
||||
|
||||
@@ -21,9 +21,9 @@ import { Eye, Info, Pencil, Settings2, Timer, Trash2 } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { DataTableRowActionMenu } from '@/components/data-table/core/row-action-menu'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { CopyableStatusBadge, StatusBadge } from '@/components/status-badge'
|
||||
import { TableId } from '@/components/table-id'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
@@ -53,7 +53,12 @@ export function useDeploymentsColumns(opts: {
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: t('ID'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 10,
|
||||
cardSpan: 2,
|
||||
contentMode: 'full',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const id = row.original.id
|
||||
return <TableId value={id} />
|
||||
@@ -65,17 +70,22 @@ export function useDeploymentsColumns(opts: {
|
||||
accessorFn: (row) =>
|
||||
row.container_name || row.deployment_name || row.name || '',
|
||||
header: t('Name'),
|
||||
meta: { mobileTitle: true },
|
||||
meta: {
|
||||
cardRole: 'title',
|
||||
cardSpan: 2,
|
||||
contentMode: 'wrap',
|
||||
},
|
||||
cell: ({ getValue }) => {
|
||||
const name = String(getValue() || '-') || '-'
|
||||
return (
|
||||
<StatusBadge
|
||||
label={name}
|
||||
<CopyableStatusBadge
|
||||
value={name}
|
||||
variant='neutral'
|
||||
copyText={name}
|
||||
size='sm'
|
||||
className='font-mono'
|
||||
/>
|
||||
className='h-auto overflow-visible [overflow-wrap:anywhere] whitespace-normal [&_[data-slot=status-badge-label]]:overflow-visible [&_[data-slot=status-badge-label]]:text-clip [&_[data-slot=status-badge-label]]:whitespace-normal'
|
||||
>
|
||||
{name}
|
||||
</CopyableStatusBadge>
|
||||
)
|
||||
},
|
||||
minSize: 220,
|
||||
@@ -83,7 +93,7 @@ export function useDeploymentsColumns(opts: {
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: t('Status'),
|
||||
meta: { mobileBadge: true },
|
||||
meta: { cardRole: 'badge', contentMode: 'wrap' },
|
||||
cell: ({ row }) => {
|
||||
const raw = row.original.status
|
||||
const key = normalizeDeploymentStatus(raw)
|
||||
@@ -93,12 +103,9 @@ export function useDeploymentsColumns(opts: {
|
||||
variant: 'neutral' as const,
|
||||
}
|
||||
return (
|
||||
<StatusBadge
|
||||
label={config.label}
|
||||
variant={config.variant}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant={config.variant} size='sm'>
|
||||
{config.label}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
@@ -125,15 +132,22 @@ export function useDeploymentsColumns(opts: {
|
||||
}
|
||||
return (
|
||||
<StatusBadge
|
||||
label={String(provider)}
|
||||
autoColor={String(provider)}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
className='h-auto overflow-visible [overflow-wrap:anywhere] whitespace-normal [&_[data-slot=status-badge-label]]:overflow-visible [&_[data-slot=status-badge-label]]:text-clip [&_[data-slot=status-badge-label]]:whitespace-normal'
|
||||
>
|
||||
{String(provider)}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
size: 140,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 10,
|
||||
cardSpan: 2,
|
||||
contentMode: 'wrap',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'time_remaining',
|
||||
@@ -166,12 +180,9 @@ export function useDeploymentsColumns(opts: {
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<span className='font-medium'>{remainingText}</span>
|
||||
{status === 'running' && percentRemain !== null ? (
|
||||
<StatusBadge
|
||||
label={`${percentRemain}%`}
|
||||
variant='info'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='info' size='sm'>
|
||||
{`${percentRemain}%`}
|
||||
</StatusBadge>
|
||||
) : null}
|
||||
</div>
|
||||
{remainingHuman ? (
|
||||
@@ -184,11 +195,22 @@ export function useDeploymentsColumns(opts: {
|
||||
},
|
||||
minSize: 220,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 20,
|
||||
cardSpan: 2,
|
||||
contentMode: 'full',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'hardware',
|
||||
header: t('Hardware'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 20,
|
||||
cardSpan: 2,
|
||||
contentMode: 'wrap',
|
||||
},
|
||||
accessorFn: (row) =>
|
||||
row.hardware_info || row.hardware_name || row.brand_name || '',
|
||||
cell: ({ row }) => {
|
||||
@@ -205,13 +227,15 @@ export function useDeploymentsColumns(opts: {
|
||||
return <span className='text-muted-foreground text-xs'>-</span>
|
||||
}
|
||||
return (
|
||||
<div className='flex max-w-full min-w-0 flex-nowrap items-center gap-2 overflow-hidden'>
|
||||
<StatusBadge
|
||||
label={String(hardware)}
|
||||
<div className='flex max-w-full min-w-0 flex-wrap items-center gap-2'>
|
||||
<CopyableStatusBadge
|
||||
value={String(hardware)}
|
||||
variant='neutral'
|
||||
copyText={String(hardware)}
|
||||
size='sm'
|
||||
/>
|
||||
className='h-auto overflow-visible [overflow-wrap:anywhere] whitespace-normal [&_[data-slot=status-badge-label]]:overflow-visible [&_[data-slot=status-badge-label]]:text-clip [&_[data-slot=status-badge-label]]:whitespace-normal'
|
||||
>
|
||||
{String(hardware)}
|
||||
</CopyableStatusBadge>
|
||||
{qty !== null ? (
|
||||
<span className='text-muted-foreground text-xs'>×{qty}</span>
|
||||
) : null}
|
||||
@@ -224,7 +248,11 @@ export function useDeploymentsColumns(opts: {
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: t('Created'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 30,
|
||||
contentMode: 'full',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
let ts: number | undefined
|
||||
if (typeof row.original.created_at === 'number') {
|
||||
|
||||
@@ -32,8 +32,7 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { useMediaQuery } from '@/hooks'
|
||||
} from '@/components/design-system/alert-dialog'
|
||||
import { useTableUrlState } from '@/hooks/use-table-url-state'
|
||||
|
||||
import { deleteDeployment, listDeployments, searchDeployments } from '../api'
|
||||
@@ -52,7 +51,6 @@ const route = getRouteApi('/_authenticated/models/$section')
|
||||
export function DeploymentsTable() {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const isMobile = useMediaQuery('(max-width: 640px)')
|
||||
|
||||
// URL state (use dedicated keys so it won't collide with metadata table)
|
||||
const {
|
||||
@@ -70,7 +68,8 @@ export function DeploymentsTable() {
|
||||
pageKey: 'dPage',
|
||||
pageSizeKey: 'dPageSize',
|
||||
defaultPage: 1,
|
||||
defaultPageSize: isMobile ? 8 : 10,
|
||||
defaultPageSize: 20,
|
||||
pageSizeStorageKey: 'model-deployments:page-size:v1',
|
||||
},
|
||||
globalFilter: { enabled: true, key: 'dFilter' },
|
||||
columnFilters: [
|
||||
@@ -220,6 +219,7 @@ export function DeploymentsTable() {
|
||||
<DataTablePage
|
||||
table={table}
|
||||
columns={columns}
|
||||
tableLabel={t('Deployments')}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
emptyTitle={t('No Deployments Found')}
|
||||
|
||||
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
|
||||
import { useModels } from './models-provider'
|
||||
|
||||
@@ -41,11 +41,11 @@ export function DescriptionCell({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='max-w-[150px]'>
|
||||
<div className='max-w-full sm:max-w-[150px]'>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={handleClick}
|
||||
className='text-muted-foreground hover:text-foreground block h-auto w-full cursor-pointer overflow-hidden p-0 text-left text-sm text-ellipsis whitespace-nowrap no-underline'
|
||||
className='text-muted-foreground hover:text-foreground block h-auto w-full cursor-pointer p-0 text-left text-sm [overflow-wrap:anywhere] whitespace-normal no-underline sm:h-auto sm:overflow-hidden sm:text-ellipsis sm:whitespace-nowrap'
|
||||
>
|
||||
{description}
|
||||
</Button>
|
||||
|
||||
+10
-10
@@ -24,6 +24,16 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/design-system/select'
|
||||
import {
|
||||
SideDrawerSection,
|
||||
sideDrawerContentClassName,
|
||||
@@ -32,7 +42,6 @@ import {
|
||||
sideDrawerHeaderClassName,
|
||||
} from '@/components/drawer-layout'
|
||||
import { MultiSelect } from '@/components/multi-select'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -41,15 +50,6 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
|
||||
+2
-2
@@ -22,9 +22,9 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
import { estimatePrice, extendDeployment, getDeployment } from '../../api'
|
||||
|
||||
+11
-14
@@ -21,9 +21,10 @@ import { ChevronLeft, ChevronRight, Loader2, Plus, Search } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { CopyableStatusBadge } from '@/components/status-badge'
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
@@ -31,7 +32,6 @@ import {
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from '@/components/ui/empty'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useIsMobile } from '@/hooks/use-mobile'
|
||||
|
||||
import { getMissingModels } from '../../api'
|
||||
@@ -124,18 +124,20 @@ export function MissingModelsDialog({
|
||||
bodyClassName='space-y-4'
|
||||
initialFocus={!isMobile}
|
||||
>
|
||||
{isLoading ? (
|
||||
{isLoading && (
|
||||
<div className='flex items-center justify-center py-12'>
|
||||
<Loader2 className='h-8 w-8 animate-spin' />
|
||||
</div>
|
||||
) : missingModels.length === 0 ? (
|
||||
)}
|
||||
{!isLoading && missingModels.length === 0 && (
|
||||
<div className='text-muted-foreground py-12 text-center'>
|
||||
<p>{t('No missing models found.')}</p>
|
||||
<p className='text-sm'>
|
||||
{t('All models in use are properly configured.')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
{!isLoading && missingModels.length > 0 && (
|
||||
<div className='flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto'>
|
||||
<div className='flex flex-shrink-0 items-center justify-between gap-3'>
|
||||
<div className='text-muted-foreground text-sm whitespace-nowrap'>
|
||||
@@ -177,14 +179,11 @@ export function MissingModelsDialog({
|
||||
className='flex items-center justify-between gap-3 p-3'
|
||||
>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<StatusBadge
|
||||
label={modelName}
|
||||
variant='neutral'
|
||||
copyText={modelName}
|
||||
/>
|
||||
<CopyableStatusBadge value={modelName} variant='neutral'>
|
||||
{modelName}
|
||||
</CopyableStatusBadge>
|
||||
</div>
|
||||
<Button
|
||||
size='sm'
|
||||
className='flex-shrink-0 gap-1'
|
||||
onClick={() => handleConfigureModel(modelName)}
|
||||
>
|
||||
@@ -207,7 +206,6 @@ export function MissingModelsDialog({
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
className='h-8 w-8'
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.max(1, prev - 1))
|
||||
}
|
||||
@@ -219,7 +217,6 @@ export function MissingModelsDialog({
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
className='h-8 w-8'
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(totalPages, prev + 1))
|
||||
}
|
||||
|
||||
+25
-45
@@ -32,11 +32,12 @@ import { toast } from 'sonner'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { StaticDataTable } from '@/components/data-table/static/static-data-table'
|
||||
import { StaticRowActions } from '@/components/data-table/static/static-row-actions'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { TableId } from '@/components/table-id'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -196,7 +197,7 @@ export function PrefillGroupManagementDialog({
|
||||
<div className='space-y-2'>
|
||||
<CardTitle className='flex flex-wrap items-center gap-2'>
|
||||
{group.name}
|
||||
<StatusBadge variant={meta.badge} size='sm' copyable={false}>
|
||||
<StatusBadge variant={meta.badge} size='sm'>
|
||||
{meta.label}
|
||||
<span className='text-muted-foreground/30'>·</span>
|
||||
<span className='text-muted-foreground font-mono'>
|
||||
@@ -238,30 +239,21 @@ export function PrefillGroupManagementDialog({
|
||||
<CardContent className='space-y-3'>
|
||||
<div className='text-muted-foreground flex flex-wrap items-center gap-2 text-xs font-medium tracking-wide uppercase'>
|
||||
<span>Items</span>
|
||||
<StatusBadge
|
||||
label={`${parsedItems.length} item${parsedItems.length === 1 ? '' : 's'}`}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<Badge variant='outline'>
|
||||
{`${parsedItems.length} item${parsedItems.length === 1 ? '' : 's'}`}
|
||||
</Badge>
|
||||
</div>
|
||||
{parsedItems.length > 0 ? (
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{parsedItems.slice(0, 6).map((item) => (
|
||||
<StatusBadge
|
||||
key={item}
|
||||
label={item}
|
||||
autoColor={item}
|
||||
size='sm'
|
||||
/>
|
||||
<StatusBadge key={item} variant='neutral' size='sm'>
|
||||
{item}
|
||||
</StatusBadge>
|
||||
))}
|
||||
{parsedItems.length > 6 && (
|
||||
<StatusBadge
|
||||
label={`+${parsedItems.length - 6} more`}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral' size='sm'>
|
||||
{`+${parsedItems.length - 6} more`}
|
||||
</StatusBadge>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
@@ -310,12 +302,9 @@ export function PrefillGroupManagementDialog({
|
||||
header: t('Type'),
|
||||
cellClassName: 'align-top',
|
||||
cell: ({ meta }) => (
|
||||
<StatusBadge
|
||||
label={meta.label}
|
||||
variant={meta.badge}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant={meta.badge} size='sm'>
|
||||
{meta.label}
|
||||
</StatusBadge>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -329,20 +318,14 @@ export function PrefillGroupManagementDialog({
|
||||
{parsedItems.length > 0 ? (
|
||||
<>
|
||||
{parsedItems.slice(0, 6).map((item) => (
|
||||
<StatusBadge
|
||||
key={item}
|
||||
label={item}
|
||||
autoColor={item}
|
||||
size='sm'
|
||||
/>
|
||||
<StatusBadge key={item} variant='neutral' size='sm'>
|
||||
{item}
|
||||
</StatusBadge>
|
||||
))}
|
||||
{parsedItems.length > 6 && (
|
||||
<StatusBadge
|
||||
label={`+${parsedItems.length - 6} more`}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral' size='sm'>
|
||||
{`+${parsedItems.length - 6} more`}
|
||||
</StatusBadge>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
@@ -408,12 +391,11 @@ export function PrefillGroupManagementDialog({
|
||||
>
|
||||
<div className='bg-muted/30 flex flex-wrap items-center justify-between gap-3 rounded-md border p-2 text-sm'>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<Button size='sm' onClick={onCreateGroup}>
|
||||
<Button onClick={onCreateGroup}>
|
||||
<Plus className='mr-2 h-4 w-4' />
|
||||
{t('New Group')}
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='ghost'
|
||||
onClick={() => refetchGroups()}
|
||||
disabled={isFetching}
|
||||
@@ -426,11 +408,9 @@ export function PrefillGroupManagementDialog({
|
||||
{t('Refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
<StatusBadge
|
||||
label={`${groups.length} group${groups.length === 1 ? '' : 's'}`}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
/>
|
||||
<Badge variant='outline'>
|
||||
{`${groups.length} group${groups.length === 1 ? '' : 's'}`}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-3'>
|
||||
|
||||
+2
-2
@@ -22,9 +22,9 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
import { checkClusterNameAvailability, updateDeploymentName } from '../../api'
|
||||
import { deploymentsQueryKeys } from '../../lib'
|
||||
|
||||
@@ -22,9 +22,9 @@ import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { useIsMobile } from '@/hooks/use-mobile'
|
||||
@@ -188,11 +188,7 @@ export function SyncWizardDialog({
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='font-medium'>{option.label}</span>
|
||||
{option.value === 'official' && (
|
||||
<StatusBadge
|
||||
label='Default'
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral'>Default</StatusBadge>
|
||||
)}
|
||||
</div>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
|
||||
+2
-2
@@ -25,8 +25,9 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
@@ -40,7 +41,6 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
import { getDeployment, updateDeployment } from '../../api'
|
||||
|
||||
+28
-49
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { type ColumnDef, type RowSelectionState } from '@tanstack/react-table'
|
||||
import type { ColumnDef, RowSelectionState } from '@tanstack/react-table'
|
||||
import {
|
||||
Search,
|
||||
Info,
|
||||
@@ -30,16 +30,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { DataTableView, useDataTable } from '@/components/data-table'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -47,7 +39,15 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
} from '@/components/design-system/select'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { useIsMobile } from '@/hooks/use-mobile'
|
||||
|
||||
import { applyUpstreamOverwrite } from '../../api'
|
||||
@@ -119,7 +119,7 @@ export function UpstreamConflictDialog({
|
||||
const [search, setSearch] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [pageIndex, setPageIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -213,12 +213,9 @@ export function UpstreamConflictDialog({
|
||||
{row.original.fieldKey}
|
||||
</span>
|
||||
{isMobile ? (
|
||||
<StatusBadge
|
||||
label={row.original.fieldLabel}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral' size='sm'>
|
||||
{row.original.fieldLabel}
|
||||
</StatusBadge>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,11 +232,7 @@ export function UpstreamConflictDialog({
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className={isMobile ? 'h-7 w-7 p-0' : 'h-7 gap-2 px-2 text-xs'}
|
||||
/>
|
||||
<Button variant='ghost' size={isMobile ? 'icon-sm' : 'sm'} />
|
||||
}
|
||||
>
|
||||
<MousePointerClick className='h-3.5 w-3.5' />
|
||||
@@ -247,25 +240,17 @@ export function UpstreamConflictDialog({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-[min(90vw,24rem)] space-y-4 text-sm'>
|
||||
<div>
|
||||
<StatusBadge
|
||||
label='Local'
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='mb-1'
|
||||
/>
|
||||
<StatusBadge variant='neutral' size='sm' className='mb-1'>
|
||||
Local
|
||||
</StatusBadge>
|
||||
<pre className='bg-muted rounded-md p-2 text-xs'>
|
||||
{formatValue(row.original.localValue)}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<StatusBadge
|
||||
label='Upstream'
|
||||
variant='info'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='mb-1'
|
||||
/>
|
||||
<StatusBadge variant='info' size='sm' className='mb-1'>
|
||||
Upstream
|
||||
</StatusBadge>
|
||||
<pre className='bg-muted rounded-md p-2 text-xs'>
|
||||
{formatValue(row.original.upstreamValue)}
|
||||
</pre>
|
||||
@@ -309,12 +294,9 @@ export function UpstreamConflictDialog({
|
||||
accessorKey: 'fieldLabel',
|
||||
header: 'Field',
|
||||
cell: ({ row }) => (
|
||||
<StatusBadge
|
||||
label={row.original.fieldLabel}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant='neutral' size='sm'>
|
||||
{row.original.fieldLabel}
|
||||
</StatusBadge>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 160,
|
||||
@@ -396,7 +378,7 @@ export function UpstreamConflictDialog({
|
||||
const payload: SyncOverwritePayload[] = Object.entries(groupedSelections)
|
||||
.map(([modelName, fields]) => ({
|
||||
model_name: modelName,
|
||||
fields: Array.from(fields),
|
||||
fields: [...fields],
|
||||
}))
|
||||
.filter((item) => item.fields.length > 0)
|
||||
|
||||
@@ -512,7 +494,6 @@ export function UpstreamConflictDialog({
|
||||
</div>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={clearSelections}
|
||||
disabled={!hasSelection}
|
||||
>
|
||||
@@ -560,7 +541,7 @@ export function UpstreamConflictDialog({
|
||||
setPageIndex(0)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className='h-8 w-[70px] text-xs sm:h-8 sm:w-[72px]'>
|
||||
<SelectTrigger className='w-[70px] text-xs sm:w-[72px]'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
@@ -578,7 +559,6 @@ export function UpstreamConflictDialog({
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
className='h-7 w-7 sm:h-8 sm:w-8'
|
||||
onClick={() =>
|
||||
setPageIndex((prev) => Math.max(0, prev - 1))
|
||||
}
|
||||
@@ -596,7 +576,6 @@ export function UpstreamConflictDialog({
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
className='h-7 w-7 sm:h-8 sm:w-8'
|
||||
onClick={() =>
|
||||
setPageIndex((prev) =>
|
||||
Math.min(totalPages - 1, prev + 1)
|
||||
|
||||
+2
-2
@@ -24,8 +24,9 @@ import { useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -35,7 +36,6 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
import { createVendor, updateVendor } from '../../api'
|
||||
|
||||
@@ -22,8 +22,8 @@ import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
@@ -145,13 +145,12 @@ export function ViewDetailsDialog({
|
||||
<span className='font-mono'>{deploymentId}</span>
|
||||
</div>
|
||||
<div className='grid grid-cols-2 gap-2 sm:flex sm:items-center'>
|
||||
<Button variant='outline' size='sm' onClick={handleCopyId}>
|
||||
<Button variant='outline' onClick={handleCopyId}>
|
||||
<Copy className='mr-2 h-4 w-4' />
|
||||
{t('Copy')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleRefresh}
|
||||
disabled={isFetchingDetails || isFetchingContainers}
|
||||
>
|
||||
@@ -257,7 +256,6 @@ export function ViewDetailsDialog({
|
||||
{url ? (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => window.open(url, '_blank')}
|
||||
>
|
||||
<ExternalLink className='mr-2 h-4 w-4' />
|
||||
|
||||
@@ -21,8 +21,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -30,7 +29,8 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
} from '@/components/design-system/select'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
|
||||
import { getDeploymentLogs, listDeploymentContainers } from '../../api'
|
||||
@@ -206,7 +206,6 @@ export function ViewLogsDialog({
|
||||
<div className='grid grid-cols-2 gap-2 sm:flex sm:flex-wrap sm:items-center'>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
refetchContainers()
|
||||
refetchLogs()
|
||||
@@ -222,7 +221,6 @@ export function ViewLogsDialog({
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleDownload}
|
||||
disabled={!logsText.trim()}
|
||||
>
|
||||
|
||||
+11
-11
@@ -25,6 +25,16 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/design-system/select'
|
||||
import {
|
||||
SideDrawerSection,
|
||||
sideDrawerContentClassName,
|
||||
@@ -35,7 +45,6 @@ import {
|
||||
} from '@/components/drawer-layout'
|
||||
import { JsonEditor } from '@/components/json-editor'
|
||||
import { TagInput } from '@/components/tag-input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
@@ -50,17 +59,8 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
@@ -870,7 +870,7 @@ export function ModelMutateDrawer({
|
||||
v !== null && handleFillEndpointTemplate(v)
|
||||
}
|
||||
>
|
||||
<SelectTrigger size='sm' className='w-[200px]'>
|
||||
<SelectTrigger className='w-[200px]'>
|
||||
<SelectValue placeholder={t('Load template...')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
|
||||
+49
-51
@@ -24,6 +24,16 @@ import { useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/design-system/select'
|
||||
import {
|
||||
SideDrawerSection,
|
||||
sideDrawerContentClassName,
|
||||
@@ -34,7 +44,6 @@ import {
|
||||
import { JsonEditor } from '@/components/json-editor'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { TagInput } from '@/components/tag-input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -44,15 +53,6 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
@@ -143,27 +143,28 @@ export function PrefillGroupFormDrawer({
|
||||
|
||||
const handleSubmit = async (values: PrefillGroupFormValues) => {
|
||||
setIsSaving(true)
|
||||
let items: string | string[]
|
||||
if (values.type === 'endpoint') {
|
||||
items = typeof values.items === 'string' ? values.items : ''
|
||||
} else {
|
||||
items = Array.isArray(values.items) ? values.items : []
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: values.name.trim(),
|
||||
type: values.type,
|
||||
description: values.description?.trim() || '',
|
||||
items:
|
||||
values.type === 'endpoint'
|
||||
? typeof values.items === 'string'
|
||||
? values.items
|
||||
: ''
|
||||
: Array.isArray(values.items)
|
||||
? values.items
|
||||
: [],
|
||||
items,
|
||||
}
|
||||
|
||||
try {
|
||||
const response = isEdit
|
||||
? await updatePrefillGroup({
|
||||
id: currentGroup!.id,
|
||||
...payload,
|
||||
})
|
||||
: await createPrefillGroup(payload)
|
||||
const response =
|
||||
isEdit && currentGroup
|
||||
? await updatePrefillGroup({
|
||||
id: currentGroup.id,
|
||||
...payload,
|
||||
})
|
||||
: await createPrefillGroup(payload)
|
||||
|
||||
if (response.success) {
|
||||
toast.success(
|
||||
@@ -185,6 +186,12 @@ export function PrefillGroupFormDrawer({
|
||||
|
||||
const meta =
|
||||
PREFILL_GROUP_TYPE_META[selectedType] || PREFILL_GROUP_TYPE_META.model
|
||||
let submitLabel = t('Create')
|
||||
if (isSaving) {
|
||||
submitLabel = t('Saving...')
|
||||
} else if (isEdit) {
|
||||
submitLabel = t('Save changes')
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||
@@ -277,22 +284,20 @@ export function PrefillGroupFormDrawer({
|
||||
<FormItem>
|
||||
<FormLabel>Group Type</FormLabel>
|
||||
<Select
|
||||
items={[
|
||||
...PREFILL_GROUP_TYPES.map((type) => ({
|
||||
value: type.value,
|
||||
label: (
|
||||
<div className='flex flex-col text-left'>
|
||||
<span className='font-medium'>{type.label}</span>
|
||||
<span
|
||||
data-prefill-description
|
||||
className='text-muted-foreground text-xs'
|
||||
>
|
||||
{type.description}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
})),
|
||||
]}
|
||||
items={PREFILL_GROUP_TYPES.map((type) => ({
|
||||
value: type.value,
|
||||
label: (
|
||||
<div className='flex flex-col text-left'>
|
||||
<span className='font-medium'>{type.label}</span>
|
||||
<span
|
||||
data-prefill-description
|
||||
className='text-muted-foreground text-xs'
|
||||
>
|
||||
{type.description}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
value={field.value}
|
||||
onValueChange={(value) =>
|
||||
value !== null &&
|
||||
@@ -335,12 +340,9 @@ export function PrefillGroupFormDrawer({
|
||||
<div className='border-border/60 flex flex-col gap-3 border-y py-4'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<h4 className='text-sm font-medium'>{t('Project')}</h4>
|
||||
<StatusBadge
|
||||
label={meta.label}
|
||||
variant={meta.badge}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant={meta.badge} size='sm'>
|
||||
{meta.label}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -399,11 +401,7 @@ export function PrefillGroupFormDrawer({
|
||||
</SheetClose>
|
||||
<Button type='submit' form='prefill-group-form' disabled={isSaving}>
|
||||
{isSaving && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
||||
{isSaving
|
||||
? t('Saving...')
|
||||
: isEdit
|
||||
? t('Save changes')
|
||||
: t('Create')}
|
||||
{submitLabel}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
|
||||
+114
-58
@@ -22,7 +22,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { BadgeCell, BadgeListCell } from '@/components/data-table'
|
||||
import { GroupBadge } from '@/components/group-badge'
|
||||
import { ProviderBadge } from '@/components/provider-badge'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { CopyableStatusBadge, StatusBadge } from '@/components/status-badge'
|
||||
import { TableId } from '@/components/table-id'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
@@ -94,7 +94,12 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: t('ID'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 10,
|
||||
cardSpan: 2,
|
||||
contentMode: 'full',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const id = row.getValue('id') as number
|
||||
return <TableId value={id} />
|
||||
@@ -106,7 +111,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
{
|
||||
accessorKey: 'icon',
|
||||
header: t('Icon'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: { cardRole: 'hidden' },
|
||||
cell: ({ row }) => {
|
||||
const model = row.original
|
||||
const iconKey =
|
||||
@@ -130,17 +135,22 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
{
|
||||
accessorKey: 'model_name',
|
||||
header: t('Model Name'),
|
||||
meta: { mobileTitle: true },
|
||||
meta: {
|
||||
cardRole: 'title',
|
||||
cardSpan: 2,
|
||||
contentMode: 'wrap',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const name = row.getValue('model_name') as string
|
||||
return (
|
||||
<StatusBadge
|
||||
label={name}
|
||||
<CopyableStatusBadge
|
||||
value={name}
|
||||
variant='neutral'
|
||||
copyText={name}
|
||||
size='sm'
|
||||
className='font-mono'
|
||||
/>
|
||||
className='h-auto overflow-visible [overflow-wrap:anywhere] whitespace-normal [&_[data-slot=status-badge-label]]:overflow-visible [&_[data-slot=status-badge-label]]:text-clip [&_[data-slot=status-badge-label]]:whitespace-normal'
|
||||
>
|
||||
{name}
|
||||
</CopyableStatusBadge>
|
||||
)
|
||||
},
|
||||
minSize: 200,
|
||||
@@ -161,18 +171,9 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
}
|
||||
|
||||
const badge = (
|
||||
<StatusBadge
|
||||
label={label}
|
||||
variant={
|
||||
(config.color === 'error' ? 'danger' : config.color) as
|
||||
| 'neutral'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'info'
|
||||
}
|
||||
size='sm'
|
||||
/>
|
||||
<StatusBadge variant={config.variant} size='sm'>
|
||||
{label}
|
||||
</StatusBadge>
|
||||
)
|
||||
|
||||
// Show tooltip with matched models for non-exact rules
|
||||
@@ -182,7 +183,9 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
model.matched_models.length > 0
|
||||
) {
|
||||
const matchedBadges = model.matched_models.map((m) => (
|
||||
<StatusBadge key={m} label={m} autoColor={m} size='sm' />
|
||||
<StatusBadge key={m} variant='neutral' size='sm'>
|
||||
{m}
|
||||
</StatusBadge>
|
||||
))
|
||||
|
||||
return (
|
||||
@@ -204,25 +207,27 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
},
|
||||
size: 140,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 10,
|
||||
contentMode: 'wrap',
|
||||
},
|
||||
},
|
||||
|
||||
// Status column
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: t('Status'),
|
||||
meta: { mobileBadge: true },
|
||||
meta: { cardRole: 'badge', contentMode: 'wrap' },
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as number
|
||||
const config =
|
||||
MODEL_STATUS_CONFIG[status as 0 | 1] || MODEL_STATUS_CONFIG[0]
|
||||
|
||||
return (
|
||||
<StatusBadge
|
||||
label={config.label}
|
||||
variant={config.variant}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge variant={config.variant} size='sm'>
|
||||
{config.label}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
@@ -249,8 +254,12 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
}
|
||||
|
||||
return (
|
||||
<BadgeCell>
|
||||
<ProviderBadge iconKey={vendor.icon} label={vendor.name} />
|
||||
<BadgeCell className='overflow-visible'>
|
||||
<ProviderBadge
|
||||
iconKey={vendor.icon}
|
||||
label={vendor.name}
|
||||
className='h-auto overflow-visible [overflow-wrap:anywhere] whitespace-normal [&_[data-slot=status-badge-label]]:overflow-visible [&_[data-slot=status-badge-label]]:text-clip [&_[data-slot=status-badge-label]]:whitespace-normal'
|
||||
/>
|
||||
</BadgeCell>
|
||||
)
|
||||
},
|
||||
@@ -260,13 +269,24 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
},
|
||||
size: 150,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 20,
|
||||
cardSpan: 2,
|
||||
contentMode: 'wrap',
|
||||
},
|
||||
},
|
||||
|
||||
// Description column
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: t('Description'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 20,
|
||||
cardSpan: 2,
|
||||
contentMode: 'summary',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const description = row.getValue('description') as string
|
||||
const modelName = row.getValue('model_name') as string
|
||||
@@ -283,14 +303,21 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
{
|
||||
accessorKey: 'tags',
|
||||
header: t('Tags'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 30,
|
||||
cardSpan: 2,
|
||||
contentMode: 'summary',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const tags = row.getValue('tags') as string
|
||||
const tagArray = parseModelTags(tags)
|
||||
return (
|
||||
<BadgeListCell
|
||||
items={tagArray.map((tag) => (
|
||||
<StatusBadge key={tag} label={tag} autoColor={tag} size='sm' />
|
||||
<StatusBadge key={tag} variant='neutral' size='sm'>
|
||||
{tag}
|
||||
</StatusBadge>
|
||||
))}
|
||||
/>
|
||||
)
|
||||
@@ -303,14 +330,21 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
{
|
||||
accessorKey: 'endpoints',
|
||||
header: t('Endpoints'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 40,
|
||||
cardSpan: 2,
|
||||
contentMode: 'summary',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const endpoints = row.getValue('endpoints') as string
|
||||
const endpointArray = formatEndpointsDisplay(endpoints)
|
||||
return (
|
||||
<BadgeListCell
|
||||
items={endpointArray.map((ep) => (
|
||||
<StatusBadge key={ep} label={ep} autoColor={ep} size='sm' />
|
||||
<StatusBadge key={ep} variant='neutral' size='sm'>
|
||||
{ep}
|
||||
</StatusBadge>
|
||||
))}
|
||||
/>
|
||||
)
|
||||
@@ -323,7 +357,12 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
{
|
||||
accessorKey: 'bound_channels',
|
||||
header: t('Bound Channels'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 50,
|
||||
cardSpan: 2,
|
||||
contentMode: 'summary',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const channels = row.getValue('bound_channels') as Array<{
|
||||
id: number
|
||||
@@ -336,10 +375,11 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
items={(channels ?? []).map((c) => (
|
||||
<StatusBadge
|
||||
key={`${c.id}-${c.name}-${c.type ?? ''}`}
|
||||
label={`${c.name} (${c.type})`}
|
||||
autoColor={c.name}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
/>
|
||||
>
|
||||
{`${c.name} (${c.type})`}
|
||||
</StatusBadge>
|
||||
))}
|
||||
/>
|
||||
)
|
||||
@@ -352,7 +392,12 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
{
|
||||
accessorKey: 'enable_groups',
|
||||
header: t('Enable Groups'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 60,
|
||||
cardSpan: 2,
|
||||
contentMode: 'summary',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const groups = row.getValue('enable_groups') as string[]
|
||||
return (
|
||||
@@ -371,7 +416,12 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
{
|
||||
accessorKey: 'quota_types',
|
||||
header: t('Quota Types'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 70,
|
||||
cardSpan: 2,
|
||||
contentMode: 'summary',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const quotaTypes = row.getValue('quota_types') as number[]
|
||||
return (
|
||||
@@ -381,17 +431,11 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
return (
|
||||
<StatusBadge
|
||||
key={qt}
|
||||
label={config?.label || String(qt)}
|
||||
variant={
|
||||
(config?.color === 'error' ? 'danger' : config?.color) as
|
||||
| 'neutral'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'info'
|
||||
}
|
||||
variant={config?.variant || 'neutral'}
|
||||
size='sm'
|
||||
/>
|
||||
>
|
||||
{config?.label || String(qt)}
|
||||
</StatusBadge>
|
||||
)
|
||||
})}
|
||||
/>
|
||||
@@ -405,16 +449,20 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
{
|
||||
accessorKey: 'sync_official',
|
||||
header: t('Official Sync'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 80,
|
||||
contentMode: 'wrap',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const syncOfficial = row.getValue('sync_official') as number
|
||||
return (
|
||||
<StatusBadge
|
||||
label={syncOfficial === 1 ? t('Official Sync') : t('No Sync')}
|
||||
variant={syncOfficial === 1 ? 'success' : 'warning'}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
>
|
||||
{syncOfficial === 1 ? t('Official Sync') : t('No Sync')}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
@@ -432,7 +480,11 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
{
|
||||
accessorKey: 'created_time',
|
||||
header: t('Created'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 90,
|
||||
contentMode: 'full',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const timestamp = row.getValue('created_time') as number
|
||||
return (
|
||||
@@ -448,7 +500,11 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
{
|
||||
accessorKey: 'updated_time',
|
||||
header: t('Updated'),
|
||||
meta: { mobileHidden: true },
|
||||
meta: {
|
||||
cardRole: 'secondary',
|
||||
cardOrder: 100,
|
||||
contentMode: 'full',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const timestamp = row.getValue('updated_time') as number
|
||||
return (
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -66,14 +66,14 @@ export function ModelsPrimaryButtons() {
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
{/* Create Model */}
|
||||
<Button onClick={handleCreateModel} size='sm'>
|
||||
<Button onClick={handleCreateModel}>
|
||||
<Plus className='h-4 w-4' />
|
||||
{t('Add Model')}
|
||||
</Button>
|
||||
|
||||
{/* More Actions */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger render={<Button variant='outline' size='sm' />}>
|
||||
<DropdownMenuTrigger render={<Button variant='outline' />}>
|
||||
<MoreHorizontal className='h-4 w-4' />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-56'>
|
||||
|
||||
@@ -22,7 +22,6 @@ import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { DataTablePage, useDataTable } from '@/components/data-table'
|
||||
import { useMediaQuery } from '@/hooks'
|
||||
import { useTableUrlState } from '@/hooks/use-table-url-state'
|
||||
|
||||
import { getModels, searchModels, getVendors } from '../api'
|
||||
@@ -41,7 +40,6 @@ const route = getRouteApi('/_authenticated/models/$section')
|
||||
export function ModelsTable() {
|
||||
const { t } = useTranslation()
|
||||
const { selectedVendor } = useModels()
|
||||
const isMobile = useMediaQuery('(max-width: 640px)')
|
||||
|
||||
// URL state management
|
||||
const {
|
||||
@@ -57,7 +55,8 @@ export function ModelsTable() {
|
||||
navigate: route.useNavigate(),
|
||||
pagination: {
|
||||
defaultPage: 1,
|
||||
defaultPageSize: isMobile ? 10 : DEFAULT_PAGE_SIZE,
|
||||
defaultPageSize: DEFAULT_PAGE_SIZE,
|
||||
pageSizeStorageKey: 'models:page-size:v1',
|
||||
},
|
||||
globalFilter: { enabled: true, key: 'filter' },
|
||||
columnFilters: [
|
||||
@@ -201,6 +200,7 @@ export function ModelsTable() {
|
||||
<DataTablePage
|
||||
table={table}
|
||||
columns={columns}
|
||||
tableLabel={t('Models')}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
emptyTitle={t('No Models Found')}
|
||||
|
||||
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import type { StatusBadgeProps } from '@/components/status-badge'
|
||||
|
||||
import { type PrefillGroup, type PrefillGroupFormValues } from '../types'
|
||||
import type { PrefillGroup, PrefillGroupFormValues } from '../types'
|
||||
|
||||
export type PrefillGroupType = PrefillGroup['type']
|
||||
|
||||
@@ -27,19 +27,19 @@ export const PREFILL_GROUP_TYPES = [
|
||||
value: 'model' as PrefillGroupType,
|
||||
label: 'Model Group',
|
||||
description: 'Reusable sets of models you can attach to channels.',
|
||||
badge: 'blue' as StatusBadgeProps['variant'],
|
||||
badge: 'neutral' as StatusBadgeProps['variant'],
|
||||
},
|
||||
{
|
||||
value: 'tag' as PrefillGroupType,
|
||||
label: 'Tag Group',
|
||||
description: 'Collections of metadata tags for bulk operations.',
|
||||
badge: 'purple' as StatusBadgeProps['variant'],
|
||||
badge: 'neutral' as StatusBadgeProps['variant'],
|
||||
},
|
||||
{
|
||||
value: 'endpoint' as PrefillGroupType,
|
||||
label: 'Endpoint Group',
|
||||
description: 'HTTP endpoint mappings shared across providers.',
|
||||
badge: 'cyan' as StatusBadgeProps['variant'],
|
||||
badge: 'neutral' as StatusBadgeProps['variant'],
|
||||
},
|
||||
] as const
|
||||
|
||||
@@ -98,13 +98,11 @@ export function parseEndpointKeys(items: PrefillGroup['items']): string[] {
|
||||
typeof items === 'string' ? JSON.parse(items || '{}') : (items as unknown)
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed
|
||||
.map((item) =>
|
||||
typeof item === 'string'
|
||||
? item
|
||||
: typeof item?.name === 'string'
|
||||
? item.name
|
||||
: ''
|
||||
)
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') return item
|
||||
if (typeof item?.name === 'string') return item.name
|
||||
return ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
|
||||
+26
-16
@@ -16,7 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { type TFunction } from 'i18next'
|
||||
import type { TFunction } from 'i18next'
|
||||
|
||||
import type { StatusBadgeProps } from '@/components/status-badge'
|
||||
|
||||
import type { NameRule, ModelStatus, SyncSource } from './types'
|
||||
|
||||
@@ -39,28 +41,33 @@ export function getNameRuleOptions(t: TFunction) {
|
||||
] as const
|
||||
}
|
||||
|
||||
export function getNameRuleConfig(
|
||||
t: TFunction
|
||||
): Record<NameRule, { label: string; color: string; description: string }> {
|
||||
export function getNameRuleConfig(t: TFunction): Record<
|
||||
NameRule,
|
||||
{
|
||||
label: string
|
||||
variant: StatusBadgeProps['variant']
|
||||
description: string
|
||||
}
|
||||
> {
|
||||
return {
|
||||
0: {
|
||||
label: t('Exact'),
|
||||
color: 'green',
|
||||
variant: 'neutral',
|
||||
description: t('Match model name exactly'),
|
||||
},
|
||||
1: {
|
||||
label: t('Prefix'),
|
||||
color: 'blue',
|
||||
variant: 'neutral',
|
||||
description: t('Match models starting with this name'),
|
||||
},
|
||||
2: {
|
||||
label: t('Contains'),
|
||||
color: 'orange',
|
||||
variant: 'neutral',
|
||||
description: t('Match models containing this name'),
|
||||
},
|
||||
3: {
|
||||
label: t('Suffix'),
|
||||
color: 'purple',
|
||||
variant: 'neutral',
|
||||
description: t('Match models ending with this name'),
|
||||
},
|
||||
}
|
||||
@@ -80,7 +87,10 @@ export function getModelStatusOptions(t: TFunction) {
|
||||
|
||||
export function getModelStatusConfig(
|
||||
t: TFunction
|
||||
): Record<ModelStatus, { label: string; variant: 'success' | 'neutral' }> {
|
||||
): Record<
|
||||
ModelStatus,
|
||||
{ label: string; variant: StatusBadgeProps['variant'] }
|
||||
> {
|
||||
return {
|
||||
1: { label: t('Enabled'), variant: 'success' },
|
||||
0: { label: t('Disabled'), variant: 'neutral' },
|
||||
@@ -119,15 +129,15 @@ export function getDeploymentStatusConfig(t: TFunction): Record<
|
||||
string,
|
||||
{
|
||||
label: string
|
||||
variant: 'success' | 'neutral' | 'warning' | 'danger'
|
||||
variant: StatusBadgeProps['variant']
|
||||
}
|
||||
> {
|
||||
return {
|
||||
running: { label: t('Running'), variant: 'success' },
|
||||
completed: { label: t('Completed'), variant: 'success' },
|
||||
failed: { label: t('Failed'), variant: 'danger' },
|
||||
error: { label: t('Failed'), variant: 'danger' },
|
||||
destroyed: { label: t('Destroyed'), variant: 'danger' },
|
||||
failed: { label: t('Failed'), variant: 'destructive' },
|
||||
error: { label: t('Failed'), variant: 'destructive' },
|
||||
destroyed: { label: t('Destroyed'), variant: 'destructive' },
|
||||
'deployment requested': {
|
||||
label: t('Deployment requested'),
|
||||
variant: 'warning',
|
||||
@@ -145,10 +155,10 @@ export function getDeploymentStatusConfig(t: TFunction): Record<
|
||||
|
||||
export function getQuotaTypeConfig(
|
||||
t: TFunction
|
||||
): Record<number, { label: string; color: string }> {
|
||||
): Record<number, { label: string; variant: StatusBadgeProps['variant'] }> {
|
||||
return {
|
||||
0: { label: t('Usage-based'), color: 'violet' },
|
||||
1: { label: t('Per-call'), color: 'teal' },
|
||||
0: { label: t('Usage-based'), variant: 'neutral' },
|
||||
1: { label: t('Per-call'), variant: 'neutral' },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -22,9 +22,9 @@ import { Plus } from 'lucide-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/design-system/tabs'
|
||||
import { SectionPageLayout } from '@/components/layout'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
|
||||
import { listDeployments } from './api'
|
||||
import { DeploymentAccessGuard } from './components/deployment-access-guard'
|
||||
@@ -91,7 +91,7 @@ function ModelsContent() {
|
||||
{activeSection === 'metadata' ? (
|
||||
<ModelsPrimaryButtons />
|
||||
) : (
|
||||
<Button onClick={() => setCreateDeploymentOpen(true)} size='sm'>
|
||||
<Button onClick={() => setCreateDeploymentOpen(true)}>
|
||||
<Plus className='h-4 w-4' />
|
||||
{t('Create deployment')}
|
||||
</Button>
|
||||
@@ -100,7 +100,7 @@ function ModelsContent() {
|
||||
<SectionPageLayout.Content>
|
||||
<div className='flex h-full min-h-0 flex-col gap-4'>
|
||||
<Tabs value={activeSection} onValueChange={handleSectionChange}>
|
||||
<TabsList className='max-w-full flex-wrap justify-start group-data-horizontal/tabs:h-auto'>
|
||||
<TabsList className='max-w-full flex-wrap justify-start group-data-horizontal/tabs:h-auto sm:group-data-horizontal/tabs:h-auto'>
|
||||
{MODELS_SECTION_IDS.map((section) => (
|
||||
<TabsTrigger key={section} value={section}>
|
||||
{t(SECTION_META[section].titleKey)}
|
||||
@@ -143,7 +143,7 @@ function DeploymentsSection() {
|
||||
// Prefetch deployments list while connection check is in progress.
|
||||
useEffect(() => {
|
||||
if (isIoNetEnabled && loadingPhase === 'connection') {
|
||||
const defaultParams = { p: 1, page_size: 10 }
|
||||
const defaultParams = { p: 1, page_size: 20 }
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: deploymentsQueryKeys.list(defaultParams),
|
||||
queryFn: () => listDeployments(defaultParams),
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
|
||||
type PlaygroundEmptyStateProps = {
|
||||
onSelectPrompt: (prompt: string) => void
|
||||
@@ -67,7 +67,7 @@ export function PlaygroundEmptyState({
|
||||
|
||||
return (
|
||||
<Button
|
||||
className='h-auto min-h-11 justify-start gap-2 px-3 py-2.5 text-left whitespace-normal'
|
||||
className='h-auto min-h-11 justify-start gap-2 px-3 py-2.5 text-left whitespace-normal sm:h-auto'
|
||||
key={text}
|
||||
onClick={() => onSelectPrompt(prompt)}
|
||||
variant='outline'
|
||||
|
||||
+6
-7
@@ -20,8 +20,8 @@ import { SlidersHorizontalIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { PromptInputButton } from '@/components/ai-elements/prompt-input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Input } from '@/components/design-system/input'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -123,12 +123,12 @@ function PlaygroundParameterContent({
|
||||
>
|
||||
{t(control.labelKey)}
|
||||
</label>
|
||||
<Badge
|
||||
className='h-5 max-w-24 shrink-0 px-1.5 font-mono text-xs'
|
||||
variant='outline'
|
||||
<StatusBadge
|
||||
appearance='outline'
|
||||
className='max-w-24 shrink-0'
|
||||
>
|
||||
{t(getParameterControlValueText(control.key, value))}
|
||||
</Badge>
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<p className='text-muted-foreground text-xs leading-4'>
|
||||
{t(control.descriptionKey)}
|
||||
@@ -249,7 +249,6 @@ export function PlaygroundParameterPanel(props: PlaygroundParameterPanelProps) {
|
||||
<PopoverContent
|
||||
align='start'
|
||||
className='w-[22rem] max-w-[calc(100vw-2rem)] gap-3 p-3'
|
||||
collisionPadding={8}
|
||||
side='top'
|
||||
sideOffset={8}
|
||||
>
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/design-system/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -185,7 +185,7 @@ export function MessageActions({
|
||||
render={
|
||||
<Button
|
||||
aria-label={t('Open menu')}
|
||||
className='data-popup-open:bg-muted text-muted-foreground hover:text-foreground size-11'
|
||||
className='data-popup-open:bg-muted text-muted-foreground hover:text-foreground'
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user