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
@@ -87,7 +87,7 @@ export function ProviderTable(props: ProviderTableProps) {
{provider.icon ? (
<span className='text-lg'>{provider.icon}</span>
) : (
<span className='text-muted-foreground text-xs'>--</span>
<span className='text-muted-foreground text-sm'>--</span>
)}
</TableCell>
<TableCell className='font-medium'>{provider.name}</TableCell>
@@ -105,7 +105,7 @@ export function ProviderTable(props: ProviderTableProps) {
copyable={false}
/>
</TableCell>
<TableCell className='text-muted-foreground max-w-[120px] truncate font-mono text-xs'>
<TableCell className='text-muted-foreground max-w-[120px] truncate font-mono'>
{provider.client_id}
</TableCell>
<TableCell className='text-right'>
@@ -41,7 +41,7 @@ import {
} from '@/components/ui/table'
import { Textarea } from '@/components/ui/textarea'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { StatusBadge } from '@/components/status-badge'
import { StatusBadge, StatusBadgeList } from '@/components/status-badge'
import { SettingsSwitchField } from '../../components/settings-form-layout'
import { SettingsPageActionsPortal } from '../../components/settings-page-context'
import { SettingsSection } from '../../components/settings-section'
@@ -64,6 +64,24 @@ function parseRules(jsonStr: string): AffinityRule[] {
}
}
function RuleBadgeList(props: { items: string[] }) {
return (
<StatusBadgeList
items={props.items}
max={2}
getKey={(item) => item}
renderItem={(item) => (
<StatusBadge
label={item}
variant='neutral'
size='sm'
copyable={false}
/>
)}
/>
)
}
function serializeRules(rules: AffinityRule[]): string {
return JSON.stringify(rules.map(({ id: _, ...rest }) => rest))
}
@@ -500,65 +518,15 @@ export function ChannelAffinitySection(props: Props) {
{rule.name || '-'}
</TableCell>
<TableCell>
<div className='text-muted-foreground flex items-center gap-1.5 text-xs font-medium'>
{(rule.model_regex || []).length > 0 && (
<span
className='size-1.5 shrink-0 rounded-full bg-slate-400'
aria-hidden='true'
/>
)}
{(rule.model_regex || [])
.slice(0, 2)
.map((r, i, arr) => (
<span
key={i}
className='flex items-center gap-1.5'
>
{r}
{i < arr.length - 1 && (
<span className='text-muted-foreground/30'>
·
</span>
)}
</span>
))}
{(rule.model_regex || []).length > 2 && (
<span className='text-muted-foreground/50'>
+{(rule.model_regex || []).length - 2}
</span>
)}
</div>
<RuleBadgeList items={rule.model_regex || []} />
</TableCell>
<TableCell>
<div className='text-muted-foreground flex items-center gap-1.5 text-xs font-medium'>
{(rule.key_sources || []).length > 0 && (
<span
className='size-1.5 shrink-0 rounded-full bg-slate-400'
aria-hidden='true'
/>
<RuleBadgeList
items={(rule.key_sources || []).map(
(src) =>
`${src.type}:${src.type === 'gjson' ? src.path : src.key}`
)}
{(rule.key_sources || [])
.slice(0, 2)
.map((src, i, arr) => (
<span
key={i}
className='flex items-center gap-1.5'
>
{src.type}:
{src.type === 'gjson' ? src.path : src.key}
{i < arr.length - 1 && (
<span className='text-muted-foreground/30'>
·
</span>
)}
</span>
))}
{(rule.key_sources || []).length > 2 && (
<span className='text-muted-foreground/50'>
+{(rule.key_sources || []).length - 2}
</span>
)}
</div>
/>
</TableCell>
<TableCell>{rule.ttl_seconds || '-'}</TableCell>
<TableCell>
@@ -582,27 +550,7 @@ export function ChannelAffinitySection(props: Props) {
rule.include_rule_name && t('Rule'),
].filter(Boolean) as string[]
if (scopeItems.length === 0) return '-'
return (
<div className='text-muted-foreground flex items-center gap-1.5 text-xs font-medium'>
<span
className='size-1.5 shrink-0 rounded-full bg-slate-400'
aria-hidden='true'
/>
{scopeItems.map((item, idx, arr) => (
<span
key={idx}
className='flex items-center gap-1.5'
>
{item}
{idx < arr.length - 1 && (
<span className='text-muted-foreground/30'>
·
</span>
)}
</span>
))}
</div>
)
return <RuleBadgeList items={scopeItems} />
})()}
</TableCell>
<TableCell>
@@ -166,7 +166,7 @@ export function AmountDiscountVisualEditor({
</span>
</TableCell>
<TableCell>
<code className='bg-muted rounded px-1.5 py-0.5 text-xs'>
<code className='bg-muted rounded px-1.5 py-0.5 text-sm'>
{discount.discountRate.toFixed(2)}
</code>
</TableCell>
@@ -201,7 +201,7 @@ export function CreemProductsVisualEditor({
{product.name}
</TableCell>
<TableCell>
<code className='bg-muted rounded px-1.5 py-0.5 text-xs'>
<code className='bg-muted rounded px-1.5 py-0.5 text-sm'>
{product.productId}
</code>
</TableCell>
@@ -311,7 +311,7 @@ export function PaymentMethodsVisualEditor({
{method.name}
</TableCell>
<TableCell>
<code className='bg-muted rounded px-1.5 py-0.5 text-xs'>
<code className='bg-muted rounded px-1.5 py-0.5 text-sm'>
{method.type}
</code>
</TableCell>
@@ -323,7 +323,7 @@ export function PaymentMethodsVisualEditor({
style={{ backgroundColor: colorPreview }}
/>
)}
<span className='text-muted-foreground truncate font-mono text-xs'>
<span className='text-muted-foreground truncate font-mono text-sm'>
{method.color}
</span>
</div>
@@ -91,12 +91,12 @@ export function ConflictConfirmDialog({
{conflict.model}
</TableCell>
<TableCell>
<pre className='text-xs whitespace-pre-wrap'>
<pre className='text-sm whitespace-pre-wrap'>
{conflict.current}
</pre>
</TableCell>
<TableCell>
<pre className='text-xs whitespace-pre-wrap'>
<pre className='text-sm whitespace-pre-wrap'>
{conflict.newVal}
</pre>
</TableCell>
@@ -45,6 +45,11 @@ import {
} from '@/components/ui/sheet'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import {
sideDrawerContentClassName,
sideDrawerFormClassName,
sideDrawerHeaderClassName,
} from '@/components/drawer-layout'
import {
SettingsForm,
SettingsSwitchContent,
@@ -337,8 +342,11 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side='right' className='w-full gap-0 p-0 sm:max-w-2xl'>
<SheetHeader className='border-b p-4'>
<SheetContent
side='right'
className={sideDrawerContentClassName('sm:max-w-2xl')}
>
<SheetHeader className={sideDrawerHeaderClassName()}>
<SheetTitle>{t('Group pricing usage guide')}</SheetTitle>
<SheetDescription>
{t(
@@ -347,7 +355,7 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
</SheetDescription>
</SheetHeader>
<div className='space-y-5 overflow-y-auto p-4'>
<div className={sideDrawerFormClassName('gap-5')}>
<section className='space-y-2'>
<h3 className='text-sm font-semibold'>{t('Core concepts')}</h3>
<div className='text-muted-foreground space-y-2 text-sm leading-6'>
@@ -61,6 +61,10 @@ import {
SheetTitle,
} from '@/components/ui/sheet'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
sideDrawerContentClassName,
sideDrawerFooterClassName,
} from '@/components/drawer-layout'
import { combineBillingExpr } from '@/features/pricing/lib/billing-expr'
import {
SettingsControlGroup,
@@ -387,7 +391,10 @@ export function ModelPricingSheet({
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side='right' className='w-full gap-0 p-0 sm:max-w-2xl'>
<SheetContent
side='right'
className={sideDrawerContentClassName('sm:max-w-2xl')}
>
<SheetHeader className='sr-only'>
<SheetTitle>{title}</SheetTitle>
<SheetDescription>{description}</SheetDescription>
@@ -733,7 +740,7 @@ export function ModelPricingEditorPanel({
return (
<div
className={cn(
'bg-card flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border',
'bg-background flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border',
className
)}
>
@@ -948,7 +955,11 @@ export function ModelPricingEditorPanel({
</FieldGroup>
</div>
<SheetFooter className='bg-background/95 border-t sm:flex-row sm:items-center sm:justify-between'>
<SheetFooter
className={sideDrawerFooterClassName(
'grid-cols-1 sm:items-center sm:justify-between'
)}
>
<div className='text-muted-foreground text-xs'>
{selectedTargetCount > 0
? t('{{count}} selected targets available for bulk copy.', {