Commit be60e25a by yyhhyyyyyy

fix(channels): refresh channel test dialog status

parent aeea3fae
......@@ -17,7 +17,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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<TData> = {
row: Row<TData>
className?: string
getColumnClassName?: DataTableColumnClassName
cellRenderColumns?: TanstackTable<TData>['options']['columns']
} & Omit<React.ComponentProps<typeof TableRow>, 'children'>
function DataTableRowInner<TData>({
row,
className,
getColumnClassName,
cellRenderColumns,
...rowProps
}: DataTableRowProps<TData>) {
void cellRenderColumns
return (
<TableRow
data-state={row.getIsSelected() ? 'selected' : undefined}
......@@ -60,10 +69,13 @@ export const DataTableRow = React.memo(DataTableRowInner, (prev, next) => {
// 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
......
......@@ -320,6 +320,7 @@ function renderDefaultRow<TData>(
row={row}
className={cn(props.tableBodyRowClassName, props.getRowClassName?.(row))}
getColumnClassName={getColumnClassName}
cellRenderColumns={props.table.options.columns}
/>
)
}
......@@ -17,6 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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<LatestChannelTestCachePatch | undefined>(
(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<ChannelListCache>(
{ 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<TestResult | undefined> => {
async (
model: string,
silent = false,
refreshList = true
): Promise<TestResult | undefined> => {
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 && (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
<Loader2 className='animate-spin' data-icon='inline-start' />
)}
{t('Test')}
</Button>
......@@ -685,7 +811,7 @@ function TestStatusCell({
if (result.status === 'testing') {
return (
<div className='text-muted-foreground flex min-w-0 items-center gap-2 text-sm'>
<Loader2 className='h-4 w-4 shrink-0 animate-spin' />
<Loader2 className='size-4 shrink-0 animate-spin' />
<span className='truncate'>{t('Testing...')}</span>
</div>
)
......@@ -878,7 +1004,7 @@ function TestModelsBulkActions({
>
{disabled ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
<Loader2 className='animate-spin' data-icon='inline-start' />
{t('Testing...')}
</>
) : (
......
......@@ -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 } } }
......
......@@ -143,6 +143,7 @@ export interface ChannelTestResponse {
success: boolean
message?: string
error_code?: string
time?: number
data?: {
response_time?: number
error?: string
......
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