/*
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 { 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, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { BadgeListCell } from '@/components/data-table'
import { GroupBadge } from '@/components/group-badge'
import { ProviderBadge } from '@/components/provider-badge'
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
import { TableId } from '@/components/table-id'
import { TruncatedText } from '@/components/truncated-text'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { toIntlLocale } from '@/i18n/languages'
import {
formatCurrencyFromUSD,
formatQuotaWithCurrency,
getCurrencyLabel,
} from '@/lib/currency'
import { formatTimestampToDate } from '@/lib/format'
import { truncateText } from '@/lib/utils'
import { getCodexUsage, updateChannelBalance } from '../api'
import { CHANNEL_STATUS_CONFIG, MODEL_FETCHABLE_TYPES } from '../constants'
import {
formatRelativeTime,
formatResponseTime,
getBalanceVariant,
getChannelTypeIcon,
getChannelTypeLabel,
getResponseTimeConfig,
isMultiKeyChannel,
parseModelsList,
parseGroupsList,
parseChannelSettings,
channelsQueryKeys,
handleUpdateChannelField,
handleUpdateTagField,
createChannelFieldUpdateScheduler,
isTagAggregateRow,
type TagRow,
} 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'
import { BalanceQueryDialog } from './dialogs/balance-query-dialog'
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
}
/**
* 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 }) {
if (isTagAggregateRow(channel)) {
return
}
return (
)
}
function TagPriorityCell({ channel }: { channel: TagRow }) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const priority = channel.priority
const [confirmOpen, setConfirmOpen] = useState(false)
const [pendingValue, setPendingValue] = useState(null)
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)
}}
/>
>
)
}
function ChannelFieldCell({
channelId,
value,
field,
min,
}: {
channelId: number
value: number | null | undefined
field: 'priority' | 'weight'
min: number
}) {
const queryClient = useQueryClient()
const fieldUpdateScheduler = useMemo(
() =>
createChannelFieldUpdateScheduler((nextValue) => {
void handleUpdateChannelField(channelId, field, nextValue, queryClient)
}),
[channelId, field, queryClient]
)
useEffect(() => () => fieldUpdateScheduler.flush(), [fieldUpdateScheduler])
return (
)
}
/**
* Weight cell component with inline editing
*/
function WeightCell({ channel }: { channel: Channel }) {
if (isTagAggregateRow(channel)) {
return
}
return (
)
}
function TagWeightCell({ channel }: { channel: TagRow }) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const weight = channel.weight
const [confirmOpen, setConfirmOpen] = useState(false)
const [pendingValue, setPendingValue] = useState(null)
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)
}}
/>
>
)
}
/**
* Inline balance/used values longer than this switch to locale-aware compact
* notation (e.g. "$28万"); the precise value stays available in the tooltip.
*/
const MAX_INLINE_BALANCE_CHARS = 8
const SENSITIVE_MASK = '••••'
/**
* Balance cell component with click to update
*/
export function BalanceCell({ channel }: { channel: Channel }) {
const { t, i18n } = useTranslation()
const queryClient = useQueryClient()
const layout = useContext(ChannelRowActionsLayoutContext)
const { sensitiveVisible, setCurrentRow } = useChannels()
const isTagRow = isTagAggregateRow(channel)
const balance = channel.balance || 0
const usedQuota = channel.used_quota || 0
const [isUpdating, setIsUpdating] = useState(false)
const [rawBalanceResponse, setRawBalanceResponse] = useState(
null
)
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 locale = toIntlLocale(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(
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,
showSymbol: layout !== 'card',
})
)
: usedFull
const remainingDisplay =
remainingFull.length > MAX_INLINE_BALANCE_CHARS
? withSuffix(
formatCurrencyFromUSD(balance, {
compact: true,
locale,
showSymbol: layout !== 'card',
})
)
: remainingFull
const usedLabel = `${t('Used:')} ${usedFull}`
const remainingLabel = `${t('Remaining:')} ${remainingFull}`
const maskedUsedLabel = `${t('Used:')} ${SENSITIVE_MASK}`
const maskedRemainingLabel = `${t('Remaining:')} ${SENSITIVE_MASK}`
// Tag row: only show cumulative used quota
if (isTagRow) {
return (
}
/>
{sensitiveVisible ? usedLabel : maskedUsedLabel}
)
}
// 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
}
try {
const response = await updateChannelBalance(channel.id)
if (response.success && response.balance !== undefined) {
toast.success(
t('Balance updated: {{balance}}', {
balance: formatCurrencyFromUSD(response.balance, {
digitsLarge: 2,
digitsSmall: 4,
abbreviate: false,
}),
})
)
void queryClient.invalidateQueries({
queryKey: channelsQueryKeys.lists(),
})
} else if (response.success && response.raw_response !== undefined) {
setCurrentRow(channel)
setRawBalanceResponse(response.raw_response)
} else {
toast.error(response.message || t('Failed to update balance'))
}
} catch (error: unknown) {
toast.error(
error instanceof Error ? error.message : t('Failed to update balance')
)
} finally {
setIsUpdating(false)
}
}
let remainingBadgeLabel = sensitiveVisible ? remainingDisplay : SENSITIVE_MASK
if (sensitiveVisible && isUpdating) {
remainingBadgeLabel = t('Updating...')
} else if (sensitiveVisible && channel.type === 57) {
remainingBadgeLabel = t('Account Info')
}
let remainingTooltipLabel = remainingLabel
if (!sensitiveVisible) {
remainingTooltipLabel = maskedRemainingLabel
} else if (channel.type === 57) {
remainingTooltipLabel = t('Click to view Codex usage')
}
let remainingBadgeVariant: StatusBadgeProps['variant'] = variant
if (channel.type === 57) {
remainingBadgeVariant = 'info'
} else if (isUpdating) {
remainingBadgeVariant = 'neutral'
}
return (
}
/>
{sensitiveVisible ? usedLabel : maskedUsedLabel}
}
/>
{remainingTooltipLabel}
{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}
/>
{rawBalanceResponse !== null && (
{
if (!open) {
setRawBalanceResponse(null)
}
}}
/>
)}
)
}
/**
* Generate channels columns configuration
*/
export function useChannelsColumns(
options: {
enableSelection?: boolean
} = {}
): ColumnDef[] {
const { t, i18n } = useTranslation()
const { sensitiveVisible } = useChannels()
const enableSelection = options.enableSelection ?? true
const locale = toIntlLocale(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
// cell renderer reference) stable across unrelated re-renders, so react-table
// does not invalidate the whole row model on each parent render.
return useMemo[]>(
() => [
// Checkbox column
...(enableSelection
? [
{
id: 'select',
header: ({ table }) => (
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
}
return (
row.toggleSelected(!!value)}
aria-label={t('Select row')}
/>
)
},
enableSorting: false,
enableHiding: false,
enableResizing: false,
size: 40,
} satisfies ColumnDef,
]
: []),
// ID column
{
accessorKey: 'id',
header: t('ID'),
meta: { mobileHidden: true },
cell: ({ row }) => {
const id = row.getValue('id') as number
return
},
size: 80,
},
// Name column
{
accessorKey: 'name',
header: t('Name'),
meta: { mobileTitle: true },
cell: ({ row }) => {
const isTagRow = isTagAggregateRow(row.original)
const name = row.getValue('name') as string
const channel = row.original
// 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
const hasParamOverride = Boolean(channel.param_override?.trim())
return (
{isPassThrough && (
}
/>
{t(
'Request body pass-through is enabled. The request body will be sent directly to the upstream without any conversion.'
)}
)}
{hasParamOverride && (
}
/>
{t('Override request parameters')}
)}
{channel.remark && (
}
>
{truncateText(channel.remark, 40)}
{channel.remark}
)}
)
},
size: 260,
minSize: 200,
},
// Type column
{
accessorKey: '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 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}
)}
}
>
{typeName}
{isIonet && (
{
e.stopPropagation()
if (!deploymentId) {
return
}
const targetUrl = `/models/deployments?dFilter=${encodeURIComponent(String(deploymentId))}`
window.open(targetUrl, '_blank', 'noopener')
}}
/>
}
>
{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: 220,
enableSorting: false,
},
// Status column
{
accessorKey: 'status',
header: t('Status'),
meta: { mobileBadge: true },
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',
header: t('Models'),
meta: { mobileHidden: true },
cell: ({ row }) => {
const models = row.getValue('models') as string
const modelArray = parseModelsList(models)
return (
(
))}
/>
)
},
size: 200,
enableSorting: false,
},
// Group column
{
accessorKey: 'group',
header: t('Groups'),
meta: { mobileHidden: true },
cell: ({ row }) => {
const group = row.getValue('group') as string
const groupArray = parseGroupsList(group)
return (
(
))}
/>
)
},
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',
header: t('Tag'),
meta: { mobileHidden: true },
cell: ({ row }) => {
const tag = row.getValue('tag') as string | null
if (!tag) {
return -
}
return (
)
},
size: 120,
enableSorting: false,
},
// Priority column
{
accessorKey: 'priority',
header: t('Priority'),
meta: { mobileHidden: true },
cell: ({ row }) => ,
size: 100,
},
// Weight column
{
accessorKey: 'weight',
header: t('Weight'),
meta: { mobileHidden: true },
cell: ({ row }) => ,
size: 90,
enableSorting: false,
},
// Balance column (Used/Remaining)
{
accessorKey: 'balance',
header: t('Used / Remaining'),
cell: ({ row }) => ,
size: 180,
},
// Response Time column
{
accessorKey: 'response_time',
header: t('Response'),
meta: { mobileHidden: true },
cell: ({ row }) => {
const responseTime = row.getValue('response_time') as number
const config = getResponseTimeConfig(responseTime)
return (
)
},
size: 110,
},
// Test Time column
{
accessorKey: 'test_time',
header: t('Last Tested'),
meta: { mobileHidden: true },
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, locale)
const fullDate = formatTimestampToDate(testTime)
// For valid timestamps, show tooltip with full date
return (
}
/>
{fullDate}
)
},
size: 120,
enableSorting: false,
},
// Actions column
{
id: 'actions',
header: () => t('Actions'),
cell: ({ row }) => {
// Check if this is a tag row (has children)
const isTagRow = isTagAggregateRow(row.original)
if (isTagRow) {
return (
)
}
return
},
enableSorting: false,
enableHiding: false,
meta: { pinned: 'right' as const },
},
],
[enableSelection, t, locale, sensitiveVisible]
)
}