✨ 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:
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user