✨ 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:
@@ -35,7 +35,7 @@ import {
|
||||
formatQuota as formatQuotaValue,
|
||||
} from '@/lib/format'
|
||||
import { getLobeIcon } from '@/lib/lobe-icon'
|
||||
import { cn, truncateText } from '@/lib/utils'
|
||||
import { truncateText } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
@@ -47,11 +47,8 @@ import {
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { DataTableColumnHeader } from '@/components/data-table/column-header'
|
||||
import { GroupBadge } from '@/components/group-badge'
|
||||
import {
|
||||
StatusBadge,
|
||||
dotColorMap,
|
||||
textColorMap,
|
||||
} from '@/components/status-badge'
|
||||
import { StatusBadge, StatusBadgeList } from '@/components/status-badge'
|
||||
import { TableId } from '@/components/table-id'
|
||||
import { TruncatedText } from '@/components/truncated-text'
|
||||
import { getCodexUsage } from '../api'
|
||||
import { CHANNEL_STATUS_CONFIG, MODEL_FETCHABLE_TYPES } from '../constants'
|
||||
@@ -107,25 +104,12 @@ function renderLimitedItems(
|
||||
items: React.ReactNode[],
|
||||
maxDisplay: number = 2
|
||||
): React.ReactNode {
|
||||
if (items.length === 0)
|
||||
return <span className='text-muted-foreground text-xs'>-</span>
|
||||
|
||||
const displayed = items.slice(0, maxDisplay)
|
||||
const remaining = items.length - maxDisplay
|
||||
|
||||
return (
|
||||
<div className='flex max-w-full items-center gap-1 overflow-hidden'>
|
||||
{displayed}
|
||||
{remaining > 0 && (
|
||||
<StatusBadge
|
||||
label={`+${remaining}`}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='flex-shrink-0'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<StatusBadgeList
|
||||
items={items}
|
||||
max={maxDisplay}
|
||||
renderItem={(item) => item}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -361,47 +345,50 @@ function BalanceCell({ channel }: { channel: Channel }) {
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className='flex items-center gap-1.5 text-xs font-medium'>
|
||||
<span
|
||||
className={cn(
|
||||
'size-1.5 shrink-0 rounded-full',
|
||||
dotColorMap[isUpdating ? 'neutral' : variant]
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<span className='text-muted-foreground cursor-help' />}
|
||||
>
|
||||
{usedDisplay}
|
||||
</TooltipTrigger>
|
||||
render={
|
||||
<StatusBadge
|
||||
label={usedDisplay}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='cursor-help'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{t('Used:')} {usedDisplay}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className='text-muted-foreground/30'>·</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span
|
||||
className={cn(
|
||||
'cursor-pointer transition-opacity hover:opacity-70',
|
||||
<StatusBadge
|
||||
label={
|
||||
isUpdating
|
||||
? t('Updating...')
|
||||
: channel.type === 57
|
||||
? t('Account Info')
|
||||
: remainingDisplay
|
||||
}
|
||||
variant={
|
||||
channel.type === 57
|
||||
? 'text-primary'
|
||||
: textColorMap[isUpdating ? 'neutral' : variant]
|
||||
)}
|
||||
? 'info'
|
||||
: isUpdating
|
||||
? 'neutral'
|
||||
: variant
|
||||
}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='cursor-pointer'
|
||||
onClick={handleClickUpdate}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isUpdating
|
||||
? 'Updating...'
|
||||
: channel.type === 57
|
||||
? t('Account Info')
|
||||
: remainingDisplay}
|
||||
</TooltipTrigger>
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{channel.type === 57
|
||||
@@ -491,15 +478,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const id = row.getValue('id') as number
|
||||
return (
|
||||
<StatusBadge
|
||||
label={String(id)}
|
||||
variant='neutral'
|
||||
copyText={String(id)}
|
||||
size='sm'
|
||||
className='font-mono'
|
||||
/>
|
||||
)
|
||||
return <TableId value={id} />
|
||||
},
|
||||
size: 80,
|
||||
},
|
||||
@@ -695,8 +674,13 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span className='text-muted-foreground/30'>·</span>
|
||||
<span className={cn(textColorMap.purple)}>IO.NET</span>
|
||||
<StatusBadge
|
||||
label='IO.NET'
|
||||
variant='purple'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='cursor-pointer'
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top'>
|
||||
<div className='max-w-xs space-y-1'>
|
||||
@@ -747,7 +731,6 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
<StatusBadge
|
||||
label={`Active (${childrenCount})`}
|
||||
variant='success'
|
||||
showDot
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
@@ -806,7 +789,6 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
<StatusBadge
|
||||
label={label}
|
||||
variant={config.variant}
|
||||
showDot={config.showDot}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
@@ -835,7 +817,6 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
<StatusBadge
|
||||
label={label}
|
||||
variant={config.variant}
|
||||
showDot={config.showDot}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
|
||||
+11
-5
@@ -75,6 +75,12 @@ import {
|
||||
} from '@/components/ui/tooltip'
|
||||
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
|
||||
import { DataTablePagination } from '@/components/data-table/pagination'
|
||||
import {
|
||||
sideDrawerContentClassName,
|
||||
sideDrawerFooterClassName,
|
||||
sideDrawerFormClassName,
|
||||
sideDrawerHeaderClassName,
|
||||
} from '@/components/drawer-layout'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { formatResponseTime, handleTestChannel } from '../../lib'
|
||||
import { useChannels } from '../channels-provider'
|
||||
@@ -833,19 +839,19 @@ function FailureDetailsSheet({
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
className={
|
||||
isMobile
|
||||
? 'max-h-[85dvh] gap-0 overflow-hidden rounded-t-xl p-0'
|
||||
: 'h-dvh w-full gap-0 overflow-hidden p-0 sm:max-w-lg'
|
||||
? sideDrawerContentClassName('h-auto max-h-[85dvh] rounded-t-xl')
|
||||
: sideDrawerContentClassName('sm:max-w-lg')
|
||||
}
|
||||
>
|
||||
{details && (
|
||||
<>
|
||||
<SheetHeader className='border-b px-4 py-3 text-start sm:px-5 sm:py-4'>
|
||||
<SheetHeader className={sideDrawerHeaderClassName('sm:px-5')}>
|
||||
<SheetTitle className='pr-10'>{t('Details')}</SheetTitle>
|
||||
<SheetDescription className='pr-10 wrap-break-word'>
|
||||
{details.model}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className='min-h-0 flex-1 space-y-4 overflow-y-auto px-4 py-3 sm:px-5 sm:py-4'>
|
||||
<div className={sideDrawerFormClassName('gap-4 sm:px-5')}>
|
||||
<section className='space-y-1'>
|
||||
<div className='text-muted-foreground text-xs font-medium'>
|
||||
{t('Model')}
|
||||
@@ -869,7 +875,7 @@ function FailureDetailsSheet({
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
<SheetFooter className='border-t px-4 py-3 sm:flex-row sm:justify-end sm:px-5'>
|
||||
<SheetFooter className={sideDrawerFooterClassName('sm:px-5')}>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full sm:w-auto'
|
||||
|
||||
+33
-25
@@ -99,6 +99,14 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import {
|
||||
sideDrawerContentClassName,
|
||||
sideDrawerFooterClassName,
|
||||
sideDrawerFormClassName,
|
||||
sideDrawerHeaderClassName,
|
||||
sideDrawerSectionClassName,
|
||||
sideDrawerSwitchItemClassName,
|
||||
} from '@/components/drawer-layout'
|
||||
import { JsonEditor } from '@/components/json-editor'
|
||||
import { MultiSelect } from '@/components/multi-select'
|
||||
import {
|
||||
@@ -269,9 +277,9 @@ function formatUnixTime(timestamp: unknown): string {
|
||||
|
||||
function CardHeading({ title, icon }: { title: string; icon?: ReactNode }) {
|
||||
return (
|
||||
<div className='flex items-center gap-2.5'>
|
||||
<div className='flex items-center gap-3'>
|
||||
{icon && (
|
||||
<span className='bg-primary/10 text-primary flex h-8 w-8 items-center justify-center rounded-lg'>
|
||||
<span className='bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-md'>
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
@@ -1087,10 +1095,10 @@ export function ChannelMutateDrawer({
|
||||
return (
|
||||
<>
|
||||
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||
<SheetContent className='flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-3xl'>
|
||||
<SheetHeader className='border-b px-4 py-3 text-start sm:px-6 sm:py-4'>
|
||||
<SheetContent className={sideDrawerContentClassName('sm:max-w-3xl')}>
|
||||
<SheetHeader className={sideDrawerHeaderClassName()}>
|
||||
<SheetTitle className='flex items-center gap-3'>
|
||||
<span className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border'>
|
||||
<span className='bg-muted flex size-9 shrink-0 items-center justify-center rounded-md'>
|
||||
{getLobeIcon(`${getChannelTypeIcon(currentType)}.Color`, 22)}
|
||||
</span>
|
||||
<span>
|
||||
@@ -1115,10 +1123,10 @@ export function ChannelMutateDrawer({
|
||||
<form
|
||||
id='channel-form'
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='flex-1 space-y-4 overflow-y-auto px-3 py-3 pb-4 sm:space-y-5 sm:px-4'
|
||||
className={sideDrawerFormClassName('gap-5')}
|
||||
>
|
||||
{/* ── Basic Information ── */}
|
||||
<div className='bg-card space-y-4 rounded-xl border p-3 sm:p-5'>
|
||||
<div className={sideDrawerSectionClassName()}>
|
||||
<CardHeading
|
||||
title={t('Basic Information')}
|
||||
icon={<Server className='h-4 w-4' />}
|
||||
@@ -1173,8 +1181,8 @@ export function ChannelMutateDrawer({
|
||||
control={form.control}
|
||||
name='status'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between rounded-lg border px-4 py-3'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormItem className={sideDrawerSwitchItemClassName()}>
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<FormLabel>{t('Enabled')}</FormLabel>
|
||||
<FormDescription className='text-xs'>
|
||||
{t('Enable or disable this channel')}
|
||||
@@ -1213,7 +1221,7 @@ export function ChannelMutateDrawer({
|
||||
</div>
|
||||
|
||||
{/* ── API Access ── */}
|
||||
<div className='bg-card space-y-4 rounded-xl border p-5'>
|
||||
<div className={sideDrawerSectionClassName()}>
|
||||
<CardHeading
|
||||
title={t('API Access')}
|
||||
icon={<Link2 className='h-4 w-4' />}
|
||||
@@ -1945,7 +1953,7 @@ export function ChannelMutateDrawer({
|
||||
</div>
|
||||
</FormDescription>
|
||||
{isEditing && (
|
||||
<div className='mt-4 space-y-3 rounded-lg border border-dashed p-4'>
|
||||
<div className='border-border/60 mt-4 flex flex-col gap-3 border-y border-dashed py-4'>
|
||||
<div className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div>
|
||||
<p className='text-sm font-medium'>
|
||||
@@ -2007,9 +2015,9 @@ export function ChannelMutateDrawer({
|
||||
/>
|
||||
|
||||
{currentType === 57 && (
|
||||
<div className='bg-muted/20 space-y-3 rounded-lg border p-4'>
|
||||
<div className='border-border/60 flex flex-col gap-3 border-y py-4'>
|
||||
<div className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div className='space-y-0.5'>
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<div className='text-sm font-semibold'>
|
||||
{t('Codex Authorization')}
|
||||
</div>
|
||||
@@ -2171,7 +2179,7 @@ export function ChannelMutateDrawer({
|
||||
</div>
|
||||
|
||||
{/* ── Models & Groups ── */}
|
||||
<div className='bg-card space-y-4 rounded-xl border p-5'>
|
||||
<div className={sideDrawerSectionClassName()}>
|
||||
<CardHeading
|
||||
title={t('Models & Groups')}
|
||||
icon={<Boxes className='h-4 w-4' />}
|
||||
@@ -2429,11 +2437,11 @@ export function ChannelMutateDrawer({
|
||||
render={
|
||||
<button
|
||||
type='button'
|
||||
className='bg-card hover:bg-accent/50 flex w-full items-center justify-between rounded-xl border px-5 py-4 text-left transition-colors'
|
||||
className='hover:bg-muted/40 flex w-full items-center justify-between rounded-md py-2 text-left transition-colors'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className='space-y-0.5'>
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<div className='text-[13px] font-semibold'>
|
||||
{t('Advanced Settings')}
|
||||
</div>
|
||||
@@ -2451,14 +2459,14 @@ export function ChannelMutateDrawer({
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent className='mt-5 space-y-5'>
|
||||
<CollapsibleContent className='mt-5 flex flex-col gap-5'>
|
||||
{/* ── Routing & Overrides ── */}
|
||||
<div className='bg-card space-y-4 rounded-xl border p-5'>
|
||||
<div className={sideDrawerSectionClassName()}>
|
||||
<CardHeading
|
||||
title={t('Routing & Overrides')}
|
||||
icon={<Route className='h-4 w-4' />}
|
||||
/>
|
||||
<div className='space-y-4'>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<SubHeading
|
||||
title={t('Routing Strategy')}
|
||||
icon={<Route className='h-3.5 w-3.5' />}
|
||||
@@ -2557,7 +2565,7 @@ export function ChannelMutateDrawer({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4 border-t pt-4'>
|
||||
<div className='flex flex-col gap-4 border-t pt-4'>
|
||||
<SubHeading
|
||||
title={t('Internal Notes')}
|
||||
icon={<FileText className='h-3.5 w-3.5' />}
|
||||
@@ -2606,7 +2614,7 @@ export function ChannelMutateDrawer({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4 border-t pt-4'>
|
||||
<div className='flex flex-col gap-4 border-t pt-4'>
|
||||
<SubHeading
|
||||
title={t('Override Rules')}
|
||||
icon={<Code className='h-3.5 w-3.5' />}
|
||||
@@ -2848,13 +2856,13 @@ export function ChannelMutateDrawer({
|
||||
</div>
|
||||
|
||||
{/* ── Extra Settings ── */}
|
||||
<div className='bg-card space-y-4 rounded-xl border p-5'>
|
||||
<div className={sideDrawerSectionClassName()}>
|
||||
<CardHeading
|
||||
title={t('Channel Extra Settings')}
|
||||
icon={<Settings className='h-4 w-4' />}
|
||||
/>
|
||||
{(currentType === 1 || currentType === 14) && (
|
||||
<div className='space-y-3 rounded-lg border p-4'>
|
||||
<div className='border-border/60 flex flex-col gap-3 border-y py-4'>
|
||||
<SubHeading
|
||||
title={t('Field passthrough controls')}
|
||||
icon={<SlidersHorizontal className='h-3.5 w-3.5' />}
|
||||
@@ -3220,7 +3228,7 @@ export function ChannelMutateDrawer({
|
||||
/>
|
||||
|
||||
{MODEL_FETCHABLE_TYPES.has(currentType) && (
|
||||
<div className='space-y-3 rounded-lg border p-4'>
|
||||
<div className='border-border/60 flex flex-col gap-3 border-y py-4'>
|
||||
<SubHeading
|
||||
title={t('Upstream Model Detection Settings')}
|
||||
icon={<RefreshCw className='h-3.5 w-3.5' />}
|
||||
@@ -3341,7 +3349,7 @@ export function ChannelMutateDrawer({
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<SheetFooter className='grid grid-cols-2 gap-2 border-t px-4 py-3 sm:flex sm:px-6 sm:py-4'>
|
||||
<SheetFooter className={sideDrawerFooterClassName()}>
|
||||
<SheetClose
|
||||
render={<Button variant='outline' disabled={isSubmitting} />}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user