| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044 |
- /* eslint-disable react-refresh/only-export-components */
- import { useState } from 'react'
- import { useQueryClient } from '@tanstack/react-query'
- import { type ColumnDef } from '@tanstack/react-table'
- import {
- AlertTriangle,
- ChevronDown,
- ChevronRight,
- ListOrdered,
- Shuffle,
- } from 'lucide-react'
- import { useTranslation } from 'react-i18next'
- import { toast } from 'sonner'
- import { getCurrencyLabel } from '@/lib/currency'
- import {
- formatTimestampToDate,
- formatQuota as formatQuotaValue,
- } from '@/lib/format'
- import { getLobeIcon } from '@/lib/lobe-icon'
- import { cn, truncateText } from '@/lib/utils'
- import { Button } from '@/components/ui/button'
- import { Checkbox } from '@/components/ui/checkbox'
- import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
- } from '@/components/ui/tooltip'
- 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 { getCodexUsage } from '../api'
- import { CHANNEL_STATUS_CONFIG, MODEL_FETCHABLE_TYPES } from '../constants'
- import {
- formatBalance,
- formatRelativeTime,
- formatResponseTime,
- getBalanceVariant,
- getChannelTypeIcon,
- getChannelTypeLabel,
- getResponseTimeConfig,
- isMultiKeyChannel,
- parseModelsList,
- parseGroupsList,
- parseChannelSettings,
- handleUpdateChannelField,
- handleUpdateTagField,
- handleUpdateChannelBalance,
- isTagAggregateRow,
- type TagRow,
- } from '../lib'
- import { parseUpstreamUpdateMeta } from '../lib/upstream-update-utils'
- import type { Channel } from '../types'
- import { useChannels } from './channels-provider'
- import { DataTableRowActions } from './data-table-row-actions'
- import { DataTableTagRowActions } from './data-table-tag-row-actions'
- import {
- CodexUsageDialog,
- type CodexUsageDialogData,
- } from './dialogs/codex-usage-dialog'
- import { NumericSpinnerInput } from './numeric-spinner-input'
- function parseIonetMeta(otherInfo: string | null | undefined): null | {
- source?: string
- deployment_id?: string
- } {
- if (!otherInfo) return null
- try {
- const parsed = JSON.parse(otherInfo)
- if (parsed && typeof parsed === 'object') {
- return parsed
- }
- } catch {
- return null
- }
- return null
- }
- /**
- * Render limited items with "and X more" indicator
- */
- 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>
- )
- }
- /**
- * Upstream update tags (+N / -N) shown on channel name for model-fetchable channels
- */
- function UpstreamUpdateTags({ channel }: { channel: Channel }) {
- const { upstream, setCurrentRow } = useChannels()
- if (!MODEL_FETCHABLE_TYPES.has(channel.type)) return null
- const meta = parseUpstreamUpdateMeta(channel.settings)
- if (!meta.enabled) return null
- const addCount = meta.pendingAddModels.length
- const removeCount = meta.pendingRemoveModels.length
- if (addCount === 0 && removeCount === 0) return null
- return (
- <div className='flex items-center gap-0.5'>
- {addCount > 0 && (
- <StatusBadge
- label={`+${addCount}`}
- variant='success'
- size='sm'
- copyable={false}
- className='cursor-pointer'
- onClick={(e: React.MouseEvent) => {
- e.stopPropagation()
- setCurrentRow(channel)
- upstream.openModal(
- channel,
- meta.pendingAddModels,
- meta.pendingRemoveModels,
- 'add'
- )
- }}
- />
- )}
- {removeCount > 0 && (
- <StatusBadge
- label={`-${removeCount}`}
- variant='danger'
- size='sm'
- copyable={false}
- className='cursor-pointer'
- onClick={(e: React.MouseEvent) => {
- e.stopPropagation()
- setCurrentRow(channel)
- upstream.openModal(
- channel,
- meta.pendingAddModels,
- meta.pendingRemoveModels,
- 'remove'
- )
- }}
- />
- )}
- </div>
- )
- }
- /**
- * Priority cell component with inline editing
- */
- function PriorityCell({ channel }: { channel: Channel }) {
- const { t } = useTranslation()
- const queryClient = useQueryClient()
- const isTagRow = isTagAggregateRow(channel)
- const priority = channel.priority
- const [confirmOpen, setConfirmOpen] = useState(false)
- const [pendingValue, setPendingValue] = useState<number | null>(null)
- // Tag row - editable with confirmation for all tag channels
- if (isTagRow) {
- const tag = channel.tag || ''
- const channelCount = channel.children?.length || 0
- return (
- <>
- <NumericSpinnerInput
- value={priority ?? 0}
- onChange={(value) => {
- setPendingValue(value)
- setConfirmOpen(true)
- }}
- min={-999}
- />
- <ConfirmDialog
- open={confirmOpen}
- onOpenChange={setConfirmOpen}
- title={t('Confirm Batch Update')}
- desc={`This will update the priority to ${pendingValue} for all ${channelCount} channel(s) with tag "${tag}". Continue?`}
- confirmText='Update'
- handleConfirm={() => {
- if (pendingValue !== null) {
- handleUpdateTagField(tag, 'priority', pendingValue, queryClient)
- }
- setConfirmOpen(false)
- }}
- />
- </>
- )
- }
- // Regular channel row - editable
- return (
- <NumericSpinnerInput
- value={priority ?? 0}
- onChange={(value) => {
- handleUpdateChannelField(channel.id, 'priority', value, queryClient)
- }}
- min={-999}
- />
- )
- }
- /**
- * Weight cell component with inline editing
- */
- function WeightCell({ channel }: { channel: Channel }) {
- const { t } = useTranslation()
- const queryClient = useQueryClient()
- const isTagRow = isTagAggregateRow(channel)
- const weight = channel.weight
- const [confirmOpen, setConfirmOpen] = useState(false)
- const [pendingValue, setPendingValue] = useState<number | null>(null)
- // Tag row - editable with confirmation for all tag channels
- if (isTagRow) {
- const tag = channel.tag || ''
- const channelCount = channel.children?.length || 0
- return (
- <>
- <NumericSpinnerInput
- value={weight ?? 0}
- onChange={(value) => {
- setPendingValue(value)
- setConfirmOpen(true)
- }}
- min={0}
- />
- <ConfirmDialog
- open={confirmOpen}
- onOpenChange={setConfirmOpen}
- title={t('Confirm Batch Update')}
- desc={`This will update the weight to ${pendingValue} for all ${channelCount} channel(s) with tag "${tag}". Continue?`}
- confirmText='Update'
- handleConfirm={() => {
- if (pendingValue !== null) {
- handleUpdateTagField(tag, 'weight', pendingValue, queryClient)
- }
- setConfirmOpen(false)
- }}
- />
- </>
- )
- }
- // Regular channel row - editable
- return (
- <NumericSpinnerInput
- value={weight ?? 0}
- onChange={(value) => {
- handleUpdateChannelField(channel.id, 'weight', value, queryClient)
- }}
- min={0}
- />
- )
- }
- /**
- * Balance cell component with click to update
- */
- function BalanceCell({ channel }: { channel: Channel }) {
- const { t } = useTranslation()
- const queryClient = useQueryClient()
- const isTagRow = isTagAggregateRow(channel)
- const balance = channel.balance || 0
- const usedQuota = channel.used_quota || 0
- const [isUpdating, setIsUpdating] = useState(false)
- const [codexUsageOpen, setCodexUsageOpen] = useState(false)
- const [codexUsageResponse, setCodexUsageResponse] =
- useState<CodexUsageDialogData | null>(null)
- const currencyLabel = getCurrencyLabel()
- const tokenSuffix = currencyLabel === 'Tokens' ? ' Tokens' : ''
- const withSuffix = (value: string) =>
- tokenSuffix && value !== '-' ? `${value}${tokenSuffix}` : value
- const usedDisplay = withSuffix(formatQuotaValue(usedQuota))
- const remainingDisplay = withSuffix(formatBalance(balance))
- // Tag row: only show cumulative used quota
- if (isTagRow) {
- return (
- <StatusBadge
- label={`Used: ${usedDisplay}`}
- variant='neutral'
- size='sm'
- copyable={false}
- />
- )
- }
- // Regular channel row: show used and remaining with click to update
- const variant = getBalanceVariant(balance)
- const handleClickUpdate = async () => {
- if (isUpdating) return
- setIsUpdating(true)
- if (channel.type === 57) {
- try {
- const res = await getCodexUsage(channel.id)
- if (!res.success) {
- throw new Error(res.message || t('Failed to fetch usage'))
- }
- setCodexUsageResponse(res)
- setCodexUsageOpen(true)
- } catch (error) {
- toast.error(
- error instanceof Error ? error.message : t('Failed to fetch usage')
- )
- } finally {
- setIsUpdating(false)
- }
- return
- }
- await handleUpdateChannelBalance(channel.id, queryClient)
- setIsUpdating(false)
- }
- 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'
- />
- <Tooltip>
- <TooltipTrigger asChild>
- <span className='text-muted-foreground cursor-help'>
- {usedDisplay}
- </span>
- </TooltipTrigger>
- <TooltipContent>
- <p>
- {t('Used:')} {usedDisplay}
- </p>
- </TooltipContent>
- </Tooltip>
- <span className='text-muted-foreground/30'>·</span>
- <Tooltip>
- <TooltipTrigger asChild>
- <span
- className={cn(
- 'cursor-pointer transition-opacity hover:opacity-70',
- channel.type === 57
- ? 'text-primary'
- : textColorMap[isUpdating ? 'neutral' : variant]
- )}
- onClick={handleClickUpdate}
- >
- {isUpdating
- ? 'Updating...'
- : channel.type === 57
- ? t('Account Info')
- : remainingDisplay}
- </span>
- </TooltipTrigger>
- <TooltipContent>
- <p>
- {channel.type === 57
- ? t('Click to view Codex usage')
- : `${t('Remaining:')} ${remainingDisplay}`}
- </p>
- {channel.type !== 57 && <p>{t('Click to update balance')}</p>}
- </TooltipContent>
- </Tooltip>
- </div>
- <CodexUsageDialog
- open={codexUsageOpen}
- onOpenChange={setCodexUsageOpen}
- channelName={channel.name}
- channelId={channel.id}
- response={codexUsageResponse}
- onRefresh={async () => {
- if (isUpdating) return
- setIsUpdating(true)
- try {
- const res = await getCodexUsage(channel.id)
- if (!res.success) {
- throw new Error(res.message || t('Failed to fetch usage'))
- }
- setCodexUsageResponse(res)
- } catch (error) {
- toast.error(
- error instanceof Error
- ? error.message
- : t('Failed to fetch usage')
- )
- } finally {
- setIsUpdating(false)
- }
- }}
- isRefreshing={isUpdating}
- />
- </TooltipProvider>
- )
- }
- /**
- * Generate channels columns configuration
- */
- export function useChannelsColumns(): ColumnDef<Channel>[] {
- const { t } = useTranslation()
- return [
- // Checkbox column
- {
- id: 'select',
- header: ({ table }) => (
- <Checkbox
- checked={
- table.getIsAllPageRowsSelected() ||
- (table.getIsSomePageRowsSelected() && 'indeterminate')
- }
- onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
- aria-label='Select all'
- />
- ),
- cell: ({ row }) => {
- const isTagRow = isTagAggregateRow(row.original)
- // 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,
- },
- // ID column
- {
- accessorKey: 'id',
- meta: { label: t('ID'), mobileHidden: true },
- header: ({ column }) => (
- <DataTableColumnHeader column={column} title='ID' />
- ),
- cell: ({ row }) => {
- const id = row.getValue('id') as number
- return (
- <StatusBadge
- label={String(id)}
- variant='neutral'
- copyText={String(id)}
- size='sm'
- className='font-mono'
- />
- )
- },
- size: 80,
- },
- // Name column
- {
- accessorKey: 'name',
- meta: { label: t('Name'), mobileTitle: true },
- header: ({ column }) => (
- <DataTableColumnHeader column={column} title={t('Name')} />
- ),
- cell: ({ row }) => {
- const isTagRow = isTagAggregateRow(row.original)
- const name = row.getValue('name') as string
- const channel = row.original
- const isMultiKey = isMultiKeyChannel(channel)
- // Tag row with expand/collapse
- if (isTagRow) {
- const tag = (row.original as TagRow).tag || name
- const childrenCount = (row.original as TagRow).children?.length || 0
- return (
- <div className='flex items-center gap-2'>
- <Button
- variant='ghost'
- size='sm'
- className='h-6 w-6 p-0'
- onClick={row.getToggleExpandedHandler()}
- >
- {row.getIsExpanded() ? (
- <ChevronDown className='h-4 w-4' />
- ) : (
- <ChevronRight className='h-4 w-4' />
- )}
- </Button>
- <div className='flex items-center gap-1.5'>
- <span className='font-semibold'>Tag:{tag}</span>
- <StatusBadge
- label={`${childrenCount} channels`}
- variant='blue'
- size='sm'
- copyable={false}
- />
- </div>
- </div>
- )
- }
- // Regular channel row
- const settings = parseChannelSettings(channel.setting)
- const isPassThrough = settings.pass_through_body_enabled === true
- return (
- <div className='flex items-center gap-2'>
- <div className='flex flex-col gap-1'>
- <div className='flex items-center gap-1.5'>
- <span className='font-medium'>{truncateText(name, 30)}</span>
- {isPassThrough && (
- <TooltipProvider delayDuration={100}>
- <Tooltip>
- <TooltipTrigger asChild>
- <AlertTriangle className='h-3.5 w-3.5 flex-shrink-0 text-amber-500' />
- </TooltipTrigger>
- <TooltipContent side='top'>
- {t(
- 'Request body pass-through is enabled. The request body will be sent directly to the upstream without any conversion.'
- )}
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- )}
- {isMultiKey && (
- <StatusBadge
- label={`${channel.channel_info.multi_key_size} keys`}
- variant='purple'
- size='sm'
- copyable={false}
- />
- )}
- <UpstreamUpdateTags channel={channel} />
- </div>
- {channel.remark && (
- <TooltipProvider delayDuration={200}>
- <Tooltip>
- <TooltipTrigger asChild>
- <span className='text-muted-foreground text-xs'>
- {truncateText(channel.remark, 40)}
- </span>
- </TooltipTrigger>
- <TooltipContent side='bottom' className='max-w-xs'>
- {channel.remark}
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- )}
- </div>
- </div>
- )
- },
- minSize: 200,
- },
- // Type column
- {
- accessorKey: 'type',
- meta: { label: t('Type') },
- header: t('Type'),
- cell: ({ row }) => {
- const isTagRow = isTagAggregateRow(row.original)
- if (isTagRow) {
- return (
- <StatusBadge
- label={t('Tag Aggregate')}
- variant='blue'
- size='sm'
- copyable={false}
- />
- )
- }
- const type = row.getValue('type') as number
- const typeNameKey = getChannelTypeLabel(type)
- const typeName = t(typeNameKey)
- const iconName = getChannelTypeIcon(type)
- const icon = getLobeIcon(`${iconName}.Color`, 20)
- const channel = row.original as Channel
- const isMultiKey = isMultiKeyChannel(channel)
- const multiKeyMode = channel.channel_info?.multi_key_mode ?? 'random'
- const MultiKeyModeIcon =
- multiKeyMode === 'random' ? Shuffle : ListOrdered
- const multiKeyTooltip =
- multiKeyMode === 'random'
- ? t('Multi-key: Random rotation')
- : t('Multi-key: Polling rotation')
- const ionetMeta = parseIonetMeta(channel.other_info)
- const isIonet = ionetMeta?.source === 'ionet'
- const deploymentId =
- typeof ionetMeta?.deployment_id === 'string'
- ? ionetMeta?.deployment_id
- : undefined
- return (
- <div className='flex items-center gap-2'>
- <div className='flex items-center gap-1.5'>
- {isMultiKey && (
- <TooltipProvider delayDuration={100}>
- <Tooltip>
- <TooltipTrigger asChild>
- <span className='border-border bg-muted text-primary inline-flex h-6 w-6 items-center justify-center rounded-full border'>
- <MultiKeyModeIcon className='h-3.5 w-3.5' />
- </span>
- </TooltipTrigger>
- <TooltipContent side='top'>
- {multiKeyTooltip}
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- )}
- {icon}
- </div>
- <StatusBadge
- label={typeName}
- autoColor={typeName}
- size='sm'
- copyable={false}
- />
- {isIonet && (
- <TooltipProvider delayDuration={100}>
- <Tooltip>
- <TooltipTrigger asChild>
- <span
- className='flex cursor-pointer items-center gap-1.5 text-xs font-medium'
- onClick={(e) => {
- e.stopPropagation()
- if (!deploymentId) return
- const targetUrl = `/console/deployment?deployment_id=${deploymentId}`
- window.open(targetUrl, '_blank', 'noopener')
- }}
- >
- <span className='text-muted-foreground/30'>·</span>
- <span className={cn(textColorMap.purple)}>IO.NET</span>
- </span>
- </TooltipTrigger>
- <TooltipContent side='top'>
- <div className='max-w-xs space-y-1'>
- <div className='text-xs'>
- {t('From IO.NET deployment')}
- </div>
- {deploymentId && (
- <div className='text-muted-foreground font-mono text-xs'>
- {t('Deployment ID')}: {deploymentId}
- </div>
- )}
- <div className='text-muted-foreground text-xs'>
- {t('Click to open deployment')}
- </div>
- </div>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- )}
- </div>
- )
- },
- filterFn: (row, id, value) => {
- if (!value || value.length === 0 || value.includes('all')) return true
- return value.includes(String(row.getValue(id)))
- },
- size: 140,
- enableSorting: false,
- },
- // Status column
- {
- accessorKey: 'status',
- meta: { label: t('Status'), mobileBadge: true },
- header: t('Status'),
- cell: ({ row }) => {
- const isTagRow = isTagAggregateRow(row.original)
- const status = row.getValue('status') as number
- const channel = row.original as Channel
- // Tag row: show aggregated status
- if (isTagRow) {
- const childrenCount = (row.original as TagRow).children?.length || 0
- const hasEnabled = status === 1
- if (hasEnabled) {
- return (
- <StatusBadge
- label={`Active (${childrenCount})`}
- variant='success'
- showDot
- size='sm'
- copyable={false}
- />
- )
- } else {
- return (
- <StatusBadge
- label={`Inactive (${childrenCount})`}
- variant='neutral'
- size='sm'
- copyable={false}
- />
- )
- }
- }
- // Regular channel row
- const config =
- CHANNEL_STATUS_CONFIG[status as keyof typeof CHANNEL_STATUS_CONFIG] ||
- CHANNEL_STATUS_CONFIG[0]
- const isMultiKey = isMultiKeyChannel(channel)
- const keySize = channel.channel_info?.multi_key_size ?? 0
- const disabledCount = channel.channel_info?.multi_key_status_list
- ? Object.keys(channel.channel_info.multi_key_status_list).length
- : 0
- const enabledCount = Math.max(0, keySize - disabledCount)
- const label =
- isMultiKey && keySize > 0
- ? `${t(config.label)} (${enabledCount}/${keySize})`
- : t(config.label)
- // Auto-disabled: show reason and time tooltip
- if (status === 3) {
- let statusReason = ''
- let statusTime = ''
- try {
- const otherInfo = channel.other_info
- ? JSON.parse(channel.other_info)
- : null
- if (otherInfo) {
- statusReason = otherInfo.status_reason || ''
- statusTime = otherInfo.status_time
- ? formatTimestampToDate(otherInfo.status_time)
- : ''
- }
- } catch {
- /* empty */
- }
- if (statusReason || statusTime) {
- return (
- <TooltipProvider delayDuration={100}>
- <Tooltip>
- <TooltipTrigger asChild>
- <span>
- <StatusBadge
- label={label}
- variant={config.variant}
- showDot={config.showDot}
- size='sm'
- copyable={false}
- />
- </span>
- </TooltipTrigger>
- <TooltipContent side='top' className='max-w-xs'>
- <div className='space-y-1 text-xs'>
- {statusReason && (
- <div>
- {t('Reason:')} {statusReason}
- </div>
- )}
- {statusTime && (
- <div>
- {t('Time:')} {statusTime}
- </div>
- )}
- </div>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- )
- }
- }
- return (
- <StatusBadge
- label={label}
- variant={config.variant}
- showDot={config.showDot}
- size='sm'
- copyable={false}
- />
- )
- },
- filterFn: (row, id, value) => {
- if (!value || value.length === 0 || value.includes('all')) return true
- const status = row.getValue(id) as number
- if (value.includes('enabled')) return status === 1
- if (value.includes('disabled')) return status !== 1
- return false
- },
- size: 120,
- enableSorting: false,
- },
- // Models column
- {
- accessorKey: 'models',
- meta: { label: t('Models'), mobileHidden: true },
- header: t('Models'),
- cell: ({ row }) => {
- const models = row.getValue('models') as string
- const modelArray = parseModelsList(models)
- if (modelArray.length === 0) {
- return <span className='text-muted-foreground text-xs'>-</span>
- }
- const modelBadges = modelArray.map((model, idx) => (
- <StatusBadge
- key={idx}
- label={model}
- autoColor={model}
- size='sm'
- className='font-mono'
- />
- ))
- return (
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger asChild>
- <div>{renderLimitedItems(modelBadges, 2)}</div>
- </TooltipTrigger>
- {modelArray.length > 2 && (
- <TooltipContent
- side='top'
- className='border-border bg-popover max-h-48 max-w-[320px] overflow-y-auto p-2'
- >
- <div className='flex flex-wrap gap-1'>{modelBadges}</div>
- </TooltipContent>
- )}
- </Tooltip>
- </TooltipProvider>
- )
- },
- size: 200,
- enableSorting: false,
- },
- // Group column
- {
- accessorKey: 'group',
- meta: { label: t('Groups'), mobileHidden: true },
- header: t('Groups'),
- cell: ({ row }) => {
- const group = row.getValue('group') as string
- const groupArray = parseGroupsList(group)
- const groupBadges = groupArray.map((g) => (
- <GroupBadge key={g} group={g} size='sm' />
- ))
- return (
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger asChild>
- <div>{renderLimitedItems(groupBadges, 2)}</div>
- </TooltipTrigger>
- {groupArray.length > 2 && (
- <TooltipContent
- side='top'
- className='border-border bg-popover max-h-48 max-w-[320px] overflow-y-auto p-2'
- >
- <div className='flex flex-wrap gap-1'>{groupBadges}</div>
- </TooltipContent>
- )}
- </Tooltip>
- </TooltipProvider>
- )
- },
- filterFn: (row, id, value) => {
- if (!value || value.length === 0 || value.includes('all')) return true
- const group = row.getValue(id) as string
- const groupArray = parseGroupsList(group)
- return groupArray.some((g) => value.includes(g))
- },
- size: 150,
- enableSorting: false,
- },
- // Tag column
- {
- accessorKey: 'tag',
- meta: { label: t('Tag'), mobileHidden: true },
- header: t('Tag'),
- cell: ({ row }) => {
- const tag = row.getValue('tag') as string | null
- if (!tag)
- return <span className='text-muted-foreground text-xs'>-</span>
- return <StatusBadge label={tag} autoColor={tag} size='sm' />
- },
- size: 120,
- enableSorting: false,
- },
- // Priority column
- {
- accessorKey: 'priority',
- meta: { label: t('Priority'), mobileHidden: true },
- header: ({ column }) => (
- <DataTableColumnHeader column={column} title={t('Priority')} />
- ),
- cell: ({ row }) => <PriorityCell channel={row.original} />,
- size: 100,
- },
- // Weight column
- {
- accessorKey: 'weight',
- meta: { label: t('Weight'), mobileHidden: true },
- header: t('Weight'),
- cell: ({ row }) => <WeightCell channel={row.original} />,
- size: 90,
- enableSorting: false,
- },
- // Balance column (Used/Remaining)
- {
- accessorKey: 'balance',
- meta: { label: t('Used / Remaining') },
- header: ({ column }) => (
- <DataTableColumnHeader column={column} title={t('Used / Remaining')} />
- ),
- cell: ({ row }) => <BalanceCell channel={row.original} />,
- size: 180,
- },
- // Response Time column
- {
- accessorKey: 'response_time',
- meta: { label: t('Response'), mobileHidden: true },
- header: ({ column }) => (
- <DataTableColumnHeader column={column} title={t('Response')} />
- ),
- cell: ({ row }) => {
- const responseTime = row.getValue('response_time') as number
- const config = getResponseTimeConfig(responseTime)
- return (
- <StatusBadge
- label={formatResponseTime(responseTime, t)}
- variant={config.variant}
- size='sm'
- copyable={false}
- />
- )
- },
- size: 110,
- },
- // Test Time column
- {
- accessorKey: 'test_time',
- meta: { label: t('Last Tested'), mobileHidden: true },
- header: ({ column }) => (
- <DataTableColumnHeader column={column} title={t('Last Tested')} />
- ),
- cell: ({ row }) => {
- const testTime = row.getValue('test_time') as number
- // For invalid timestamps, show "Never" badge
- if (!testTime || testTime === 0) {
- return <span className='text-muted-foreground text-xs'>-</span>
- }
- const timeText = formatRelativeTime(testTime)
- const fullDate = formatTimestampToDate(testTime)
- // For valid timestamps, show tooltip with full date
- return (
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger asChild>
- <span className='text-muted-foreground cursor-pointer font-mono text-sm'>
- {timeText}
- </span>
- </TooltipTrigger>
- <TooltipContent side='top'>
- <p className='font-mono text-sm'>{fullDate}</p>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- )
- },
- size: 120,
- enableSorting: false,
- },
- // Actions column
- {
- id: 'actions',
- cell: ({ row }) => {
- // Check if this is a tag row (has children)
- const isTagRow = isTagAggregateRow(row.original)
- if (isTagRow) {
- return (
- <DataTableTagRowActions
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- row={row as any}
- />
- )
- }
- return <DataTableRowActions row={row} />
- },
- size: 132,
- enableSorting: false,
- enableHiding: false,
- },
- ]
- }
|