feat: refine channel management UI
This commit is contained in:
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user