mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-06 17:46:23 +00:00
fix(channels): refresh channel test dialog status
This commit is contained in:
@@ -17,7 +17,12 @@ 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 * as React from 'react'
|
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 { cn } from '@/lib/utils'
|
||||||
import { TableCell, TableRow } from '@/components/ui/table'
|
import { TableCell, TableRow } from '@/components/ui/table'
|
||||||
import { TruncatedCell } from './truncated-cell'
|
import { TruncatedCell } from './truncated-cell'
|
||||||
@@ -27,14 +32,18 @@ type DataTableRowProps<TData> = {
|
|||||||
row: Row<TData>
|
row: Row<TData>
|
||||||
className?: string
|
className?: string
|
||||||
getColumnClassName?: DataTableColumnClassName
|
getColumnClassName?: DataTableColumnClassName
|
||||||
|
cellRenderColumns?: TanstackTable<TData>['options']['columns']
|
||||||
} & Omit<React.ComponentProps<typeof TableRow>, 'children'>
|
} & Omit<React.ComponentProps<typeof TableRow>, 'children'>
|
||||||
|
|
||||||
function DataTableRowInner<TData>({
|
function DataTableRowInner<TData>({
|
||||||
row,
|
row,
|
||||||
className,
|
className,
|
||||||
getColumnClassName,
|
getColumnClassName,
|
||||||
|
cellRenderColumns,
|
||||||
...rowProps
|
...rowProps
|
||||||
}: DataTableRowProps<TData>) {
|
}: DataTableRowProps<TData>) {
|
||||||
|
void cellRenderColumns
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
data-state={row.getIsSelected() ? 'selected' : undefined}
|
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
|
// Skip re-render when only the getColumnClassName reference changed but the
|
||||||
// row identity and selection state are the same — callers rarely stabilize
|
// row identity and selection state are the same — callers rarely stabilize
|
||||||
// this callback, so excluding it from comparison avoids unnecessary renders.
|
// 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 (
|
return (
|
||||||
prev.row === next.row &&
|
prev.row === next.row &&
|
||||||
prev.className === next.className &&
|
prev.className === next.className &&
|
||||||
prev.row.getIsSelected() === next.row.getIsSelected()
|
prev.row.getIsSelected() === next.row.getIsSelected() &&
|
||||||
|
prev.cellRenderColumns === next.cellRenderColumns
|
||||||
)
|
)
|
||||||
}) as typeof DataTableRowInner
|
}) as typeof DataTableRowInner
|
||||||
|
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ function renderDefaultRow<TData>(
|
|||||||
row={row}
|
row={row}
|
||||||
className={cn(props.tableBodyRowClassName, props.getRowClassName?.(row))}
|
className={cn(props.tableBodyRowClassName, props.getRowClassName?.(row))}
|
||||||
getColumnClassName={getColumnClassName}
|
getColumnClassName={getColumnClassName}
|
||||||
|
cellRenderColumns={props.table.options.columns}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+133
-7
@@ -17,6 +17,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 { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
type RowSelectionState,
|
type RowSelectionState,
|
||||||
@@ -67,7 +68,12 @@ import {
|
|||||||
sideDrawerHeaderClassName,
|
sideDrawerHeaderClassName,
|
||||||
} from '@/components/drawer-layout'
|
} from '@/components/drawer-layout'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
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'
|
import { useChannels } from '../channels-provider'
|
||||||
|
|
||||||
type ChannelTestDialogProps = {
|
type ChannelTestDialogProps = {
|
||||||
@@ -84,10 +90,59 @@ type TestStatus = 'idle' | 'testing' | 'success' | 'error'
|
|||||||
type TestResult = {
|
type TestResult = {
|
||||||
status: TestStatus
|
status: TestStatus
|
||||||
responseTime?: number
|
responseTime?: number
|
||||||
|
completedAt?: number
|
||||||
error?: string
|
error?: string
|
||||||
errorCode?: 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 }> = [
|
const endpointTypeOptions: Array<{ value: string; label: string }> = [
|
||||||
{ value: 'auto', label: 'Auto detect (default)' },
|
{ value: 'auto', label: 'Auto detect (default)' },
|
||||||
{ value: 'openai', label: 'OpenAI (/v1/chat/completions)' },
|
{ value: 'openai', label: 'OpenAI (/v1/chat/completions)' },
|
||||||
@@ -204,6 +259,8 @@ export function ChannelTestDialog({
|
|||||||
}: ChannelTestDialogProps) {
|
}: ChannelTestDialogProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { currentRow } = useChannels()
|
const { currentRow } = useChannels()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const currentChannelId = currentRow?.id
|
||||||
const [endpointType, setEndpointType] = useState('auto')
|
const [endpointType, setEndpointType] = useState('auto')
|
||||||
const [isStreamTest, setIsStreamTest] = useState(false)
|
const [isStreamTest, setIsStreamTest] = useState(false)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
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(
|
const testSingleModel = useCallback(
|
||||||
async (model: string, silent = false): Promise<TestResult | undefined> => {
|
async (
|
||||||
|
model: string,
|
||||||
|
silent = false,
|
||||||
|
refreshList = true
|
||||||
|
): Promise<TestResult | undefined> => {
|
||||||
if (!currentRow) return
|
if (!currentRow) return
|
||||||
|
|
||||||
markModelTesting(model, true)
|
markModelTesting(model, true)
|
||||||
@@ -319,9 +428,11 @@ export function ChannelTestDialog({
|
|||||||
silent,
|
silent,
|
||||||
},
|
},
|
||||||
(success, responseTime, error, errorCode) => {
|
(success, responseTime, error, errorCode) => {
|
||||||
|
const completedAt = Date.now()
|
||||||
finalResult = {
|
finalResult = {
|
||||||
status: success ? 'success' : 'error',
|
status: success ? 'success' : 'error',
|
||||||
responseTime,
|
responseTime,
|
||||||
|
completedAt,
|
||||||
error,
|
error,
|
||||||
errorCode,
|
errorCode,
|
||||||
}
|
}
|
||||||
@@ -331,11 +442,20 @@ export function ChannelTestDialog({
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
finalResult = {
|
finalResult = {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
|
completedAt: Date.now(),
|
||||||
error: error instanceof Error ? error.message : t('Test failed'),
|
error: error instanceof Error ? error.message : t('Test failed'),
|
||||||
}
|
}
|
||||||
updateTestResult(model, finalResult)
|
updateTestResult(model, finalResult)
|
||||||
} finally {
|
} finally {
|
||||||
markModelTesting(model, false)
|
markModelTesting(model, false)
|
||||||
|
if (refreshList) {
|
||||||
|
refreshChannelLists(
|
||||||
|
createChannelTestCachePatch(
|
||||||
|
finalResult?.responseTime,
|
||||||
|
finalResult?.completedAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return finalResult
|
return finalResult
|
||||||
},
|
},
|
||||||
@@ -344,6 +464,7 @@ export function ChannelTestDialog({
|
|||||||
endpointType,
|
endpointType,
|
||||||
isStreamTest,
|
isStreamTest,
|
||||||
markModelTesting,
|
markModelTesting,
|
||||||
|
refreshChannelLists,
|
||||||
t,
|
t,
|
||||||
updateTestResult,
|
updateTestResult,
|
||||||
]
|
]
|
||||||
@@ -354,15 +475,19 @@ export function ChannelTestDialog({
|
|||||||
if (!modelsToTest.length) return
|
if (!modelsToTest.length) return
|
||||||
|
|
||||||
setIsBatchTesting(true)
|
setIsBatchTesting(true)
|
||||||
|
let resultPatch: ChannelTestCachePatch | undefined
|
||||||
try {
|
try {
|
||||||
const settled = await Promise.allSettled(
|
const settled = await Promise.allSettled(
|
||||||
modelsToTest.map((modelName) => testSingleModel(modelName, true))
|
modelsToTest.map((modelName) =>
|
||||||
|
testSingleModel(modelName, true, false)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
const results = settled
|
const results = settled
|
||||||
.map((result) =>
|
.map((result) =>
|
||||||
result.status === 'fulfilled' ? result.value : undefined
|
result.status === 'fulfilled' ? result.value : undefined
|
||||||
)
|
)
|
||||||
.filter((result): result is TestResult => Boolean(result))
|
.filter((result): result is TestResult => Boolean(result))
|
||||||
|
resultPatch = getLatestChannelTestCachePatch(results)
|
||||||
const successCount = results.filter(
|
const successCount = results.filter(
|
||||||
(result) => result.status === 'success'
|
(result) => result.status === 'success'
|
||||||
).length
|
).length
|
||||||
@@ -387,9 +512,10 @@ export function ChannelTestDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsBatchTesting(false)
|
setIsBatchTesting(false)
|
||||||
setRowSelection({})
|
setRowSelection({})
|
||||||
|
refreshChannelLists(resultPatch)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[t, testSingleModel]
|
[refreshChannelLists, t, testSingleModel]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@@ -482,7 +608,7 @@ export function ChannelTestDialog({
|
|||||||
disabled={isTestingModel || isBatchTesting}
|
disabled={isTestingModel || isBatchTesting}
|
||||||
>
|
>
|
||||||
{isTestingModel && (
|
{isTestingModel && (
|
||||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
<Loader2 className='animate-spin' data-icon='inline-start' />
|
||||||
)}
|
)}
|
||||||
{t('Test')}
|
{t('Test')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -685,7 +811,7 @@ function TestStatusCell({
|
|||||||
if (result.status === 'testing') {
|
if (result.status === 'testing') {
|
||||||
return (
|
return (
|
||||||
<div className='text-muted-foreground flex min-w-0 items-center gap-2 text-sm'>
|
<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>
|
<span className='truncate'>{t('Testing...')}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -878,7 +1004,7 @@ function TestModelsBulkActions({
|
|||||||
>
|
>
|
||||||
{disabled ? (
|
{disabled ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
<Loader2 className='animate-spin' data-icon='inline-start' />
|
||||||
{t('Testing...')}
|
{t('Testing...')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
+28
-3
@@ -37,7 +37,7 @@ import {
|
|||||||
updateChannelBalance,
|
updateChannelBalance,
|
||||||
} from '../api'
|
} from '../api'
|
||||||
import { CHANNEL_STATUS, ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants'
|
import { CHANNEL_STATUS, ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants'
|
||||||
import type { CopyChannelParams } from '../types'
|
import type { ChannelTestResponse, CopyChannelParams } from '../types'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Query Keys
|
// Query Keys
|
||||||
@@ -52,6 +52,25 @@ export const channelsQueryKeys = {
|
|||||||
detail: (id: number) => [...channelsQueryKeys.details(), id] as const,
|
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
|
// Single Channel Actions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -237,16 +256,22 @@ export async function handleTestChannel(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await testChannel(id, payload)
|
const response = await testChannel(id, payload)
|
||||||
|
const responseTime = getChannelTestResponseTime(response)
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
if (!options?.silent) {
|
if (!options?.silent) {
|
||||||
toast.success(i18next.t(SUCCESS_MESSAGES.TESTED))
|
toast.success(i18next.t(SUCCESS_MESSAGES.TESTED))
|
||||||
}
|
}
|
||||||
onTestComplete?.(true, response.data?.response_time)
|
onTestComplete?.(true, responseTime)
|
||||||
} else {
|
} else {
|
||||||
if (!options?.silent) {
|
if (!options?.silent) {
|
||||||
toast.error(response.message || i18next.t(ERROR_MESSAGES.TEST_FAILED))
|
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) {
|
} catch (_error: unknown) {
|
||||||
const err = _error as { response?: { data?: { message?: string } } }
|
const err = _error as { response?: { data?: { message?: string } } }
|
||||||
|
|||||||
+1
@@ -143,6 +143,7 @@ export interface ChannelTestResponse {
|
|||||||
success: boolean
|
success: boolean
|
||||||
message?: string
|
message?: string
|
||||||
error_code?: string
|
error_code?: string
|
||||||
|
time?: number
|
||||||
data?: {
|
data?: {
|
||||||
response_time?: number
|
response_time?: number
|
||||||
error?: string
|
error?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user