refactor(ui): Improve usage log filter responsiveness and mobile UX

Refactor the usage log filter toolbar into a shared reusable component for common, drawing, and task logs. Optimize desktop filters with a responsive grid, move secondary filters into a mobile drawer, standardize filter typography, remove redundant filter icons, and add the missing i18n translations for the new drawer description.
This commit is contained in:
t0ng7u
2026-05-25 05:35:44 +08:00
parent b302be30e3
commit 583da45296
79 changed files with 1879 additions and 1262 deletions
@@ -118,7 +118,6 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
<StatusBadge
label={t(statusConfig.label)}
variant={statusConfig.variant}
showDot={statusConfig.showDot}
copyable={false}
/>
)
@@ -212,12 +211,11 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
>
<GroupBadge group='auto' />
{apiKey.cross_group_retry && (
<>
<span className='text-muted-foreground/30'>·</span>
<span className='text-muted-foreground/60'>
{t('Cross-group')}
</span>
</>
<StatusBadge
label={t('Cross-group')}
variant='info'
copyable={false}
/>
)}
</TooltipTrigger>
<TooltipContent>
@@ -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 { useEffect, useState } from 'react'
import { ApiKeysDeleteDialog } from './api-keys-delete-dialog'
import { ApiKeysMutateDrawer } from './api-keys-mutate-drawer'
import { useApiKeys } from './api-keys-provider'
@@ -24,19 +23,6 @@ import { CCSwitchDialog } from './dialogs/cc-switch-dialog'
export function ApiKeysDialogs() {
const { open, setOpen, currentRow, resolvedKey } = useApiKeys()
const [lastMutateSide, setLastMutateSide] = useState<'left' | 'right'>(
'right'
)
const mutateSide =
open === 'create' ? 'left' : open === 'update' ? 'right' : lastMutateSide
useEffect(() => {
if (open === 'create') {
setLastMutateSide('left')
} else if (open === 'update') {
setLastMutateSide('right')
}
}, [open])
return (
<>
@@ -44,7 +30,6 @@ export function ApiKeysDialogs() {
open={open === 'create' || open === 'update'}
onOpenChange={(isOpen) => !isOpen && setOpen(null)}
currentRow={open === 'update' ? currentRow || undefined : undefined}
side={mutateSide}
/>
<ApiKeysDeleteDialog />
<CCSwitchDialog
@@ -16,17 +16,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useEffect, useState, type ReactNode } from 'react'
import { useEffect, useState } from 'react'
import { useForm, type SubmitErrorHandler } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useQuery } from '@tanstack/react-query'
import {
ChevronDown,
KeyRound,
Settings2,
WalletCards,
type LucideIcon,
} from 'lucide-react'
import { ChevronDown, KeyRound, Settings2, WalletCards } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { getUserModels, getUserGroups } from '@/lib/api'
@@ -61,6 +55,15 @@ import {
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import { DateTimePicker } from '@/components/datetime-picker'
import {
SideDrawerSection,
SideDrawerSectionHeader,
sideDrawerContentClassName,
sideDrawerFooterClassName,
sideDrawerFormClassName,
sideDrawerHeaderClassName,
sideDrawerSwitchItemClassName,
} from '@/components/drawer-layout'
import { MultiSelect } from '@/components/multi-select'
import { createApiKey, updateApiKey, getApiKey } from '../api'
import { ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants'
@@ -82,42 +85,12 @@ type ApiKeyMutateDrawerProps = {
open: boolean
onOpenChange: (open: boolean) => void
currentRow?: ApiKey
side?: 'left' | 'right'
}
type ApiKeyFormSectionProps = {
title: string
description: string
icon: LucideIcon
children: ReactNode
}
function ApiKeyFormSection(props: ApiKeyFormSectionProps) {
const Icon = props.icon
return (
<section className='bg-card rounded-lg border'>
<div className='flex items-center gap-2.5 border-b px-3 py-2.5 sm:gap-3 sm:px-4 sm:py-3'>
<div className='bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-lg border sm:size-10'>
<Icon className='size-4 sm:size-5' />
</div>
<div className='min-w-0'>
<h3 className='text-sm leading-none font-medium'>{props.title}</h3>
<p className='text-muted-foreground mt-0.5 text-xs sm:mt-1'>
{props.description}
</p>
</div>
</div>
<div className='space-y-3 p-3 sm:space-y-4 sm:p-4'>{props.children}</div>
</section>
)
}
export function ApiKeysMutateDrawer({
open,
onOpenChange,
currentRow,
side = 'right',
}: ApiKeyMutateDrawerProps) {
const { t } = useTranslation()
const isUpdate = !!currentRow
@@ -284,31 +257,30 @@ export function ApiKeysMutateDrawer({
}}
>
<SheetContent
side={side}
className='bg-background flex !h-dvh !w-screen max-w-none gap-0 overflow-hidden p-0 sm:!w-full sm:!max-w-[620px]'
className={sideDrawerContentClassName('max-w-none sm:!max-w-[620px]')}
>
<SheetHeader className='bg-background border-b px-4 py-3 text-start sm:px-5 sm:py-4'>
<SheetTitle className='text-base sm:text-lg'>
<SheetHeader className={sideDrawerHeaderClassName()}>
<SheetTitle>
{isUpdate ? t('Update API Key') : t('Create API Key')}
</SheetTitle>
<SheetDescription className='pr-6 text-xs sm:text-sm'>
<SheetDescription>
{isUpdate
? t('Update the API key by providing necessary info.')
: t('Add a new API key by providing necessary info.')}{' '}
{t("Click save when you're done.")}
: t('Add a new API key by providing necessary info.')}
</SheetDescription>
</SheetHeader>
<Form {...form}>
<form
id='api-key-form'
onSubmit={form.handleSubmit(onSubmit, onInvalid)}
className='min-h-0 flex-1 space-y-3 overflow-y-auto overscroll-contain px-3 py-3 sm:space-y-4 sm:px-4 sm:py-4'
className={sideDrawerFormClassName('gap-5')}
>
<ApiKeyFormSection
title={t('Basic Information')}
description={t('Set API key basic information')}
icon={KeyRound}
>
<SideDrawerSection>
<SideDrawerSectionHeader
title={t('Basic Information')}
description={t('Set API key basic information')}
icon={<KeyRound className='size-4' />}
/>
<FormField
control={form.control}
name='name'
@@ -347,8 +319,8 @@ export function ApiKeysMutateDrawer({
control={form.control}
name='cross_group_retry'
render={({ field }) => (
<FormItem className='flex min-h-16 flex-row items-center justify-between gap-3 rounded-lg border px-3 py-2.5 sm:min-h-20 sm:gap-4 sm:px-4 sm:py-3'>
<div className='space-y-0.5'>
<FormItem className={sideDrawerSwitchItemClassName()}>
<div className='flex flex-col gap-0.5'>
<FormLabel className='text-sm'>
{t('Cross-group retry')}
</FormLabel>
@@ -456,13 +428,14 @@ export function ApiKeysMutateDrawer({
)}
/>
)}
</ApiKeyFormSection>
</SideDrawerSection>
<ApiKeyFormSection
title={t('Quota Settings')}
description={t('Set quota amount and limits')}
icon={WalletCards}
>
<SideDrawerSection>
<SideDrawerSectionHeader
title={t('Quota Settings')}
description={t('Set quota amount and limits')}
icon={<WalletCards className='size-4' />}
/>
{!unlimitedQuota && (
<FormField
control={form.control}
@@ -498,8 +471,8 @@ export function ApiKeysMutateDrawer({
control={form.control}
name='unlimited_quota'
render={({ field }) => (
<FormItem className='flex min-h-16 flex-row items-center justify-between gap-3 rounded-lg border px-3 py-2.5 sm:min-h-20 sm:gap-4 sm:px-4 sm:py-3'>
<div className='space-y-0.5'>
<FormItem className={sideDrawerSwitchItemClassName()}>
<div className='flex flex-col gap-0.5'>
<FormLabel className='text-sm'>
{t('Unlimited Quota')}
</FormLabel>
@@ -516,29 +489,24 @@ export function ApiKeysMutateDrawer({
</FormItem>
)}
/>
</ApiKeyFormSection>
</SideDrawerSection>
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
<section className='bg-card rounded-lg border'>
<SideDrawerSection>
<CollapsibleTrigger
render={
<button
type='button'
className='hover:bg-muted/50 flex w-full items-center gap-2.5 px-3 py-2.5 text-left transition-colors sm:gap-3 sm:px-4 sm:py-3'
className='hover:bg-muted/40 flex w-full items-center gap-3 rounded-md py-1.5 text-left transition-colors'
/>
}
>
<div className='bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-lg border sm:size-10'>
<Settings2 className='size-4 sm:size-5' />
</div>
<div className='min-w-0 flex-1'>
<h3 className='text-sm leading-none font-medium'>
{t('Advanced Settings')}
</h3>
<p className='text-muted-foreground mt-1 text-xs'>
{t('Set API key access restrictions')}
</p>
</div>
<SideDrawerSectionHeader
className='flex-1'
title={t('Advanced Settings')}
description={t('Set API key access restrictions')}
icon={<Settings2 className='size-4' />}
/>
<ChevronDown
className={cn(
'text-muted-foreground size-4 shrink-0 transition-transform',
@@ -547,7 +515,7 @@ export function ApiKeysMutateDrawer({
/>
</CollapsibleTrigger>
<CollapsibleContent>
<div className='space-y-3 border-t p-3 sm:space-y-4 sm:p-4'>
<div className='flex flex-col gap-4 pt-2'>
<FormField
control={form.control}
name='model_limits'
@@ -604,11 +572,11 @@ export function ApiKeysMutateDrawer({
/>
</div>
</CollapsibleContent>
</section>
</SideDrawerSection>
</Collapsible>
</form>
</Form>
<SheetFooter className='bg-background grid grid-cols-2 gap-2 border-t px-3 py-3 sm:flex sm:flex-row sm:justify-end sm:px-5 sm:py-4'>
<SheetFooter className={sideDrawerFooterClassName()}>
<SheetClose
render={<Button variant='outline' className='w-full sm:w-auto' />}
>
@@ -153,7 +153,6 @@ function ApiKeysMobileList({
<StatusBadge
label={t(statusConfig.label)}
variant={statusConfig.variant}
showDot={statusConfig.showDot}
copyable={false}
/>
)}
+1 -5
View File
@@ -32,7 +32,7 @@ export const API_KEY_STATUS = {
export const API_KEY_STATUSES: Record<
number,
Pick<StatusBadgeProps, 'variant' | 'showDot'> & {
Pick<StatusBadgeProps, 'variant'> & {
label: string
value: number
}
@@ -41,25 +41,21 @@ export const API_KEY_STATUSES: Record<
label: 'Enabled',
variant: 'success',
value: API_KEY_STATUS.ENABLED,
showDot: true,
},
[API_KEY_STATUS.DISABLED]: {
label: 'Disabled',
variant: 'neutral',
value: API_KEY_STATUS.DISABLED,
showDot: true,
},
[API_KEY_STATUS.EXPIRED]: {
label: 'Expired',
variant: 'warning',
value: API_KEY_STATUS.EXPIRED,
showDot: true,
},
[API_KEY_STATUS.EXHAUSTED]: {
label: 'Exhausted',
variant: 'danger',
value: API_KEY_STATUS.EXHAUSTED,
showDot: true,
},
} as const