From 22ae14f0d7c6a90f906c34a14142addeeced71c0 Mon Sep 17 00:00:00 2001 From: CaIon Date: Wed, 29 Apr 2026 13:23:27 +0800 Subject: [PATCH] feat(ui): enhance ChannelsTable and CommonLogs components with improved UI elements --- .../components/data-table/faceted-filter.tsx | 22 ++- .../src/components/data-table/toolbar.tsx | 2 + .../channels/components/channels-table.tsx | 72 ++++++-- .../profile/components/profile-header.tsx | 155 ++++++++++-------- .../columns/common-logs-columns.tsx | 121 +++++--------- .../components/common-logs-stats.tsx | 59 +++---- .../usage-logs/components/model-badge.tsx | 144 ++++++++++++++++ .../wallet/components/wallet-stats-card.tsx | 78 ++++----- 8 files changed, 406 insertions(+), 247 deletions(-) create mode 100644 web/default/src/features/usage-logs/components/model-badge.tsx diff --git a/web/default/src/components/data-table/faceted-filter.tsx b/web/default/src/components/data-table/faceted-filter.tsx index 8b5ecd80..62ebe7fd 100644 --- a/web/default/src/components/data-table/faceted-filter.tsx +++ b/web/default/src/components/data-table/faceted-filter.tsx @@ -28,6 +28,8 @@ type DataTableFacetedFilterProps = { label: string value: string icon?: React.ComponentType<{ className?: string }> + iconNode?: React.ReactNode + count?: number }[] /** Enable single select mode (only one option can be selected at a time) */ singleSelect?: boolean @@ -130,15 +132,25 @@ export function DataTableFacetedFilter({ > - {option.icon && ( + {option.iconNode ? ( + + {option.iconNode} + + ) : option.icon ? ( - )} - {t(option.label)} - {facets?.get(option.value) && ( + ) : null} + + {t(option.label)} + + {typeof option.count === 'number' ? ( + + {option.count} + + ) : facets?.get(option.value) ? ( {facets.get(option.value)} - )} + ) : null} ) })} diff --git a/web/default/src/components/data-table/toolbar.tsx b/web/default/src/components/data-table/toolbar.tsx index 4d1b4395..f842e5c9 100644 --- a/web/default/src/components/data-table/toolbar.tsx +++ b/web/default/src/components/data-table/toolbar.tsx @@ -20,6 +20,8 @@ type DataTableToolbarProps = { label: string value: string icon?: React.ComponentType<{ className?: string }> + iconNode?: React.ReactNode + count?: number }[] singleSelect?: boolean }[] diff --git a/web/default/src/features/channels/components/channels-table.tsx b/web/default/src/features/channels/components/channels-table.tsx index 1c089d08..cf7509da 100644 --- a/web/default/src/features/channels/components/channels-table.tsx +++ b/web/default/src/features/channels/components/channels-table.tsx @@ -13,6 +13,7 @@ import { } from '@tanstack/react-table' import { useDebounce, useMediaQuery } from '@/hooks' import { useTranslation } from 'react-i18next' +import { getLobeIcon } from '@/lib/lobe-icon' import { cn } from '@/lib/utils' import { useTableUrlState } from '@/hooks/use-table-url-state' import { Input } from '@/components/ui/input' @@ -37,12 +38,13 @@ import { DEFAULT_PAGE_SIZE, CHANNEL_STATUS, CHANNEL_STATUS_OPTIONS, - CHANNEL_TYPE_OPTIONS, } from '../constants' import { channelsQueryKeys, aggregateChannelsByTag, isTagAggregateRow, + getChannelTypeIcon, + getChannelTypeLabel, } from '../lib' import type { Channel } from '../types' import { useChannelsColumns } from './channels-columns' @@ -52,7 +54,9 @@ import { DataTableBulkActions } from './data-table-bulk-actions' const route = getRouteApi('/_authenticated/channels/') function isDisabledChannelRow(channel: Channel) { - return !isTagAggregateRow(channel) && channel.status !== CHANNEL_STATUS.ENABLED + return ( + !isTagAggregateRow(channel) && channel.status !== CHANNEL_STATUS.ENABLED + ) } export function ChannelsTable() { @@ -264,17 +268,57 @@ export function ChannelsTable() { ensurePageInRange(pageCount) }, [pageCount, ensurePageInRange]) - // Prepare filter options (option.label are i18n keys; faceted-filter uses t(option.label)) - const typeFilterOptions = [ - { - label: `${t('All Types')}${typeCounts?.all ? ` (${typeCounts.all})` : ''}`, - value: 'all', - }, - ...CHANNEL_TYPE_OPTIONS.map((option) => ({ - label: `${t(option.label)}${typeCounts?.[option.value] ? ` (${typeCounts[option.value]})` : ''}`, - value: String(option.value), - })), - ] + // Prepare filter options from existing channel types only. + const typeFilterOptions = useMemo(() => { + const counts = typeCounts || {} + const typeIds = Object.entries(counts) + .map(([type, count]) => ({ + type: Number(type), + count: Number(count) || 0, + })) + .filter((item) => item.type > 0 && item.count > 0) + .sort((a, b) => { + const labelA = t(getChannelTypeLabel(a.type)) + const labelB = t(getChannelTypeLabel(b.type)) + return labelA.localeCompare(labelB) + }) + + const selectedType = typeFilter.find((value) => value !== 'all') + if (selectedType) { + const selectedTypeId = Number(selectedType) + const alreadyIncluded = typeIds.some( + (item) => item.type === selectedTypeId + ) + if (selectedTypeId > 0 && !alreadyIncluded) { + typeIds.push({ + type: selectedTypeId, + count: Number(counts[selectedType]) || 0, + }) + } + } + + const totalTypes = Object.values(counts).reduce( + (sum, count) => sum + (Number(count) || 0), + 0 + ) + + return [ + { + label: 'All Types', + value: 'all', + count: totalTypes, + }, + ...typeIds.map((item) => { + const iconName = getChannelTypeIcon(item.type) + return { + label: getChannelTypeLabel(item.type), + value: String(item.type), + count: item.count, + iconNode: getLobeIcon(`${iconName}.Color`, 16), + } + }), + ] + }, [t, typeCounts, typeFilter]) const groupFilterOptions = [ { label: t('All Groups'), value: 'all' }, @@ -375,7 +419,7 @@ export function ChannelsTable() { data-state={row.getIsSelected() && 'selected'} className={cn( isDisabledChannelRow(row.original) && - 'bg-muted/85 hover:bg-muted dark:bg-zinc-700/55 dark:hover:bg-zinc-700/70 [&>td:first-child]:border-l-4 [&>td:first-child]:border-l-muted-foreground/35 [&>td:first-child]:pl-1 dark:[&>td:first-child]:border-l-zinc-300/70' + 'bg-muted/85 hover:bg-muted [&>td:first-child]:border-l-muted-foreground/35 dark:bg-zinc-700/55 dark:hover:bg-zinc-700/70 [&>td:first-child]:border-l-4 [&>td:first-child]:pl-1 dark:[&>td:first-child]:border-l-zinc-300/70' )} > {row.getVisibleCells().map((cell) => ( diff --git a/web/default/src/features/profile/components/profile-header.tsx b/web/default/src/features/profile/components/profile-header.tsx index caff1682..b9f3c666 100644 --- a/web/default/src/features/profile/components/profile-header.tsx +++ b/web/default/src/features/profile/components/profile-header.tsx @@ -1,3 +1,4 @@ +import { Activity, BarChart3, WalletCards } from 'lucide-react' import { useTranslation } from 'react-i18next' import { formatCompactNumber, formatQuota } from '@/lib/format' import { getRoleLabel } from '@/lib/roles' @@ -21,31 +22,32 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) { if (loading) { return ( -
-
-
-
- -
-
- - -
-
- - - -
+
+
+
+ +
+
+ + +
+
+ + +
-
- {Array.from({ length: 3 }).map((_, i) => ( -
- - -
- ))} -
+
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ + + +
+ ))}
@@ -61,73 +63,82 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) { { label: t('Current Balance'), value: formatQuota(profile.quota), + description: t('Remaining quota'), + icon: WalletCards, }, { label: t('Total Usage'), value: formatQuota(profile.used_quota), + description: t('Total consumed quota'), + icon: BarChart3, }, { label: t('API Requests'), value: formatCompactNumber(profile.request_count), + description: t('Total requests made'), + icon: Activity, }, ] return ( -
-
-
-
- - - {initials} - - +
+
+
+ + + {initials} + + -
-
-

- {displayName} -

- -
+
+
+

+ {displayName} +

+ +
-
- @{profile.username} - {profile.email && ( - <> - - {profile.email} - - )} - {profile.group && ( - <> - - {profile.group} - - )} -
+
+ @{profile.username} + {profile.email && ( + <> + + {profile.email} + + )} + {profile.group && ( + <> + + {profile.group} + + )}
- -
- {stats.map((item) => ( -
-

+

+
+
+
+ {stats.map((item) => ( +
+
+ +
{item.label} -

-

- {item.value} -

+
- ))} -
+ +
+ {item.value} +
+
+ {item.description} +
+
+ ))}
diff --git a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx index 0e5a53f1..ee634681 100644 --- a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx +++ b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { type ColumnDef } from '@tanstack/react-table' -import { Route, CircleAlert, Sparkles, KeyRound } from 'lucide-react' +import { CircleAlert, Sparkles, KeyRound } from 'lucide-react' import { useTranslation } from 'react-i18next' import { formatBillingCurrencyFromUSD } from '@/lib/currency' import { @@ -10,11 +10,6 @@ import { } from '@/lib/format' import { cn } from '@/lib/utils' import { Avatar, AvatarFallback } from '@/components/ui/avatar' -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover' import { Tooltip, TooltipContent, @@ -25,8 +20,6 @@ import { DataTableColumnHeader } from '@/components/data-table' import { StatusBadge, type StatusBadgeProps, - dotColorMap, - textColorMap, } from '@/components/status-badge' import type { UsageLog } from '../../data/schema' import { @@ -47,6 +40,7 @@ import { } from '../../lib/utils' import type { LogOtherData } from '../../types' import { DetailsDialog } from '../dialogs/details-dialog' +import { ModelBadge } from '../model-badge' import { useUsageLogsContext } from '../usage-logs-provider' interface DetailSegment { @@ -445,10 +439,20 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { const tokenName = log.token_name if (!tokenName) return null + const other = parseLogOther(log.other) const displayName = sensitiveVisible ? tokenName : '••••' + let group = log.group + if (!group) group = other?.group || '' + + const metaParts: string[] = [] + const groupRatioText = getGroupRatioText(other) + if (group) { + metaParts.push(sensitiveVisible ? group : '••••') + } + if (groupRatioText) metaParts.push(groupRatioText) return ( -
+
[] { showDot={false} className='max-w-full overflow-hidden rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 font-mono text-foreground' /> + {metaParts.length > 0 && ( + + {metaParts.join(' · ')} + + )}
) }, @@ -471,81 +480,17 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { ), cell: function ModelCell({ row }) { - const { sensitiveVisible } = useUsageLogsContext() const log = row.original if (!isDisplayableLogType(log.type)) return null const modelInfo = formatModelName(log) - const other = parseLogOther(log.other) - let group = log.group - if (!group) group = other?.group || '' - - const badgeClass = - 'truncate rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 font-mono' - - const modelBadge = modelInfo.isMapped ? ( - - - - - -
-
- - {t('Request Model:')} - - - {modelInfo.name} - -
-
- - {t('Actual Model:')} - - - {modelInfo.actualModel} - -
-
-
-
- ) : ( - - ) - - const metaParts: string[] = [] - const groupRatioText = getGroupRatioText(other) - if (group) { - metaParts.push(sensitiveVisible ? group : '••••') - } - if (groupRatioText) metaParts.push(groupRatioText) return (
- {modelBadge} - {metaParts.length > 0 && ( - - {metaParts.join(' · ')} - - )} +
) }, @@ -576,11 +521,21 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { const pillBg: Record = { success: - 'border border-emerald-200/60 bg-emerald-50/50 dark:border-emerald-800/50 dark:bg-emerald-950/20', + 'border border-emerald-200/40 bg-emerald-50/35 dark:border-emerald-900/40 dark:bg-emerald-950/15', warning: - 'border border-amber-200/60 bg-amber-50/50 dark:border-amber-800/50 dark:bg-amber-950/20', + 'border border-amber-200/45 bg-amber-50/35 dark:border-amber-900/40 dark:bg-amber-950/15', danger: - 'border border-rose-200/70 bg-rose-50/60 dark:border-rose-800/50 dark:bg-rose-950/25', + 'border border-rose-200/50 bg-rose-50/35 dark:border-rose-900/40 dark:bg-rose-950/15', + } + const pillText: Record = { + success: 'text-emerald-700/85 dark:text-emerald-400/85', + warning: 'text-amber-700/85 dark:text-amber-400/85', + danger: 'text-rose-700/85 dark:text-rose-400/85', + } + const pillDot: Record = { + success: 'bg-emerald-500/80', + warning: 'bg-amber-500/80', + danger: 'bg-rose-500/80', } return ( @@ -590,13 +545,13 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { className={cn( 'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 font-mono text-xs font-medium', pillBg[timeVariant], - textColorMap[timeVariant] + pillText[timeVariant] )} >