diff --git a/web/default/src/components/data-table/core/data-table-row.tsx b/web/default/src/components/data-table/core/data-table-row.tsx
index 9973e419..456ee87b 100644
--- a/web/default/src/components/data-table/core/data-table-row.tsx
+++ b/web/default/src/components/data-table/core/data-table-row.tsx
@@ -17,7 +17,12 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
-import { flexRender, type Cell, type Row } from '@tanstack/react-table'
+import {
+ flexRender,
+ type Cell,
+ type Row,
+ type Table as TanstackTable,
+} from '@tanstack/react-table'
import { cn } from '@/lib/utils'
import { TableCell, TableRow } from '@/components/ui/table'
import { TruncatedCell } from './truncated-cell'
@@ -27,14 +32,18 @@ type DataTableRowProps = {
row: Row
className?: string
getColumnClassName?: DataTableColumnClassName
+ cellRenderColumns?: TanstackTable['options']['columns']
} & Omit, 'children'>
function DataTableRowInner({
row,
className,
getColumnClassName,
+ cellRenderColumns,
...rowProps
}: DataTableRowProps) {
+ void cellRenderColumns
+
return (
{
// Skip re-render when only the getColumnClassName reference changed but the
// row identity and selection state are the same — callers rarely stabilize
// this callback, so excluding it from comparison avoids unnecessary renders.
+ // Column cell renderers can close over external state while the row stays
+ // stable, so column definitions are part of the render identity.
return (
prev.row === next.row &&
prev.className === next.className &&
- prev.row.getIsSelected() === next.row.getIsSelected()
+ prev.row.getIsSelected() === next.row.getIsSelected() &&
+ prev.cellRenderColumns === next.cellRenderColumns
)
}) as typeof DataTableRowInner
diff --git a/web/default/src/components/data-table/core/data-table-view.tsx b/web/default/src/components/data-table/core/data-table-view.tsx
index 9bc1d1e4..d483e9fb 100644
--- a/web/default/src/components/data-table/core/data-table-view.tsx
+++ b/web/default/src/components/data-table/core/data-table-view.tsx
@@ -320,6 +320,7 @@ function renderDefaultRow(
row={row}
className={cn(props.tableBodyRowClassName, props.getRowClassName?.(row))}
getColumnClassName={getColumnClassName}
+ cellRenderColumns={props.table.options.columns}
/>
)
}
diff --git a/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx b/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx
index 5d65f346..a63817bf 100644
--- a/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx
+++ b/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx
@@ -17,6 +17,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import { useCallback, useEffect, useMemo, useState } from 'react'
+import { useQueryClient } from '@tanstack/react-query'
import {
type ColumnDef,
type RowSelectionState,
@@ -67,7 +68,12 @@ import {
sideDrawerHeaderClassName,
} from '@/components/drawer-layout'
import { StatusBadge } from '@/components/status-badge'
-import { formatResponseTime, handleTestChannel } from '../../lib'
+import {
+ channelsQueryKeys,
+ formatResponseTime,
+ handleTestChannel,
+} from '../../lib'
+import type { GetChannelsResponse, SearchChannelsResponse } from '../../types'
import { useChannels } from '../channels-provider'
type ChannelTestDialogProps = {
@@ -84,10 +90,59 @@ type TestStatus = 'idle' | 'testing' | 'success' | 'error'
type TestResult = {
status: TestStatus
responseTime?: number
+ completedAt?: number
error?: string
errorCode?: string
}
+type ChannelTestCachePatch = {
+ responseTime: number
+ testTime: number
+}
+
+type LatestChannelTestCachePatch = {
+ patch: ChannelTestCachePatch
+ completedAt: number
+}
+
+type ChannelListCache = GetChannelsResponse | SearchChannelsResponse
+
+function createChannelTestCachePatch(
+ responseTime?: number,
+ completedAt = Date.now()
+): ChannelTestCachePatch | undefined {
+ if (typeof responseTime !== 'number' || !Number.isFinite(responseTime)) {
+ return undefined
+ }
+
+ return {
+ responseTime,
+ testTime: Math.floor(completedAt / 1000),
+ }
+}
+
+function getLatestChannelTestCachePatch(
+ results: TestResult[]
+): ChannelTestCachePatch | undefined {
+ const latest = results.reduce(
+ (latestPatch, result) => {
+ const completedAt = result.completedAt ?? 0
+ const patch = createChannelTestCachePatch(
+ result.responseTime,
+ completedAt
+ )
+ if (!patch) return latestPatch
+ if (!latestPatch || completedAt >= latestPatch.completedAt) {
+ return { patch, completedAt }
+ }
+ return latestPatch
+ },
+ undefined
+ )
+
+ return latest?.patch
+}
+
const endpointTypeOptions: Array<{ value: string; label: string }> = [
{ value: 'auto', label: 'Auto detect (default)' },
{ value: 'openai', label: 'OpenAI (/v1/chat/completions)' },
@@ -204,6 +259,8 @@ export function ChannelTestDialog({
}: ChannelTestDialogProps) {
const { t } = useTranslation()
const { currentRow } = useChannels()
+ const queryClient = useQueryClient()
+ const currentChannelId = currentRow?.id
const [endpointType, setEndpointType] = useState('auto')
const [isStreamTest, setIsStreamTest] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
@@ -301,8 +358,60 @@ export function ChannelTestDialog({
}))
}, [])
+ const updateChannelTestCache = useCallback(
+ (patch?: ChannelTestCachePatch) => {
+ if (!patch || currentChannelId === undefined) return
+
+ queryClient.setQueriesData(
+ { queryKey: channelsQueryKeys.lists() },
+ (oldData) => {
+ const data = oldData?.data
+ if (!oldData || !data?.items.length) return oldData
+
+ let changed = false
+ const nextItems = data.items.map((channel) => {
+ if (channel.id !== currentChannelId) return channel
+
+ changed = true
+ return {
+ ...channel,
+ response_time: patch.responseTime,
+ test_time: patch.testTime,
+ }
+ })
+
+ if (!changed) return oldData
+
+ return {
+ ...oldData,
+ data: {
+ ...data,
+ items: nextItems,
+ },
+ }
+ }
+ )
+ },
+ [currentChannelId, queryClient]
+ )
+
+ const refreshChannelLists = useCallback(
+ (patch?: ChannelTestCachePatch) => {
+ updateChannelTestCache(patch)
+ void queryClient
+ .invalidateQueries({ queryKey: channelsQueryKeys.lists() })
+ .then(() => updateChannelTestCache(patch))
+ .catch(() => undefined)
+ },
+ [queryClient, updateChannelTestCache]
+ )
+
const testSingleModel = useCallback(
- async (model: string, silent = false): Promise => {
+ async (
+ model: string,
+ silent = false,
+ refreshList = true
+ ): Promise => {
if (!currentRow) return
markModelTesting(model, true)
@@ -319,9 +428,11 @@ export function ChannelTestDialog({
silent,
},
(success, responseTime, error, errorCode) => {
+ const completedAt = Date.now()
finalResult = {
status: success ? 'success' : 'error',
responseTime,
+ completedAt,
error,
errorCode,
}
@@ -331,11 +442,20 @@ export function ChannelTestDialog({
} catch (error: unknown) {
finalResult = {
status: 'error',
+ completedAt: Date.now(),
error: error instanceof Error ? error.message : t('Test failed'),
}
updateTestResult(model, finalResult)
} finally {
markModelTesting(model, false)
+ if (refreshList) {
+ refreshChannelLists(
+ createChannelTestCachePatch(
+ finalResult?.responseTime,
+ finalResult?.completedAt
+ )
+ )
+ }
}
return finalResult
},
@@ -344,6 +464,7 @@ export function ChannelTestDialog({
endpointType,
isStreamTest,
markModelTesting,
+ refreshChannelLists,
t,
updateTestResult,
]
@@ -354,15 +475,19 @@ export function ChannelTestDialog({
if (!modelsToTest.length) return
setIsBatchTesting(true)
+ let resultPatch: ChannelTestCachePatch | undefined
try {
const settled = await Promise.allSettled(
- modelsToTest.map((modelName) => testSingleModel(modelName, true))
+ modelsToTest.map((modelName) =>
+ testSingleModel(modelName, true, false)
+ )
)
const results = settled
.map((result) =>
result.status === 'fulfilled' ? result.value : undefined
)
.filter((result): result is TestResult => Boolean(result))
+ resultPatch = getLatestChannelTestCachePatch(results)
const successCount = results.filter(
(result) => result.status === 'success'
).length
@@ -387,9 +512,10 @@ export function ChannelTestDialog({
} finally {
setIsBatchTesting(false)
setRowSelection({})
+ refreshChannelLists(resultPatch)
}
},
- [t, testSingleModel]
+ [refreshChannelLists, t, testSingleModel]
)
const handleClose = () => {
@@ -482,7 +608,7 @@ export function ChannelTestDialog({
disabled={isTestingModel || isBatchTesting}
>
{isTestingModel && (
-
+
)}
{t('Test')}
@@ -685,7 +811,7 @@ function TestStatusCell({
if (result.status === 'testing') {
return (
-
+
{t('Testing...')}
)
@@ -878,7 +1004,7 @@ function TestModelsBulkActions({
>
{disabled ? (
<>
-
+
{t('Testing...')}
>
) : (
diff --git a/web/default/src/features/channels/lib/channel-actions.ts b/web/default/src/features/channels/lib/channel-actions.ts
index a4d0e09a..bd8dfd71 100644
--- a/web/default/src/features/channels/lib/channel-actions.ts
+++ b/web/default/src/features/channels/lib/channel-actions.ts
@@ -37,7 +37,7 @@ import {
updateChannelBalance,
} from '../api'
import { CHANNEL_STATUS, ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants'
-import type { CopyChannelParams } from '../types'
+import type { ChannelTestResponse, CopyChannelParams } from '../types'
// ============================================================================
// Query Keys
@@ -52,6 +52,25 @@ export const channelsQueryKeys = {
detail: (id: number) => [...channelsQueryKeys.details(), id] as const,
}
+function getChannelTestResponseTime(
+ response: ChannelTestResponse
+): number | undefined {
+ const responseTime = response.data?.response_time
+ if (typeof responseTime === 'number' && Number.isFinite(responseTime)) {
+ return responseTime
+ }
+
+ if (
+ typeof response.time === 'number' &&
+ Number.isFinite(response.time) &&
+ response.time > 0
+ ) {
+ return Math.round(response.time * 1000)
+ }
+
+ return undefined
+}
+
// ============================================================================
// Single Channel Actions
// ============================================================================
@@ -237,16 +256,22 @@ export async function handleTestChannel(
try {
const response = await testChannel(id, payload)
+ const responseTime = getChannelTestResponseTime(response)
if (response.success) {
if (!options?.silent) {
toast.success(i18next.t(SUCCESS_MESSAGES.TESTED))
}
- onTestComplete?.(true, response.data?.response_time)
+ onTestComplete?.(true, responseTime)
} else {
if (!options?.silent) {
toast.error(response.message || i18next.t(ERROR_MESSAGES.TEST_FAILED))
}
- onTestComplete?.(false, undefined, response.message, response.error_code)
+ onTestComplete?.(
+ false,
+ responseTime,
+ response.message,
+ response.error_code
+ )
}
} catch (_error: unknown) {
const err = _error as { response?: { data?: { message?: string } } }
diff --git a/web/default/src/features/channels/types.ts b/web/default/src/features/channels/types.ts
index a282053a..d115ece4 100644
--- a/web/default/src/features/channels/types.ts
+++ b/web/default/src/features/channels/types.ts
@@ -143,6 +143,7 @@ export interface ChannelTestResponse {
success: boolean
message?: string
error_code?: string
+ time?: number
data?: {
response_time?: number
error?: string