Files
chaos-api/web/default/src/features/channels/components/channels-columns.tsx
T
2026-05-26 20:28:28 +08:00

1066 lines
32 KiB
TypeScript
Vendored
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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 <https://www.gnu.org/licenses/>.
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,
SlidersHorizontal,
} 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 { 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, StatusBadgeList } from '@/components/status-badge'
import { TableId } from '@/components/table-id'
import { TruncatedText } from '@/components/truncated-text'
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 {
return (
<StatusBadgeList
items={items}
max={maxDisplay}
renderItem={(item) => item}
/>
)
}
/**
* 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))
const usedLabel = `${t('Used:')} ${usedDisplay}`
const remainingLabel = `${t('Remaining:')} ${remainingDisplay}`
// Tag row: only show cumulative used quota
if (isTagRow) {
return (
<StatusBadge
label={usedLabel}
variant='neutral'
size='sm'
copyable={false}
showDot={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'>
<Tooltip>
<TooltipTrigger
render={
<StatusBadge
label={usedDisplay}
variant='neutral'
size='sm'
copyable={false}
showDot={false}
className='cursor-help'
/>
}
/>
<TooltipContent>
<p>{usedLabel}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<StatusBadge
label={
isUpdating
? t('Updating...')
: channel.type === 57
? t('Account Info')
: remainingDisplay
}
variant={
channel.type === 57
? 'info'
: isUpdating
? 'neutral'
: variant
}
size='sm'
copyable={false}
showDot={false}
className='cursor-pointer'
onClick={handleClickUpdate}
/>
}
/>
<TooltipContent>
<p>
{channel.type === 57
? t('Click to view Codex usage')
: remainingLabel}
</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()}
indeterminate={table.getIsSomePageRowsSelected()}
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 <TableId value={id} />
},
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
// 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
const hasParamOverride = Boolean(channel.param_override?.trim())
return (
<div className='flex items-center gap-2'>
<div className='flex flex-col gap-1'>
<div className='flex items-center gap-1.5'>
<TruncatedText
text={name}
className='font-medium'
maxWidth='max-w-[180px]'
/>
{isPassThrough && (
<TooltipProvider delay={100}>
<Tooltip>
<TooltipTrigger
render={
<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>
)}
{hasParamOverride && (
<TooltipProvider delay={100}>
<Tooltip>
<TooltipTrigger
render={
<SlidersHorizontal className='text-info h-3.5 w-3.5 flex-shrink-0' />
}
></TooltipTrigger>
<TooltipContent side='top'>
{t('Override request parameters')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<UpstreamUpdateTags channel={channel} />
</div>
{channel.remark && (
<TooltipProvider delay={200}>
<Tooltip>
<TooltipTrigger
render={
<span className='text-muted-foreground text-xs' />
}
>
{truncateText(channel.remark, 40)}
</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`, 14)
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'>
{isMultiKey && (
<TooltipProvider delay={100}>
<Tooltip>
<TooltipTrigger
render={
<span className='border-border bg-muted text-primary inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md border' />
}
>
<MultiKeyModeIcon className='h-3 w-3' />
</TooltipTrigger>
<TooltipContent side='top'>{multiKeyTooltip}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<StatusBadge
autoColor={typeName}
size='sm'
copyable={false}
showDot={false}
className='gap-1 pl-1'
>
{icon}
<span className='truncate'>{typeName}</span>
</StatusBadge>
{isIonet && (
<TooltipProvider delay={100}>
<Tooltip>
<TooltipTrigger
render={
<span
className='flex cursor-pointer items-center gap-1.5 text-xs font-medium'
onClick={(e) => {
e.stopPropagation()
if (!deploymentId) return
const targetUrl = `/models/deployments?dFilter=${encodeURIComponent(String(deploymentId))}`
window.open(targetUrl, '_blank', 'noopener')
}}
/>
}
>
<StatusBadge
label='IO.NET'
variant='purple'
size='sm'
copyable={false}
className='cursor-pointer'
/>
</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'
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 delay={100}>
<Tooltip>
<TooltipTrigger render={<span />}>
<StatusBadge
label={label}
variant={config.variant}
size='sm'
copyable={false}
/>
</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}
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 render={<div />}>
{renderLimitedItems(modelBadges, 2)}
</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 render={<div />}>
{renderLimitedItems(groupBadges, 2)}
</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
render={
<span className='text-muted-foreground cursor-pointer font-mono text-sm' />
}
>
{timeText}
</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,
},
]
}