/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ /* 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 - 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 (
{ 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 (
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) => ( )) 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: 132, enableSorting: false, enableHiding: false, }, ] }