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/>. ...@@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { memo } from 'react'
import { flexRender, type Row } from '@tanstack/react-table' import { flexRender, type Row } from '@tanstack/react-table'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
...@@ -36,7 +37,7 @@ const SENSITIVE_MASK = '••••' ...@@ -36,7 +37,7 @@ const SENSITIVE_MASK = '••••'
* priority/weight spinners, balance refresh, response/test times, tag * priority/weight spinners, balance refresh, response/test times, tag
* expand-collapse, and the per-row (or per-tag) actions menu. * 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 { t } = useTranslation()
const { sensitiveVisible } = useChannels() const { sensitiveVisible } = useChannels()
const isTagRow = isTagAggregateRow(row.original) const isTagRow = isTagAggregateRow(row.original)
...@@ -118,25 +119,23 @@ export function ChannelCard({ row }: { row: Row<Channel> }) { ...@@ -118,25 +119,23 @@ export function ChannelCard({ row }: { row: Row<Channel> }) {
</div> </div>
</div> </div>
{/* Right column (sits on the right, content left-aligned) */} {/* Right column (sits on the right, content left-aligned). A single
<div className='flex flex-shrink-0 flex-col gap-3'> grid with content-sized columns keeps Priority/Weight and
<div className='grid grid-cols-2 items-center gap-x-3'> Response/Last Tested aligned without wasting horizontal space. */}
<span className={labelClass}>{t('Priority')}</span> <div className='grid flex-shrink-0 grid-cols-[auto_auto] items-center gap-x-3 gap-y-1'>
<span className={labelClass}>{t('Weight')}</span> <span className={labelClass}>{t('Priority')}</span>
<div className='flex justify-start'>{priorityCell}</div> <span className={labelClass}>{t('Weight')}</span>
<div className='flex justify-start'>{weightCell}</div> <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>
<div className='grid grid-cols-2 gap-x-3'> <div className='overflow-hidden text-sm'>
<div className={cn('mb-1', labelClass)}> {testCell ?? <span className='text-muted-foreground'>-</span>}
{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> </div>
</div> </div>
</div> </div>
...@@ -161,3 +160,10 @@ export function ChannelCard({ row }: { row: Row<Channel> }) { ...@@ -161,3 +160,10 @@ export function ChannelCard({ row }: { row: Row<Channel> }) {
</div> </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/>. ...@@ -27,7 +27,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
/* eslint-disable react-refresh/only-export-components */ /* eslint-disable react-refresh/only-export-components */
import { useState } from 'react' import { useState, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
...@@ -384,9 +384,7 @@ function BalanceCell({ channel }: { channel: Channel }) { ...@@ -384,9 +384,7 @@ function BalanceCell({ channel }: { channel: Channel }) {
await handleUpdateChannelBalance(channel.id, queryClient) await handleUpdateChannelBalance(channel.id, queryClient)
setIsUpdating(false) setIsUpdating(false)
} }
let remainingBadgeLabel = sensitiveVisible let remainingBadgeLabel = sensitiveVisible ? remainingDisplay : SENSITIVE_MASK
? remainingDisplay
: SENSITIVE_MASK
if (sensitiveVisible && isUpdating) { if (sensitiveVisible && isUpdating) {
remainingBadgeLabel = t('Updating...') remainingBadgeLabel = t('Updating...')
} else if (sensitiveVisible && channel.type === 57) { } else if (sensitiveVisible && channel.type === 57) {
...@@ -488,622 +486,634 @@ export function useChannelsColumns(): ColumnDef<Channel>[] { ...@@ -488,622 +486,634 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const { sensitiveVisible } = useChannels() const { sensitiveVisible } = useChannels()
const locale = i18n.resolvedLanguage || i18n.language const locale = i18n.resolvedLanguage || i18n.language
return [ // The column definitions only depend on the translation function, the active
// Checkbox column // locale, and sensitive-data visibility. Memoizing keeps the array (and every
{ // cell renderer reference) stable across unrelated re-renders, so react-table
id: 'select', // does not invalidate the whole row model on each parent render.
header: ({ table }) => ( return useMemo<ColumnDef<Channel>[]>(
<Checkbox () => [
checked={table.getIsAllPageRowsSelected()} // Checkbox column
indeterminate={table.getIsSomePageRowsSelected()} {
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} id: 'select',
aria-label='Select all' header: ({ table }) => (
/>
),
cell: ({ row }) => {
const isTagRow = isTagAggregateRow(row.original)
// Don't show checkbox for tag rows
if (isTagRow) {
return null
}
return (
<Checkbox <Checkbox
checked={row.getIsSelected()} checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)} indeterminate={table.getIsSomePageRowsSelected()}
aria-label='Select row' 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, // ID column
size: 40, {
}, accessorKey: 'id',
header: t('ID'),
// ID column meta: { mobileHidden: true },
{ cell: ({ row }) => {
accessorKey: 'id', const id = row.getValue('id') as number
header: t('ID'), return <TableId value={sensitiveVisible ? id : SENSITIVE_MASK} />
meta: { mobileHidden: true }, },
cell: ({ row }) => { size: 80,
const id = row.getValue('id') as number
return <TableId value={sensitiveVisible ? id : SENSITIVE_MASK} />
}, },
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 ( // Name column
<div className='flex items-center gap-2'> {
<Button accessorKey: 'name',
variant='ghost' header: t('Name'),
size='sm' meta: { mobileTitle: true },
className='h-6 w-6 p-0' cell: ({ row }) => {
onClick={row.getToggleExpandedHandler()} const isTagRow = isTagAggregateRow(row.original)
> const name = row.getValue('name') as string
{row.getIsExpanded() ? ( const channel = row.original
<ChevronDown className='h-4 w-4' />
) : ( // Tag row with expand/collapse
<ChevronRight className='h-4 w-4' /> if (isTagRow) {
)} const tag = (row.original as TagRow).tag || name
</Button> const childrenCount = (row.original as TagRow).children?.length || 0
<div className='flex items-center gap-1.5'>
<span className='font-semibold'>Tag:{tag}</span> return (
<StatusBadge <div className='flex items-center gap-2'>
label={`${childrenCount} channels`} <Button
variant='blue' variant='ghost'
size='sm' 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>
</div> )
) }
}
// Regular channel row // Regular channel row
const settings = parseChannelSettings(channel.setting) const settings = parseChannelSettings(channel.setting)
const isPassThrough = settings.pass_through_body_enabled === true const isPassThrough = settings.pass_through_body_enabled === true
const hasParamOverride = Boolean(channel.param_override?.trim()) const hasParamOverride = Boolean(channel.param_override?.trim())
return ( return (
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
<div className='flex items-center gap-1.5'> <div className='flex items-center gap-1.5'>
<TruncatedText <TruncatedText
text={sensitiveVisible ? name : SENSITIVE_MASK} text={sensitiveVisible ? name : SENSITIVE_MASK}
className='font-medium' className='font-medium'
maxWidth='max-w-[180px]' maxWidth='max-w-[180px]'
/> />
{isPassThrough && ( {isPassThrough && (
<TooltipProvider delay={100}> <TooltipProvider delay={100}>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
render={ render={
<AlertTriangle className='h-3.5 w-3.5 flex-shrink-0 text-amber-500' /> <AlertTriangle className='h-3.5 w-3.5 flex-shrink-0 text-amber-500' />
} }
/> />
<TooltipContent side='top'> <TooltipContent side='top'>
{t( {t(
'Request body pass-through is enabled. The request body will be sent directly to the upstream without any conversion.' 'Request body pass-through is enabled. The request body will be sent directly to the upstream without any conversion.'
)} )}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
)} )}
{hasParamOverride && ( {hasParamOverride && (
<TooltipProvider delay={100}> <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> <Tooltip>
<TooltipTrigger <TooltipTrigger
render={ render={
<SlidersHorizontal className='text-info h-3.5 w-3.5 flex-shrink-0' /> <span className='text-muted-foreground text-xs' />
} }
/> >
<TooltipContent side='top'> {truncateText(channel.remark, 40)}
{t('Override request parameters')} </TooltipTrigger>
<TooltipContent side='bottom' className='max-w-xs'>
{channel.remark}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
)} )}
<UpstreamUpdateTags channel={channel} />
</div> </div>
{channel.remark && ( </div>
<TooltipProvider delay={200}> )
},
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> <Tooltip>
<TooltipTrigger <TooltipTrigger
render={ 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> </TooltipTrigger>
<TooltipContent side='bottom' className='max-w-xs'> <TooltipContent side='top'>
{channel.remark} {multiKeyTooltip}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
)} )}
</div> <TooltipProvider delay={300}>
</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> <Tooltip>
<TooltipTrigger <TooltipTrigger
render={ 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> </TooltipTrigger>
<TooltipContent side='top'>{multiKeyTooltip}</TooltipContent> <TooltipContent side='top'>{typeName}</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
)} {isIonet && (
<TooltipProvider delay={300}> <TooltipProvider delay={100}>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
render={ render={
<div className='max-w-full min-w-0 overflow-hidden' /> <span
} className='flex cursor-pointer items-center gap-1.5 text-xs font-medium'
> onClick={(e) => {
<ProviderBadge e.stopPropagation()
iconKey={`${iconName}.Color`} if (!deploymentId) {
iconSize={18} return
label={typeName} }
colorText={false} const targetUrl = `/models/deployments?dFilter=${encodeURIComponent(String(deploymentId))}`
copyable={false} window.open(targetUrl, '_blank', 'noopener')
showDot={false} }}
className='max-w-full min-w-0 overflow-hidden' />
/> }
</TooltipTrigger> >
<TooltipContent side='top'>{typeName}</TooltipContent> <StatusBadge
</Tooltip> label='IO.NET'
</TooltipProvider> variant='purple'
{isIonet && ( size='sm'
<TooltipProvider delay={100}> copyable={false}
<Tooltip> className='cursor-pointer'
<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')
}}
/> />
} </TooltipTrigger>
> <TooltipContent side='top'>
<StatusBadge <div className='max-w-xs space-y-1'>
label='IO.NET' <div className='text-xs'>
variant='purple' {t('From IO.NET deployment')}
size='sm' </div>
copyable={false} {deploymentId && (
className='cursor-pointer' <div className='text-muted-foreground font-mono text-xs'>
/> {t('Deployment ID')}: {deploymentId}
</TooltipTrigger> </div>
<TooltipContent side='top'> )}
<div className='max-w-xs space-y-1'> <div className='text-muted-foreground text-xs'>
<div className='text-xs'> {t('Click to open deployment')}
{t('From IO.NET deployment')}
</div>
{deploymentId && (
<div className='text-muted-foreground font-mono text-xs'>
{t('Deployment ID')}: {deploymentId}
</div> </div>
)}
<div className='text-muted-foreground text-xs'>
{t('Click to open deployment')}
</div> </div>
</div> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip> </TooltipProvider>
</TooltipProvider> )}
)} </div>
</div> )
) },
}, filterFn: (row, id, value) => {
filterFn: (row, id, value) => { if (!value || value.length === 0 || value.includes('all')) {
if (!value || value.length === 0 || value.includes('all')) { return true
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'
/>
)
} }
} return value.includes(String(row.getValue(id)))
},
size: 220,
enableSorting: false,
},
// Regular channel row // Status column
const config = {
CHANNEL_STATUS_CONFIG[status as keyof typeof CHANNEL_STATUS_CONFIG] || accessorKey: 'status',
CHANNEL_STATUS_CONFIG[0] header: t('Status'),
meta: { mobileBadge: true },
const isMultiKey = isMultiKeyChannel(channel) cell: ({ row }) => {
const keySize = channel.channel_info?.multi_key_size ?? 0 const isTagRow = isTagAggregateRow(row.original)
const disabledCount = channel.channel_info?.multi_key_status_list const status = row.getValue('status') as number
? Object.keys(channel.channel_info.multi_key_status_list).length const channel = row.original as Channel
: 0
const enabledCount = Math.max(0, keySize - disabledCount) // Tag row: show aggregated status
const label = if (isTagRow) {
isMultiKey && keySize > 0 const childrenCount = (row.original as TagRow).children?.length || 0
? `${t(config.label)} (${enabledCount}/${keySize})` const hasEnabled = status === 1
: t(config.label)
if (hasEnabled) {
// Auto-disabled: show reason and time tooltip return (
if (status === 3) { <StatusBadge
let statusReason = '' label={`Active (${childrenCount})`}
let statusTime = '' variant='success'
try { size='sm'
const otherInfo = channel.other_info copyable={false}
? JSON.parse(channel.other_info) className='-ml-1.5'
: null />
if (otherInfo) { )
statusReason = otherInfo.status_reason || '' } else {
statusTime = otherInfo.status_time return (
? formatTimestampToDate(otherInfo.status_time) <StatusBadge
: '' label={`Inactive (${childrenCount})`}
variant='neutral'
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
} }
} catch {
/* empty */
} }
if (statusReason || statusTime) { // Regular channel row
return ( const config =
<TooltipProvider delay={100}> CHANNEL_STATUS_CONFIG[
<Tooltip> status as keyof typeof CHANNEL_STATUS_CONFIG
<TooltipTrigger render={<span />}> ] || CHANNEL_STATUS_CONFIG[0]
<StatusBadge
label={label} const isMultiKey = isMultiKeyChannel(channel)
variant={config.variant} const keySize = channel.channel_info?.multi_key_size ?? 0
size='sm' const disabledCount = channel.channel_info?.multi_key_status_list
copyable={false} ? Object.keys(channel.channel_info.multi_key_status_list).length
/> : 0
</TooltipTrigger> const enabledCount = Math.max(0, keySize - disabledCount)
<TooltipContent side='top' className='max-w-xs'> const label =
<div className='space-y-1 text-xs'> isMultiKey && keySize > 0
{statusReason && ( ? `${t(config.label)} (${enabledCount}/${keySize})`
<div> : t(config.label)
{t('Reason:')} {statusReason}
</div> // Auto-disabled: show reason and time tooltip
)} if (status === 3) {
{statusTime && ( let statusReason = ''
<div> let statusTime = ''
{t('Time:')} {statusTime} try {
</div> const otherInfo = channel.other_info
)} ? JSON.parse(channel.other_info)
</div> : null
</TooltipContent> if (otherInfo) {
</Tooltip> statusReason = otherInfo.status_reason || ''
</TooltipProvider> 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 ( return (
<StatusBadge <StatusBadge
label={label} label={label}
variant={config.variant} variant={config.variant}
size='sm' size='sm'
copyable={false} 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')) { // Models column
return true {
} accessorKey: 'models',
const status = row.getValue(id) as number header: t('Models'),
if (value.includes('enabled')) { meta: { mobileHidden: true },
return status === 1 cell: ({ row }) => {
} const models = row.getValue('models') as string
if (value.includes('disabled')) { const modelArray = parseModelsList(models)
return status !== 1 return (
} <BadgeListCell
return false items={modelArray.map((model) => (
<StatusBadge
key={model}
label={model}
autoColor={model}
size='sm'
className='font-mono'
/>
))}
/>
)
},
size: 200,
enableSorting: false,
}, },
size: 120,
enableSorting: false, // Group column
}, {
accessorKey: 'group',
// Models column header: t('Groups'),
{ meta: { mobileHidden: true },
accessorKey: 'models', cell: ({ row }) => {
header: t('Models'), const group = row.getValue('group') as string
meta: { mobileHidden: true }, const groupArray = parseGroupsList(group)
cell: ({ row }) => { return (
const models = row.getValue('models') as string <BadgeListCell
const modelArray = parseModelsList(models) items={groupArray.map((g) => (
return ( <GroupBadge
<BadgeListCell key={g}
items={modelArray.map((model) => ( group={g}
<StatusBadge label={sensitiveVisible ? undefined : SENSITIVE_MASK}
key={model} size='sm'
label={model} />
autoColor={model} ))}
size='sm' />
className='font-mono' )
/> },
))} 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, // Tag column
}, {
accessorKey: 'tag',
// Group column header: t('Tag'),
{ meta: { mobileHidden: true },
accessorKey: 'group', cell: ({ row }) => {
header: t('Groups'), const tag = row.getValue('tag') as string | null
meta: { mobileHidden: true }, if (!tag) {
cell: ({ row }) => { return <span className='text-muted-foreground text-xs'>-</span>
const group = row.getValue('group') as string }
const groupArray = parseGroupsList(group)
return ( return (
<BadgeListCell <StatusBadge
items={groupArray.map((g) => ( label={tag}
<GroupBadge autoColor={tag}
key={g} size='sm'
group={g} className='-ml-1.5'
label={sensitiveVisible ? undefined : SENSITIVE_MASK} />
size='sm' )
/> },
))} size: 120,
/> enableSorting: false,
)
}, },
filterFn: (row, id, value) => {
if (!value || value.length === 0 || value.includes('all')) { // Priority column
return true {
} accessorKey: 'priority',
const group = row.getValue(id) as string header: t('Priority'),
const groupArray = parseGroupsList(group) meta: { mobileHidden: true },
return groupArray.some((g) => value.includes(g)) 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 ( // Weight column
<StatusBadge {
label={tag} accessorKey: 'weight',
autoColor={tag} header: t('Weight'),
size='sm' meta: { mobileHidden: true },
className='-ml-1.5' cell: ({ row }) => <WeightCell channel={row.original} />,
/> size: 90,
) enableSorting: false,
}, },
size: 120,
enableSorting: false, // Balance column (Used/Remaining)
}, {
accessorKey: 'balance',
// Priority column header: t('Used / Remaining'),
{ cell: ({ row }) => <BalanceCell channel={row.original} />,
accessorKey: 'priority', size: 180,
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'
/>
)
}, },
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) // Response Time column
const fullDate = formatTimestampToDate(testTime) {
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 ( return (
<DataTableTagRowActions <StatusBadge
// eslint-disable-next-line @typescript-eslint/no-explicit-any label={formatResponseTime(responseTime, t)}
row={row as any} 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, [t, locale, sensitiveVisible]
enableHiding: false, )
meta: { pinned: 'right' as const },
},
]
} }
...@@ -17,7 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,7 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
/* eslint-disable react-refresh/only-export-components */ /* 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 { useQueryClient } from '@tanstack/react-query'
import { useChannelUpstreamUpdates } from '../hooks/use-channel-upstream-updates' import { useChannelUpstreamUpdates } from '../hooks/use-channel-upstream-updates'
import { channelsQueryKeys } from '../lib' import { channelsQueryKeys } from '../lib'
...@@ -88,24 +94,38 @@ export function ChannelsProvider({ children }: { children: React.ReactNode }) { ...@@ -88,24 +94,38 @@ export function ChannelsProvider({ children }: { children: React.ReactNode }) {
}, [queryClient]) }, [queryClient])
const upstream = useChannelUpstreamUpdates(refreshChannels) 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 ( return (
<ChannelsContext.Provider <ChannelsContext.Provider value={value}>
value={{
open,
setOpen,
currentRow,
setCurrentRow,
currentTag,
setCurrentTag,
enableTagMode,
setEnableTagMode,
idSort,
setIdSort,
sensitiveVisible,
setSensitiveVisible,
upstream,
}}
>
{children} {children}
</ChannelsContext.Provider> </ChannelsContext.Provider>
) )
......
...@@ -371,7 +371,7 @@ export function ChannelsTable() { ...@@ -371,7 +371,7 @@ export function ChannelsTable() {
enableCardView enableCardView
viewModeStorageKey={CHANNELS_VIEW_MODE_STORAGE_KEY} viewModeStorageKey={CHANNELS_VIEW_MODE_STORAGE_KEY}
renderCard={(row) => <ChannelCard row={row} />} 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 applyHeaderSize
toolbarProps={{ toolbarProps={{
searchPlaceholder: t('Filter by name, ID, or key...'), searchPlaceholder: t('Filter by name, ID, or key...'),
......
...@@ -164,8 +164,9 @@ export function NumericSpinnerInput({ ...@@ -164,8 +164,9 @@ export function NumericSpinnerInput({
type='button' type='button'
onClick={handleStartEdit} onClick={handleStartEdit}
disabled={disabled} disabled={disabled}
title={localValue}
className={cn( 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' disabled && 'cursor-default opacity-50'
)} )}
> >
......
...@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com 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 { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { api, type ApiRequestConfig } from '@/lib/api' import { api, type ApiRequestConfig } from '@/lib/api'
...@@ -285,20 +285,41 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) { ...@@ -285,20 +285,41 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
} }
}, [refresh, t]) }, [refresh, t])
return { // Memoized so consumers (and the channels context value built from this) get
showModal, // a stable reference unless an actual field changes. Callbacks above are all
channel, // useCallback-stable, so this only changes when relevant state changes.
addModels, return useMemo(
removeModels, () => ({
preferredTab, showModal,
applyLoading, channel,
detectAllLoading, addModels,
applyAllLoading, removeModels,
openModal, preferredTab,
closeModal, applyLoading,
applyUpdates, detectAllLoading,
applyAllUpdates, applyAllLoading,
detectChannelUpdates, openModal,
detectAllUpdates, 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