Commit f9e508bd by CaIon

perf(channels): optimize card view layout and reduce re-renders

- Show 3-column card grid from xl breakpoint instead of 2xl
- Cap inline priority/weight width to avoid huge values stretching cards
- Collapse right-column grid to content-sized columns, removing wasted space
- Memoize channel columns, context value, upstream-update result, and ChannelCard
  to avoid rebuilding/re-rendering all cards on unrelated state changes
parent cb841850
......@@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { memo } from 'react'
import { flexRender, type Row } from '@tanstack/react-table'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
......@@ -36,7 +37,7 @@ const SENSITIVE_MASK = '••••'
* priority/weight spinners, balance refresh, response/test times, tag
* expand-collapse, and the per-row (or per-tag) actions menu.
*/
export function ChannelCard({ row }: { row: Row<Channel> }) {
function ChannelCardComponent({ row }: { row: Row<Channel> }) {
const { t } = useTranslation()
const { sensitiveVisible } = useChannels()
const isTagRow = isTagAggregateRow(row.original)
......@@ -118,25 +119,23 @@ export function ChannelCard({ row }: { row: Row<Channel> }) {
</div>
</div>
{/* Right column (sits on the right, content left-aligned) */}
<div className='flex flex-shrink-0 flex-col gap-3'>
<div className='grid grid-cols-2 items-center gap-x-3'>
<span className={labelClass}>{t('Priority')}</span>
<span className={labelClass}>{t('Weight')}</span>
<div className='flex justify-start'>{priorityCell}</div>
<div className='flex justify-start'>{weightCell}</div>
{/* Right column (sits on the right, content left-aligned). A single
grid with content-sized columns keeps Priority/Weight and
Response/Last Tested aligned without wasting horizontal space. */}
<div className='grid flex-shrink-0 grid-cols-[auto_auto] items-center gap-x-3 gap-y-1'>
<span className={labelClass}>{t('Priority')}</span>
<span className={labelClass}>{t('Weight')}</span>
<div className='flex justify-start'>{priorityCell}</div>
<div className='flex justify-start'>{weightCell}</div>
<span className={cn('mt-2', labelClass)}>
{fieldLabels.response_time}
</span>
<span className={cn('mt-2', labelClass)}>{fieldLabels.test_time}</span>
<div className='overflow-hidden text-sm'>
{responseCell ?? <span className='text-muted-foreground'>-</span>}
</div>
<div className='grid grid-cols-2 gap-x-3'>
<div className={cn('mb-1', labelClass)}>
{fieldLabels.response_time}
</div>
<div className={cn('mb-1', labelClass)}>{fieldLabels.test_time}</div>
<div className='overflow-hidden text-sm'>
{responseCell ?? <span className='text-muted-foreground'>-</span>}
</div>
<div className='overflow-hidden text-sm'>
{testCell ?? <span className='text-muted-foreground'>-</span>}
</div>
<div className='overflow-hidden text-sm'>
{testCell ?? <span className='text-muted-foreground'>-</span>}
</div>
</div>
</div>
......@@ -161,3 +160,10 @@ export function ChannelCard({ row }: { row: Row<Channel> }) {
</div>
)
}
/**
* Memoized so each card only re-renders when its own react-table row reference
* changes, instead of every card re-rendering whenever the parent table state
* (filters, pagination, sensitive toggle, etc.) updates.
*/
export const ChannelCard = memo(ChannelCardComponent)
......@@ -27,7 +27,7 @@ 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 { useState, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
......@@ -384,9 +384,7 @@ function BalanceCell({ channel }: { channel: Channel }) {
await handleUpdateChannelBalance(channel.id, queryClient)
setIsUpdating(false)
}
let remainingBadgeLabel = sensitiveVisible
? remainingDisplay
: SENSITIVE_MASK
let remainingBadgeLabel = sensitiveVisible ? remainingDisplay : SENSITIVE_MASK
if (sensitiveVisible && isUpdating) {
remainingBadgeLabel = t('Updating...')
} else if (sensitiveVisible && channel.type === 57) {
......@@ -488,622 +486,634 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
const { t, i18n } = useTranslation()
const { sensitiveVisible } = useChannels()
const locale = i18n.resolvedLanguage || i18n.language
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 (
// 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<ColumnDef<Channel>[]>(
() => [
// Checkbox column
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label='Select row'
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,
},
enableSorting: false,
enableHiding: false,
size: 40,
},
// ID column
{
accessorKey: 'id',
header: t('ID'),
meta: { mobileHidden: true },
cell: ({ row }) => {
const id = row.getValue('id') as number
return <TableId value={sensitiveVisible ? id : SENSITIVE_MASK} />
// ID column
{
accessorKey: 'id',
header: t('ID'),
meta: { mobileHidden: true },
cell: ({ row }) => {
const id = row.getValue('id') as number
return <TableId value={sensitiveVisible ? id : SENSITIVE_MASK} />
},
size: 80,
},
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 (
<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'
// 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 (
<div className='flex items-center gap-2'>
<Button
variant='ghost'
size='sm'
copyable={false}
/>
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>
</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={sensitiveVisible ? name : SENSITIVE_MASK}
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' />
}
/>
<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}>
// 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={sensitiveVisible ? name : SENSITIVE_MASK}
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' />
}
/>
<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' />
}
/>
<TooltipContent side='top'>
{t('Override request parameters')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<UpstreamUpdateTags channel={channel} />
</div>
{channel.remark && (
<TooltipProvider delay={200}>
<Tooltip>
<TooltipTrigger
render={
<SlidersHorizontal className='text-info h-3.5 w-3.5 flex-shrink-0' />
<span className='text-muted-foreground text-xs' />
}
/>
<TooltipContent side='top'>
{t('Override request parameters')}
>
{truncateText(channel.remark, 40)}
</TooltipTrigger>
<TooltipContent side='bottom' className='max-w-xs'>
{channel.remark}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<UpstreamUpdateTags channel={channel} />
</div>
{channel.remark && (
<TooltipProvider delay={200}>
</div>
)
},
minSize: 200,
},
// Type column
{
accessorKey: '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}
className='-ml-1.5'
/>
)
}
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 (
<div className='flex max-w-full min-w-0 items-center gap-2 overflow-hidden'>
{isMultiKey && (
<TooltipProvider delay={100}>
<Tooltip>
<TooltipTrigger
render={
<span className='text-muted-foreground text-xs' />
<span className='border-border bg-muted text-primary inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md border' />
}
>
{truncateText(channel.remark, 40)}
<MultiKeyModeIcon className='h-3 w-3' />
</TooltipTrigger>
<TooltipContent side='bottom' className='max-w-xs'>
{channel.remark}
<TooltipContent side='top'>
{multiKeyTooltip}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</div>
)
},
minSize: 200,
},
// Type column
{
accessorKey: '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}
className='-ml-1.5'
/>
)
}
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 (
<div className='flex max-w-full min-w-0 items-center gap-2 overflow-hidden'>
{isMultiKey && (
<TooltipProvider delay={100}>
<TooltipProvider delay={300}>
<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' />
<div className='max-w-full min-w-0 overflow-hidden' />
}
>
<MultiKeyModeIcon className='h-3 w-3' />
<ProviderBadge
iconKey={`${iconName}.Color`}
iconSize={18}
label={typeName}
colorText={false}
copyable={false}
showDot={false}
className='max-w-full min-w-0 overflow-hidden'
/>
</TooltipTrigger>
<TooltipContent side='top'>{multiKeyTooltip}</TooltipContent>
<TooltipContent side='top'>{typeName}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<TooltipProvider delay={300}>
<Tooltip>
<TooltipTrigger
render={
<div className='max-w-full min-w-0 overflow-hidden' />
}
>
<ProviderBadge
iconKey={`${iconName}.Color`}
iconSize={18}
label={typeName}
colorText={false}
copyable={false}
showDot={false}
className='max-w-full min-w-0 overflow-hidden'
/>
</TooltipTrigger>
<TooltipContent side='top'>{typeName}</TooltipContent>
</Tooltip>
</TooltipProvider>
{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')
}}
{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'
/>
}
>
<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}
</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 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: 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 (
<StatusBadge
label={`Active (${childrenCount})`}
variant='success'
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
} else {
return (
<StatusBadge
label={`Inactive (${childrenCount})`}
variant='neutral'
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
</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: 220,
enableSorting: 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)
: ''
// 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 (
<StatusBadge
label={`Active (${childrenCount})`}
variant='success'
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
} else {
return (
<StatusBadge
label={`Inactive (${childrenCount})`}
variant='neutral'
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
}
} 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>
)
// 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}
/>
)
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,
},
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
// Models column
{
accessorKey: 'models',
header: t('Models'),
meta: { mobileHidden: true },
cell: ({ row }) => {
const models = row.getValue('models') as string
const modelArray = parseModelsList(models)
return (
<BadgeListCell
items={modelArray.map((model) => (
<StatusBadge
key={model}
label={model}
autoColor={model}
size='sm'
className='font-mono'
/>
))}
/>
)
},
size: 200,
enableSorting: 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 (
<BadgeListCell
items={modelArray.map((model) => (
<StatusBadge
key={model}
label={model}
autoColor={model}
size='sm'
className='font-mono'
/>
))}
/>
)
// Group column
{
accessorKey: 'group',
header: t('Groups'),
meta: { mobileHidden: true },
cell: ({ row }) => {
const group = row.getValue('group') as string
const groupArray = parseGroupsList(group)
return (
<BadgeListCell
items={groupArray.map((g) => (
<GroupBadge
key={g}
group={g}
label={sensitiveVisible ? undefined : SENSITIVE_MASK}
size='sm'
/>
))}
/>
)
},
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,
},
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 (
<BadgeListCell
items={groupArray.map((g) => (
<GroupBadge
key={g}
group={g}
label={sensitiveVisible ? undefined : SENSITIVE_MASK}
size='sm'
/>
))}
/>
)
// Tag column
{
accessorKey: 'tag',
header: t('Tag'),
meta: { mobileHidden: true },
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'
className='-ml-1.5'
/>
)
},
size: 120,
enableSorting: false,
},
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))
// Priority column
{
accessorKey: 'priority',
header: t('Priority'),
meta: { mobileHidden: true },
cell: ({ row }) => <PriorityCell channel={row.original} />,
size: 100,
},
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 <span className='text-muted-foreground text-xs'>-</span>
}
return (
<StatusBadge
label={tag}
autoColor={tag}
size='sm'
className='-ml-1.5'
/>
)
// Weight column
{
accessorKey: 'weight',
header: t('Weight'),
meta: { mobileHidden: true },
cell: ({ row }) => <WeightCell channel={row.original} />,
size: 90,
enableSorting: false,
},
size: 120,
enableSorting: false,
},
// Priority column
{
accessorKey: 'priority',
header: t('Priority'),
meta: { mobileHidden: true },
cell: ({ row }) => <PriorityCell channel={row.original} />,
size: 100,
},
// Weight column
{
accessorKey: 'weight',
header: t('Weight'),
meta: { mobileHidden: true },
cell: ({ row }) => <WeightCell channel={row.original} />,
size: 90,
enableSorting: false,
},
// Balance column (Used/Remaining)
{
accessorKey: 'balance',
header: t('Used / Remaining'),
cell: ({ row }) => <BalanceCell channel={row.original} />,
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 (
<StatusBadge
label={formatResponseTime(responseTime, t)}
variant={config.variant}
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
// Balance column (Used/Remaining)
{
accessorKey: 'balance',
header: t('Used / Remaining'),
cell: ({ row }) => <BalanceCell channel={row.original} />,
size: 180,
},
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 <span className='text-muted-foreground text-xs'>-</span>
}
const timeText = formatRelativeTime(testTime, locale)
const fullDate = formatTimestampToDate(testTime)
// 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)
// For valid timestamps, show tooltip with full date
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<StatusBadge
label={timeText}
variant='neutral'
size='sm'
copyable={false}
className='-ml-1.5 cursor-pointer'
/>
}
/>
<TooltipContent side='top'>
<p className='font-mono text-sm'>{fullDate}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
},
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 (
<DataTableTagRowActions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
row={row as any}
<StatusBadge
label={formatResponseTime(responseTime, t)}
variant={config.variant}
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
}
},
size: 110,
},
return <DataTableRowActions row={row} />
// 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 <span className='text-muted-foreground text-xs'>-</span>
}
const timeText = formatRelativeTime(testTime, locale)
const fullDate = formatTimestampToDate(testTime)
// For valid timestamps, show tooltip with full date
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<StatusBadge
label={timeText}
variant='neutral'
size='sm'
copyable={false}
className='-ml-1.5 cursor-pointer'
/>
}
/>
<TooltipContent side='top'>
<p className='font-mono text-sm'>{fullDate}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
},
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 (
<DataTableTagRowActions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
row={row as any}
/>
)
}
return <DataTableRowActions row={row} />
},
size: 132,
enableSorting: false,
enableHiding: false,
meta: { pinned: 'right' as const },
},
size: 132,
enableSorting: false,
enableHiding: false,
meta: { pinned: 'right' as const },
},
]
],
[t, locale, sensitiveVisible]
)
}
......@@ -17,7 +17,13 @@ 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 React, { createContext, useContext, useState, useCallback } from 'react'
import React, {
createContext,
useContext,
useState,
useCallback,
useMemo,
} from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { useChannelUpstreamUpdates } from '../hooks/use-channel-upstream-updates'
import { channelsQueryKeys } from '../lib'
......@@ -88,24 +94,38 @@ export function ChannelsProvider({ children }: { children: React.ReactNode }) {
}, [queryClient])
const upstream = useChannelUpstreamUpdates(refreshChannels)
// useState setters are stable, so the context value only needs to change when
// an actual state value changes. Memoizing avoids handing every consumer
// (including all channel cards/cells) a brand-new object on each render.
const value = useMemo<ChannelsContextType>(
() => ({
open,
setOpen,
currentRow,
setCurrentRow,
currentTag,
setCurrentTag,
enableTagMode,
setEnableTagMode,
idSort,
setIdSort,
sensitiveVisible,
setSensitiveVisible,
upstream,
}),
[
open,
currentRow,
currentTag,
enableTagMode,
idSort,
sensitiveVisible,
upstream,
]
)
return (
<ChannelsContext.Provider
value={{
open,
setOpen,
currentRow,
setCurrentRow,
currentTag,
setCurrentTag,
enableTagMode,
setEnableTagMode,
idSort,
setIdSort,
sensitiveVisible,
setSensitiveVisible,
upstream,
}}
>
<ChannelsContext.Provider value={value}>
{children}
</ChannelsContext.Provider>
)
......
......@@ -371,7 +371,7 @@ export function ChannelsTable() {
enableCardView
viewModeStorageKey={CHANNELS_VIEW_MODE_STORAGE_KEY}
renderCard={(row) => <ChannelCard row={row} />}
cardGridClassName='grid grid-cols-1 gap-3 sm:gap-4 lg:grid-cols-2 2xl:grid-cols-3'
cardGridClassName='grid grid-cols-1 gap-3 sm:gap-4 lg:grid-cols-3'
applyHeaderSize
toolbarProps={{
searchPlaceholder: t('Filter by name, ID, or key...'),
......
......@@ -164,8 +164,9 @@ export function NumericSpinnerInput({
type='button'
onClick={handleStartEdit}
disabled={disabled}
title={localValue}
className={cn(
'h-7 min-w-8 cursor-text px-1 text-center font-mono text-sm tabular-nums',
'h-7 min-w-8 max-w-16 cursor-text truncate px-1 text-center font-mono text-sm tabular-nums',
disabled && 'cursor-default opacity-50'
)}
>
......
......@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useRef, useState, useCallback } from 'react'
import { useRef, useState, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { api, type ApiRequestConfig } from '@/lib/api'
......@@ -285,20 +285,41 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
}
}, [refresh, t])
return {
showModal,
channel,
addModels,
removeModels,
preferredTab,
applyLoading,
detectAllLoading,
applyAllLoading,
openModal,
closeModal,
applyUpdates,
applyAllUpdates,
detectChannelUpdates,
detectAllUpdates,
}
// Memoized so consumers (and the channels context value built from this) get
// a stable reference unless an actual field changes. Callbacks above are all
// useCallback-stable, so this only changes when relevant state changes.
return useMemo(
() => ({
showModal,
channel,
addModels,
removeModels,
preferredTab,
applyLoading,
detectAllLoading,
applyAllLoading,
openModal,
closeModal,
applyUpdates,
applyAllUpdates,
detectChannelUpdates,
detectAllUpdates,
}),
[
showModal,
channel,
addModels,
removeModels,
preferredTab,
applyLoading,
detectAllLoading,
applyAllLoading,
openModal,
closeModal,
applyUpdates,
applyAllUpdates,
detectChannelUpdates,
detectAllUpdates,
]
)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment