feat: refine channel management UI

This commit is contained in:
CaIon
2026-06-28 16:36:32 +08:00
parent 1d166532fe
commit 25f998595d
19 changed files with 1232 additions and 649 deletions
@@ -1,13 +1,3 @@
import { useQueryClient } from '@tanstack/react-query'
import type { ColumnDef } from '@tanstack/react-table'
import {
AlertTriangle,
ChevronDown,
ChevronRight,
ListOrdered,
Shuffle,
SlidersHorizontal,
} from 'lucide-react'
/*
Copyright (C) 2023-2026 QuantumNous
@@ -27,7 +17,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
/* eslint-disable react-refresh/only-export-components */
import { useState, useMemo } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import type { ColumnDef } from '@tanstack/react-table'
import {
AlertTriangle,
ChevronDown,
ChevronRight,
ListOrdered,
Shuffle,
SlidersHorizontal,
} from 'lucide-react'
import { useState, useMemo, useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
@@ -51,16 +51,12 @@ import {
formatQuotaWithCurrency,
getCurrencyLabel,
} from '@/lib/currency'
import {
formatTimestampToDate,
formatQuota as formatQuotaValue,
} from '@/lib/format'
import { formatTimestampToDate } from '@/lib/format'
import { truncateText } from '@/lib/utils'
import { getCodexUsage } from '../api'
import { CHANNEL_STATUS_CONFIG, MODEL_FETCHABLE_TYPES } from '../constants'
import {
formatBalance,
formatRelativeTime,
formatResponseTime,
getBalanceVariant,
@@ -79,6 +75,7 @@ import {
} from '../lib'
import { parseUpstreamUpdateMeta } from '../lib/upstream-update-utils'
import type { Channel } from '../types'
import { ChannelRowActionsLayoutContext } from './channel-row-actions-context'
import { useChannels } from './channels-provider'
import { DataTableRowActions } from './data-table-row-actions'
import { DataTableTagRowActions } from './data-table-tag-row-actions'
@@ -299,6 +296,7 @@ const SENSITIVE_MASK = '••••'
function BalanceCell({ channel }: { channel: Channel }) {
const { t, i18n } = useTranslation()
const queryClient = useQueryClient()
const layout = useContext(ChannelRowActionsLayoutContext)
const { sensitiveVisible } = useChannels()
const isTagRow = isTagAggregateRow(channel)
const balance = channel.balance || 0
@@ -313,18 +311,43 @@ function BalanceCell({ channel }: { channel: Channel }) {
tokenSuffix && value !== '-' ? `${value}${tokenSuffix}` : value
const locale = i18n.resolvedLanguage || i18n.language
const balanceFormatOptions = {
digitsLarge: 2,
digitsSmall: 4,
abbreviate: false,
showSymbol: layout !== 'card',
} as const
// Precise values are kept for the tooltip; long values are shown compactly inline.
const usedFull = withSuffix(formatQuotaValue(usedQuota))
const remainingFull = withSuffix(formatBalance(balance))
const usedFull = withSuffix(
formatQuotaWithCurrency(usedQuota, {
digitsLarge: 2,
digitsSmall: 4,
abbreviate: true,
showSymbol: layout !== 'card',
})
)
const remainingFull = withSuffix(
formatCurrencyFromUSD(balance, balanceFormatOptions)
)
const usedDisplay =
usedFull.length > MAX_INLINE_BALANCE_CHARS
? withSuffix(
formatQuotaWithCurrency(usedQuota, { compact: true, locale })
formatQuotaWithCurrency(usedQuota, {
compact: true,
locale,
showSymbol: layout !== 'card',
})
)
: usedFull
const remainingDisplay =
remainingFull.length > MAX_INLINE_BALANCE_CHARS
? withSuffix(formatCurrencyFromUSD(balance, { compact: true, locale }))
? withSuffix(
formatCurrencyFromUSD(balance, {
compact: true,
locale,
showSymbol: layout !== 'card',
})
)
: remainingFull
const usedLabel = `${t('Used:')} ${usedFull}`
const remainingLabel = `${t('Remaining:')} ${remainingFull}`
@@ -488,9 +511,14 @@ function BalanceCell({ channel }: { channel: Channel }) {
/**
* Generate channels columns configuration
*/
export function useChannelsColumns(): ColumnDef<Channel>[] {
export function useChannelsColumns(
options: {
enableSelection?: boolean
} = {}
): ColumnDef<Channel>[] {
const { t, i18n } = useTranslation()
const { sensitiveVisible } = useChannels()
const enableSelection = options.enableSelection ?? true
const locale = i18n.resolvedLanguage || i18n.language
// The column definitions only depend on the translation function, the active
// locale, and sensitive-data visibility. Memoizing keeps the array (and every
@@ -499,38 +527,42 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
return useMemo<ColumnDef<Channel>[]>(
() => [
// Checkbox column
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
indeterminate={table.getIsSomePageRowsSelected()}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
aria-label='Select all'
/>
),
cell: ({ row }) => {
const isTagRow = isTagAggregateRow(row.original)
...(enableSelection
? [
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
indeterminate={table.getIsSomePageRowsSelected()}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
aria-label={t('Select all')}
/>
),
cell: ({ row }) => {
const isTagRow = isTagAggregateRow(row.original)
// Don't show checkbox for tag rows
if (isTagRow) {
return null
}
// Don't show checkbox for tag rows
if (isTagRow) {
return null
}
return (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label='Select row'
/>
)
},
enableSorting: false,
enableHiding: false,
size: 40,
},
return (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label={t('Select row')}
/>
)
},
enableSorting: false,
enableHiding: false,
size: 40,
} satisfies ColumnDef<Channel>,
]
: []),
// ID column
{
@@ -543,7 +575,6 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
},
size: 80,
},
// Name column
{
accessorKey: 'name',
@@ -1119,6 +1150,6 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
meta: { pinned: 'right' as const },
},
],
[t, locale, sensitiveVisible]
[enableSelection, t, locale, sensitiveVisible]
)
}