/* 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 {
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 -
const displayed = items.slice(0, maxDisplay)
const remaining = items.length - maxDisplay
return (
{displayed}
{remaining > 0 && (
)}
)
}
/**
* 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 (
{addCount > 0 && (
{
e.stopPropagation()
setCurrentRow(channel)
upstream.openModal(
channel,
meta.pendingAddModels,
meta.pendingRemoveModels,
'add'
)
}}
/>
)}
{removeCount > 0 && (
{
e.stopPropagation()
setCurrentRow(channel)
upstream.openModal(
channel,
meta.pendingAddModels,
meta.pendingRemoveModels,
'remove'
)
}}
/>
)}
)
}
/**
* 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(null)
// Tag row - editable with confirmation for all tag channels
if (isTagRow) {
const tag = channel.tag || ''
const channelCount = channel.children?.length || 0
return (
<>
{
setPendingValue(value)
setConfirmOpen(true)
}}
min={-999}
/>
{
if (pendingValue !== null) {
handleUpdateTagField(tag, 'priority', pendingValue, queryClient)
}
setConfirmOpen(false)
}}
/>
>
)
}
// Regular channel row - editable
return (
{
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(null)
// Tag row - editable with confirmation for all tag channels
if (isTagRow) {
const tag = channel.tag || ''
const channelCount = channel.children?.length || 0
return (
<>
{
setPendingValue(value)
setConfirmOpen(true)
}}
min={0}
/>
{
if (pendingValue !== null) {
handleUpdateTagField(tag, 'weight', pendingValue, queryClient)
}
setConfirmOpen(false)
}}
/>
>
)
}
// Regular channel row - editable
return (
{
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(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 (
)
}
// 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 (
{usedDisplay}
{t('Used:')} {usedDisplay}
·
{isUpdating
? 'Updating...'
: channel.type === 57
? t('Account Info')
: remainingDisplay}
{channel.type === 57
? t('Click to view Codex usage')
: `${t('Remaining:')} ${remainingDisplay}`}
{channel.type !== 57 && {t('Click to update balance')}
}
{
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}
/>
)
}
/**
* Generate channels columns configuration
*/
export function useChannelsColumns(): ColumnDef[] {
const { t } = useTranslation()
return [
// Checkbox column
{
id: 'select',
header: ({ table }) => (
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 (
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 }) => (
),
cell: ({ row }) => {
const id = row.getValue('id') as number
return (
)
},
size: 80,
},
// Name column
{
accessorKey: 'name',
meta: { label: t('Name'), mobileTitle: true },
header: ({ column }) => (
),
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 (
{row.getIsExpanded() ? (
) : (
)}
Tag:{tag}
)
}
// Regular channel row
const settings = parseChannelSettings(channel.setting)
const isPassThrough = settings.pass_through_body_enabled === true
return (
{truncateText(name, 30)}
{isPassThrough && (
{t(
'Request body pass-through is enabled. The request body will be sent directly to the upstream without any conversion.'
)}
)}
{isMultiKey && (
)}
{channel.remark && (
{truncateText(channel.remark, 40)}
{channel.remark}
)}
)
},
minSize: 200,
},
// Type column
{
accessorKey: 'type',
meta: { label: t('Type') },
header: t('Type'),
cell: ({ row }) => {
const isTagRow = isTagAggregateRow(row.original)
if (isTagRow) {
return (
)
}
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 (
{isMultiKey && (
{multiKeyTooltip}
)}
{icon}
{isIonet && (
{
e.stopPropagation()
if (!deploymentId) return
const targetUrl = `/console/deployment?deployment_id=${deploymentId}`
window.open(targetUrl, '_blank', 'noopener')
}}
>
·
IO.NET
{t('From IO.NET deployment')}
{deploymentId && (
{t('Deployment ID')}: {deploymentId}
)}
{t('Click to open deployment')}
)}
)
},
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 (
)
} else {
return (
)
}
}
// 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 (
{statusReason && (
{t('Reason:')} {statusReason}
)}
{statusTime && (
{t('Time:')} {statusTime}
)}
)
}
}
return (
)
},
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 -
}
const modelBadges = modelArray.map((model, idx) => (
))
return (
{renderLimitedItems(modelBadges, 2)}
{modelArray.length > 2 && (
{modelBadges}
)}
)
},
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, idx) => (
))
return (
{renderLimitedItems(groupBadges, 2)}
{groupArray.length > 2 && (
{groupBadges}
)}
)
},
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 -
return
},
size: 120,
enableSorting: false,
},
// Priority column
{
accessorKey: 'priority',
meta: { label: t('Priority'), mobileHidden: true },
header: ({ column }) => (
),
cell: ({ row }) => ,
size: 100,
},
// Weight column
{
accessorKey: 'weight',
meta: { label: t('Weight'), mobileHidden: true },
header: t('Weight'),
cell: ({ row }) => ,
size: 90,
enableSorting: false,
},
// Balance column (Used/Remaining)
{
accessorKey: 'balance',
meta: { label: t('Used / Remaining') },
header: ({ column }) => (
),
cell: ({ row }) => ,
size: 180,
},
// Response Time column
{
accessorKey: 'response_time',
meta: { label: t('Response'), mobileHidden: true },
header: ({ column }) => (
),
cell: ({ row }) => {
const responseTime = row.getValue('response_time') as number
const config = getResponseTimeConfig(responseTime)
return (
)
},
size: 110,
},
// Test Time column
{
accessorKey: 'test_time',
meta: { label: t('Last Tested'), mobileHidden: true },
header: ({ column }) => (
),
cell: ({ row }) => {
const testTime = row.getValue('test_time') as number
// For invalid timestamps, show "Never" badge
if (!testTime || testTime === 0) {
return -
}
const timeText = formatRelativeTime(testTime)
const fullDate = formatTimestampToDate(testTime)
// For valid timestamps, show tooltip with full date
return (
{timeText}
{fullDate}
)
},
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 (
)
}
return
},
size: 100,
enableSorting: false,
enableHiding: false,
},
]
}