-
-
-
- {isPassThrough && (
-
-
-
- }
- />
-
- {t(
- 'Request body pass-through is enabled. The request body will be sent directly to the upstream without any conversion.'
- )}
-
-
-
- )}
- {hasParamOverride && (
-
-
-
- }
- />
-
- {t('Override request parameters')}
-
-
-
- )}
-
-
- {channel.remark && (
-
+ if (isTagRow) {
+ return (
+
+ )
+ }
+
+ const type = row.getValue('type') as number
+ const typeNameKey = getChannelTypeLabel(type)
+ const typeName = t(typeNameKey)
+ const iconName = getChannelTypeIcon(type)
+ const channel = row.original as Channel
+ const isMultiKey = isMultiKeyChannel(channel)
+ const multiKeyMode = channel.channel_info?.multi_key_mode ?? 'random'
+ const MultiKeyModeIcon =
+ multiKeyMode === 'random' ? Shuffle : ListOrdered
+ const multiKeyTooltip =
+ multiKeyMode === 'random'
+ ? t('Multi-key: Random rotation')
+ : t('Multi-key: Polling rotation')
+
+ const ionetMeta = parseIonetMeta(channel.other_info)
+ const isIonet = ionetMeta?.source === 'ionet'
+ const deploymentId =
+ typeof ionetMeta?.deployment_id === 'string'
+ ? ionetMeta?.deployment_id
+ : undefined
+
+ return (
+
+ {isMultiKey && (
+
+
}
>
- {truncateText(channel.remark, 40)}
+
-
- {channel.remark}
+
+ {multiKeyTooltip}
+
+
+
+ )}
+
+
+
+ }
+ >
+
+
+ {typeName}
+
+
+ {isIonet && (
+
+
+ {
+ e.stopPropagation()
+ if (!deploymentId) {
+ return
+ }
+ const targetUrl = `/models/deployments?dFilter=${encodeURIComponent(String(deploymentId))}`
+ window.open(targetUrl, '_blank', 'noopener')
+ }}
+ />
+ }
+ >
+
+
+
+
+
+ {t('From IO.NET deployment')}
+
+ {deploymentId && (
+
+ {t('Deployment ID')}: {deploymentId}
+
+ )}
+
+ {t('Click to open deployment')}
+
+
)}
-
- )
+ )
+ },
+ filterFn: (row, id, value) => {
+ if (!value || value.length === 0 || value.includes('all')) {
+ return true
+ }
+ return value.includes(String(row.getValue(id)))
+ },
+ size: 220,
+ enableSorting: false,
},
- minSize: 200,
- },
- // Type column
- {
- accessorKey: 'type',
- header: t('Type'),
- cell: ({ row }) => {
- const isTagRow = isTagAggregateRow(row.original)
+ // Status column
+ {
+ accessorKey: 'status',
+ header: t('Status'),
+ meta: { mobileBadge: true },
+ cell: ({ row }) => {
+ const isTagRow = isTagAggregateRow(row.original)
+ const status = row.getValue('status') as number
+ const channel = row.original as Channel
+
+ // Tag row: show aggregated status
+ if (isTagRow) {
+ const childrenCount = (row.original as TagRow).children?.length || 0
+ const hasEnabled = status === 1
+
+ if (hasEnabled) {
+ return (
+
+ )
+ } else {
+ return (
+
+ )
+ }
+ }
+
+ // Regular channel row
+ const config =
+ CHANNEL_STATUS_CONFIG[
+ status as keyof typeof CHANNEL_STATUS_CONFIG
+ ] || CHANNEL_STATUS_CONFIG[0]
+
+ const isMultiKey = isMultiKeyChannel(channel)
+ const keySize = channel.channel_info?.multi_key_size ?? 0
+ const disabledCount = channel.channel_info?.multi_key_status_list
+ ? Object.keys(channel.channel_info.multi_key_status_list).length
+ : 0
+ const enabledCount = Math.max(0, keySize - disabledCount)
+ const label =
+ isMultiKey && keySize > 0
+ ? `${t(config.label)} (${enabledCount}/${keySize})`
+ : t(config.label)
+
+ // Auto-disabled: show reason and time tooltip
+ if (status === 3) {
+ let statusReason = ''
+ let statusTime = ''
+ try {
+ const otherInfo = channel.other_info
+ ? JSON.parse(channel.other_info)
+ : null
+ if (otherInfo) {
+ statusReason = otherInfo.status_reason || ''
+ statusTime = otherInfo.status_time
+ ? formatTimestampToDate(otherInfo.status_time)
+ : ''
+ }
+ } catch {
+ /* empty */
+ }
+
+ if (statusReason || statusTime) {
+ return (
+
+
+ }>
+
+
+
+
+ {statusReason && (
+
+ {t('Reason:')} {statusReason}
+
+ )}
+ {statusTime && (
+
+ {t('Time:')} {statusTime}
+
+ )}
+
+
+
+
+ )
+ }
+ }
- if (isTagRow) {
return (
+ )
+ },
+ filterFn: (row, id, value) => {
+ if (!value || value.length === 0 || value.includes('all')) {
+ return true
+ }
+ const status = row.getValue(id) as number
+ if (value.includes('enabled')) {
+ return status === 1
+ }
+ if (value.includes('disabled')) {
+ return status !== 1
+ }
+ return false
+ },
+ size: 120,
+ enableSorting: false,
+ },
+
+ // Models column
+ {
+ accessorKey: 'models',
+ header: t('Models'),
+ meta: { mobileHidden: true },
+ cell: ({ row }) => {
+ const models = row.getValue('models') as string
+ const modelArray = parseModelsList(models)
+ return (
+
(
+
+ ))}
+ />
+ )
+ },
+ size: 200,
+ enableSorting: false,
+ },
+
+ // Group column
+ {
+ accessorKey: 'group',
+ header: t('Groups'),
+ meta: { mobileHidden: true },
+ cell: ({ row }) => {
+ const group = row.getValue('group') as string
+ const groupArray = parseGroupsList(group)
+ return (
+ (
+
+ ))}
+ />
+ )
+ },
+ filterFn: (row, id, value) => {
+ if (!value || value.length === 0 || value.includes('all')) {
+ return true
+ }
+ const group = row.getValue(id) as string
+ const groupArray = parseGroupsList(group)
+ return groupArray.some((g) => value.includes(g))
+ },
+ size: 150,
+ enableSorting: false,
+ },
+
+ // Tag column
+ {
+ accessorKey: 'tag',
+ header: t('Tag'),
+ meta: { mobileHidden: true },
+ cell: ({ row }) => {
+ const tag = row.getValue('tag') as string | null
+ if (!tag) {
+ return -
+ }
+
+ return (
+
+ )
+ },
+ size: 120,
+ enableSorting: false,
+ },
+
+ // Priority column
+ {
+ accessorKey: 'priority',
+ header: t('Priority'),
+ meta: { mobileHidden: true },
+ cell: ({ row }) => ,
+ size: 100,
+ },
+
+ // Weight column
+ {
+ accessorKey: 'weight',
+ header: t('Weight'),
+ meta: { mobileHidden: true },
+ cell: ({ row }) => ,
+ size: 90,
+ enableSorting: false,
+ },
+
+ // Balance column (Used/Remaining)
+ {
+ accessorKey: 'balance',
+ header: t('Used / Remaining'),
+ cell: ({ row }) => ,
+ size: 180,
+ },
+
+ // Response Time column
+ {
+ accessorKey: 'response_time',
+ header: t('Response'),
+ meta: { mobileHidden: true },
+ cell: ({ row }) => {
+ const responseTime = row.getValue('response_time') as number
+ const config = getResponseTimeConfig(responseTime)
+
+ return (
+
)
- }
+ },
+ size: 110,
+ },
- 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')
+ // Test Time column
+ {
+ accessorKey: 'test_time',
+ header: t('Last Tested'),
+ meta: { mobileHidden: true },
+ cell: ({ row }) => {
+ const testTime = row.getValue('test_time') as number
- const ionetMeta = parseIonetMeta(channel.other_info)
- const isIonet = ionetMeta?.source === 'ionet'
- const deploymentId =
- typeof ionetMeta?.deployment_id === 'string'
- ? ionetMeta?.deployment_id
- : undefined
+ // For invalid timestamps, show "Never" badge
+ if (!testTime || testTime === 0) {
+ return -
+ }
- return (
-
- {isMultiKey && (
-
-
-
- }
- >
-
-
- {multiKeyTooltip}
-
-
- )}
-
+ const timeText = formatRelativeTime(testTime, locale)
+ const fullDate = formatTimestampToDate(testTime)
+
+ // For valid timestamps, show tooltip with full date
+ return (
+
+
}
- >
-
-
- {typeName}
+ />
+
+ {fullDate}
+
- {isIonet && (
-
-
- {
- e.stopPropagation()
- if (!deploymentId) {
- return
- }
- const targetUrl = `/models/deployments?dFilter=${encodeURIComponent(String(deploymentId))}`
- window.open(targetUrl, '_blank', 'noopener')
- }}
- />
- }
- >
-
-
-
-
-
- {t('From IO.NET deployment')}
-
- {deploymentId && (
-
- {t('Deployment ID')}: {deploymentId}
-
- )}
-
- {t('Click to open deployment')}
-
-
-
-
-
- )}
-
- )
- },
- filterFn: (row, id, value) => {
- if (!value || value.length === 0 || value.includes('all')) {
- return true
- }
- return value.includes(String(row.getValue(id)))
- },
- size: 220,
- enableSorting: false,
- },
-
- // Status column
- {
- accessorKey: 'status',
- header: t('Status'),
- meta: { mobileBadge: true },
- cell: ({ row }) => {
- const isTagRow = isTagAggregateRow(row.original)
- const status = row.getValue('status') as number
- const channel = row.original as Channel
-
- // Tag row: show aggregated status
- if (isTagRow) {
- const childrenCount = (row.original as TagRow).children?.length || 0
- const hasEnabled = status === 1
-
- if (hasEnabled) {
- return (
-
- )
- } else {
- return (
-
- )
- }
- }
-
- // Regular channel row
- const config =
- CHANNEL_STATUS_CONFIG[status as keyof typeof CHANNEL_STATUS_CONFIG] ||
- CHANNEL_STATUS_CONFIG[0]
-
- const isMultiKey = isMultiKeyChannel(channel)
- const keySize = channel.channel_info?.multi_key_size ?? 0
- const disabledCount = channel.channel_info?.multi_key_status_list
- ? Object.keys(channel.channel_info.multi_key_status_list).length
- : 0
- const enabledCount = Math.max(0, keySize - disabledCount)
- const label =
- isMultiKey && keySize > 0
- ? `${t(config.label)} (${enabledCount}/${keySize})`
- : t(config.label)
-
- // Auto-disabled: show reason and time tooltip
- if (status === 3) {
- let statusReason = ''
- let statusTime = ''
- try {
- const otherInfo = channel.other_info
- ? JSON.parse(channel.other_info)
- : null
- if (otherInfo) {
- statusReason = otherInfo.status_reason || ''
- statusTime = otherInfo.status_time
- ? formatTimestampToDate(otherInfo.status_time)
- : ''
- }
- } catch {
- /* empty */
- }
-
- if (statusReason || statusTime) {
- return (
-
-
- }>
-
-
-
-
- {statusReason && (
-
- {t('Reason:')} {statusReason}
-
- )}
- {statusTime && (
-
- {t('Time:')} {statusTime}
-
- )}
-
-
-
-
- )
- }
- }
-
- return (
-
- )
- },
- filterFn: (row, id, value) => {
- if (!value || value.length === 0 || value.includes('all')) {
- return true
- }
- const status = row.getValue(id) as number
- if (value.includes('enabled')) {
- return status === 1
- }
- if (value.includes('disabled')) {
- return status !== 1
- }
- return false
- },
- size: 120,
- enableSorting: false,
- },
-
- // Models column
- {
- accessorKey: 'models',
- header: t('Models'),
- meta: { mobileHidden: true },
- cell: ({ row }) => {
- const models = row.getValue('models') as string
- const modelArray = parseModelsList(models)
- return (
- (
-
- ))}
- />
- )
- },
- size: 200,
- enableSorting: false,
- },
-
- // Group column
- {
- accessorKey: 'group',
- header: t('Groups'),
- meta: { mobileHidden: true },
- cell: ({ row }) => {
- const group = row.getValue('group') as string
- const groupArray = parseGroupsList(group)
- return (
- (
-
- ))}
- />
- )
- },
- filterFn: (row, id, value) => {
- if (!value || value.length === 0 || value.includes('all')) {
- return true
- }
- const group = row.getValue(id) as string
- const groupArray = parseGroupsList(group)
- return groupArray.some((g) => value.includes(g))
- },
- size: 150,
- enableSorting: false,
- },
-
- // Tag column
- {
- accessorKey: 'tag',
- header: t('Tag'),
- meta: { mobileHidden: true },
- cell: ({ row }) => {
- const tag = row.getValue('tag') as string | null
- if (!tag) {
- return -
- }
-
- return (
-
- )
- },
- size: 120,
- enableSorting: false,
- },
-
- // Priority column
- {
- accessorKey: 'priority',
- header: t('Priority'),
- meta: { mobileHidden: true },
- cell: ({ row }) => ,
- size: 100,
- },
-
- // Weight column
- {
- accessorKey: 'weight',
- header: t('Weight'),
- meta: { mobileHidden: true },
- cell: ({ row }) => ,
- size: 90,
- enableSorting: false,
- },
-
- // Balance column (Used/Remaining)
- {
- accessorKey: 'balance',
- header: t('Used / Remaining'),
- cell: ({ row }) => ,
- size: 180,
- },
-
- // Response Time column
- {
- accessorKey: 'response_time',
- header: t('Response'),
- meta: { mobileHidden: true },
- cell: ({ row }) => {
- const responseTime = row.getValue('response_time') as number
- const config = getResponseTimeConfig(responseTime)
-
- return (
-
- )
- },
- size: 110,
- },
-
- // Test Time column
- {
- accessorKey: 'test_time',
- header: t('Last Tested'),
- meta: { mobileHidden: true },
- cell: ({ row }) => {
- const testTime = row.getValue('test_time') as number
-
- // For invalid timestamps, show "Never" badge
- if (!testTime || testTime === 0) {
- return -
- }
-
- const timeText = formatRelativeTime(testTime, locale)
- const fullDate = formatTimestampToDate(testTime)
-
- // For valid timestamps, show tooltip with full date
- return (
-
-
-
- }
- />
-
- {fullDate}
-
-
-
- )
- },
- size: 120,
- enableSorting: false,
- },
-
- // Actions column
- {
- id: 'actions',
- header: () => t('Actions'),
- cell: ({ row }) => {
- // Check if this is a tag row (has children)
- const isTagRow = isTagAggregateRow(row.original)
-
- if (isTagRow) {
- return (
-
)
- }
-
- return
+ },
+ size: 120,
+ enableSorting: false,
},
- size: 132,
- enableSorting: false,
- enableHiding: false,
- meta: { pinned: 'right' as const },
- },
- ]
+
+ // 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
+ },
+ size: 132,
+ enableSorting: false,
+ enableHiding: false,
+ meta: { pinned: 'right' as const },
+ },
+ ],
+ [t, locale, sensitiveVisible]
+ )
}
diff --git a/web/default/src/features/channels/components/channels-provider.tsx b/web/default/src/features/channels/components/channels-provider.tsx
index 6f4b9a07..a5db5579 100644
--- a/web/default/src/features/channels/components/channels-provider.tsx
+++ b/web/default/src/features/channels/components/channels-provider.tsx
@@ -17,7 +17,13 @@ along with this program. If not, see .
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(
+ () => ({
+ open,
+ setOpen,
+ currentRow,
+ setCurrentRow,
+ currentTag,
+ setCurrentTag,
+ enableTagMode,
+ setEnableTagMode,
+ idSort,
+ setIdSort,
+ sensitiveVisible,
+ setSensitiveVisible,
+ upstream,
+ }),
+ [
+ open,
+ currentRow,
+ currentTag,
+ enableTagMode,
+ idSort,
+ sensitiveVisible,
+ upstream,
+ ]
+ )
+
return (
-
+
{children}
)
diff --git a/web/default/src/features/channels/components/channels-table.tsx b/web/default/src/features/channels/components/channels-table.tsx
index 9d22e494..7e002b76 100644
--- a/web/default/src/features/channels/components/channels-table.tsx
+++ b/web/default/src/features/channels/components/channels-table.tsx
@@ -371,7 +371,7 @@ export function ChannelsTable() {
enableCardView
viewModeStorageKey={CHANNELS_VIEW_MODE_STORAGE_KEY}
renderCard={(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...'),
diff --git a/web/default/src/features/channels/components/numeric-spinner-input.tsx b/web/default/src/features/channels/components/numeric-spinner-input.tsx
index ebfd80b7..780d1416 100644
--- a/web/default/src/features/channels/components/numeric-spinner-input.tsx
+++ b/web/default/src/features/channels/components/numeric-spinner-input.tsx
@@ -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'
)}
>
diff --git a/web/default/src/features/channels/hooks/use-channel-upstream-updates.ts b/web/default/src/features/channels/hooks/use-channel-upstream-updates.ts
index e28c21d9..ab190de1 100644
--- a/web/default/src/features/channels/hooks/use-channel-upstream-updates.ts
+++ b/web/default/src/features/channels/hooks/use-channel-upstream-updates.ts
@@ -16,7 +16,7 @@ along with this program. If not, see .
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) {
}
}, [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,
+ ]
+ )
}