perf(channels): streamline channel test dialog layout
- split model test status from result details so failures and latency no longer crowd one column. - move batch progress into toast updates to keep the dialog height stable during tests. - consolidate the channel title and model actions to reduce vertical churn.
This commit is contained in:
+176
-139
@@ -17,10 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
type ColumnDef,
|
||||
type RowSelectionState,
|
||||
type Table as TanStackTable,
|
||||
import type {
|
||||
ColumnDef,
|
||||
RowSelectionState,
|
||||
Table as TanStackTable,
|
||||
} from '@tanstack/react-table'
|
||||
import {
|
||||
Check,
|
||||
@@ -31,7 +31,14 @@ import {
|
||||
Settings,
|
||||
Trash2,
|
||||
} from 'lucide-react'
|
||||
import { type ChangeEvent, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
type ChangeEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
@@ -54,7 +61,6 @@ import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -224,7 +230,7 @@ function sleep(ms: number) {
|
||||
}
|
||||
|
||||
function normalizeInlineError(errorText: string) {
|
||||
return errorText.replace(/\s+/g, ' ').trim()
|
||||
return errorText.replaceAll(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
function getFirstErrorLine(errorText: string) {
|
||||
@@ -281,9 +287,11 @@ function getTestTableColumnClass(columnId: string) {
|
||||
case 'select':
|
||||
return 'w-10 min-w-10'
|
||||
case 'model':
|
||||
return 'w-auto whitespace-nowrap'
|
||||
return 'w-auto min-w-48 whitespace-nowrap'
|
||||
case 'status':
|
||||
return 'w-70 min-w-70 max-w-70 whitespace-normal'
|
||||
return 'w-28 min-w-28 whitespace-nowrap'
|
||||
case 'result':
|
||||
return 'w-80 min-w-80 max-w-80 whitespace-normal'
|
||||
case 'actions':
|
||||
return 'bg-popover w-24 min-w-24 whitespace-nowrap sm:w-28 sm:min-w-28'
|
||||
default:
|
||||
@@ -320,6 +328,9 @@ function ChannelTestDialogContent({
|
||||
const queryClient = useQueryClient()
|
||||
const currentChannelId = currentRow.id
|
||||
const batchStopRequestedRef = useRef(false)
|
||||
const batchProgressToastIdRef = useRef<ReturnType<
|
||||
typeof toast.loading
|
||||
> | null>(null)
|
||||
const [endpointType, setEndpointType] = useState('auto')
|
||||
const [isStreamTest, setIsStreamTest] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
@@ -352,6 +363,39 @@ function ChannelTestDialogContent({
|
||||
[t]
|
||||
)
|
||||
|
||||
const dismissBatchProgressToast = useCallback(() => {
|
||||
if (batchProgressToastIdRef.current === null) return
|
||||
|
||||
toast.dismiss(batchProgressToastIdRef.current)
|
||||
batchProgressToastIdRef.current = null
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!batchProgress) {
|
||||
dismissBatchProgressToast()
|
||||
return
|
||||
}
|
||||
|
||||
const title = isBatchStopRequested
|
||||
? t('Stopping batch test...')
|
||||
: t('Batch testing models...')
|
||||
const completedText = t('{{completed}}/{{total}} completed', {
|
||||
completed: batchProgress.completed,
|
||||
total: batchProgress.total,
|
||||
})
|
||||
const resultText = t('{{success}} succeeded, {{failed}} failed', {
|
||||
success: batchProgress.success,
|
||||
failed: batchProgress.failed,
|
||||
})
|
||||
|
||||
batchProgressToastIdRef.current = toast.loading(title, {
|
||||
id: batchProgressToastIdRef.current ?? undefined,
|
||||
description: `${completedText} · ${resultText}`,
|
||||
})
|
||||
}, [batchProgress, dismissBatchProgressToast, isBatchStopRequested, t])
|
||||
|
||||
useEffect(() => dismissBatchProgressToast, [dismissBatchProgressToast])
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
batchStopRequestedRef.current = true
|
||||
setEndpointType('auto')
|
||||
@@ -571,9 +615,9 @@ function ChannelTestDialogContent({
|
||||
|
||||
const handleBatchTest = useCallback(
|
||||
async (modelsToTest: string[]) => {
|
||||
const uniqueModels = Array.from(
|
||||
new Set(modelsToTest.map((model) => model.trim()).filter(Boolean))
|
||||
)
|
||||
const uniqueModels = [
|
||||
...new Set(modelsToTest.map((model) => model.trim()).filter(Boolean)),
|
||||
]
|
||||
if (!uniqueModels.length) return
|
||||
|
||||
batchStopRequestedRef.current = false
|
||||
@@ -661,6 +705,7 @@ function ChannelTestDialogContent({
|
||||
const stopped =
|
||||
batchStopRequestedRef.current && completedCount < uniqueModels.length
|
||||
|
||||
dismissBatchProgressToast()
|
||||
if (stopped) {
|
||||
toast.info(
|
||||
t(
|
||||
@@ -699,7 +744,13 @@ function ChannelTestDialogContent({
|
||||
refreshChannelLists(resultPatch)
|
||||
}
|
||||
},
|
||||
[refreshChannelLists, t, testSingleModel, updateTestResult]
|
||||
[
|
||||
dismissBatchProgressToast,
|
||||
refreshChannelLists,
|
||||
t,
|
||||
testSingleModel,
|
||||
updateTestResult,
|
||||
]
|
||||
)
|
||||
|
||||
const handleSelectSuccessfulModels = useCallback(() => {
|
||||
@@ -838,11 +889,22 @@ function ChannelTestDialogContent({
|
||||
{
|
||||
id: 'status',
|
||||
header: t('Status'),
|
||||
cell: ({ row }) => {
|
||||
const model = row.original.model
|
||||
const result = testResults[model]
|
||||
return <TestStatusCell result={result} />
|
||||
},
|
||||
enableSorting: false,
|
||||
size: 112,
|
||||
},
|
||||
{
|
||||
id: 'result',
|
||||
header: t('Result'),
|
||||
cell: ({ row }) => {
|
||||
const model = row.original.model
|
||||
const result = testResults[model]
|
||||
return (
|
||||
<TestStatusCell
|
||||
<TestResultCell
|
||||
result={result}
|
||||
model={model}
|
||||
onOpenDetails={setFailureDetails}
|
||||
@@ -850,7 +912,7 @@ function ChannelTestDialogContent({
|
||||
)
|
||||
},
|
||||
enableSorting: false,
|
||||
size: 220,
|
||||
size: 320,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
@@ -905,22 +967,19 @@ function ChannelTestDialogContent({
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={handleDialogOpenChange}
|
||||
title={t('Test Channel Connection')}
|
||||
description={
|
||||
<>
|
||||
{t('Test connectivity for:')}
|
||||
<strong>{currentRow.name}</strong>
|
||||
</>
|
||||
title={
|
||||
<span className='inline-flex max-w-full min-w-0 items-center gap-1.5'>
|
||||
<span className='shrink-0'>{t('Test Channel Connection')}:</span>
|
||||
<span className='min-w-0 truncate'>{currentRow.name}</span>
|
||||
</span>
|
||||
}
|
||||
contentClassName='max-h-[90vh] overflow-hidden sm:max-w-3xl'
|
||||
contentClassName='max-h-[90vh] overflow-hidden sm:max-w-4xl'
|
||||
contentHeight='auto'
|
||||
bodyClassName='space-y-4'
|
||||
footer={
|
||||
<>
|
||||
<Button variant='outline' onClick={handleClose}>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
</>
|
||||
<Button variant='outline' onClick={handleClose}>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className='max-h-[78vh] space-y-4 overflow-y-auto py-4 pr-1'>
|
||||
@@ -983,12 +1042,60 @@ function ChannelTestDialogContent({
|
||||
</div>
|
||||
|
||||
<div className='space-y-3 max-sm:has-[div[role="toolbar"]]:pb-16'>
|
||||
<div className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div>
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
|
||||
<div className='min-w-0 space-y-2'>
|
||||
<p className='text-sm font-medium'>{t('Channel models')}</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('Select models to run batch tests.')}
|
||||
</p>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
{isBatchTesting ? (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleStopBatchTest}
|
||||
disabled={isBatchStopRequested}
|
||||
>
|
||||
{isBatchStopRequested
|
||||
? t('Stopping...')
|
||||
: t('Stop testing')}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={() => handleBatchTest(filteredModels)}
|
||||
disabled={isAnyTesting || filteredModels.length === 0}
|
||||
>
|
||||
{testAllButtonLabel}
|
||||
</Button>
|
||||
{successModels.length > 0 && (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleSelectSuccessfulModels}
|
||||
>
|
||||
<CheckCircle2 data-icon='inline-start' />
|
||||
{t('Select successful models ({{count}})', {
|
||||
count: successModels.length,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
{failedModels.length > 0 && (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => setIsDeleteFailedDialogOpen(true)}
|
||||
>
|
||||
<Trash2 data-icon='inline-start' />
|
||||
{t('Delete failed models ({{count}})', {
|
||||
count: failedModels.length,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2 sm:flex-row sm:items-center'>
|
||||
<Input
|
||||
@@ -997,64 +1104,9 @@ function ChannelTestDialogContent({
|
||||
onChange={handleSearchTermChange}
|
||||
className='sm:w-64'
|
||||
/>
|
||||
{isBatchTesting ? (
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={handleStopBatchTest}
|
||||
disabled={isBatchStopRequested}
|
||||
>
|
||||
{isBatchStopRequested
|
||||
? t('Stopping...')
|
||||
: t('Stop testing')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => handleBatchTest(filteredModels)}
|
||||
disabled={isAnyTesting || filteredModels.length === 0}
|
||||
>
|
||||
{testAllButtonLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{batchProgress && (
|
||||
<BatchProgressSummary
|
||||
progress={batchProgress}
|
||||
isStopping={isBatchStopRequested}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isAnyTesting &&
|
||||
(successModels.length > 0 || failedModels.length > 0) && (
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
{successModels.length > 0 && (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleSelectSuccessfulModels}
|
||||
>
|
||||
<CheckCircle2 data-icon='inline-start' />
|
||||
{t('Select successful models ({{count}})', {
|
||||
count: successModels.length,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
{failedModels.length > 0 && (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => setIsDeleteFailedDialogOpen(true)}
|
||||
>
|
||||
<Trash2 data-icon='inline-start' />
|
||||
{t('Delete failed models ({{count}})', {
|
||||
count: failedModels.length,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='space-y-3'>
|
||||
<DataTableView
|
||||
table={table}
|
||||
@@ -1076,7 +1128,8 @@ function ChannelTestDialogContent({
|
||||
<colgroup>
|
||||
<col className='w-10 min-w-10' />
|
||||
<col className='w-auto' />
|
||||
<col className='w-70' />
|
||||
<col className='w-28' />
|
||||
<col className='w-80' />
|
||||
<col className='w-auto' />
|
||||
</colgroup>
|
||||
}
|
||||
@@ -1123,46 +1176,36 @@ function ChannelTestDialogContent({
|
||||
)
|
||||
}
|
||||
|
||||
function BatchProgressSummary({
|
||||
progress,
|
||||
isStopping,
|
||||
}: {
|
||||
progress: BatchProgress
|
||||
isStopping: boolean
|
||||
}) {
|
||||
function TestStatusCell({ result }: { result?: TestResult }) {
|
||||
const { t } = useTranslation()
|
||||
const progressValue =
|
||||
progress.total > 0
|
||||
? Math.min(100, Math.round((progress.completed / progress.total) * 100))
|
||||
: 0
|
||||
|
||||
return (
|
||||
<div className='bg-muted/30 flex flex-col gap-2 rounded-md border p-3'>
|
||||
<div className='flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<p className='text-sm font-medium'>
|
||||
{isStopping
|
||||
? t('Stopping batch test...')
|
||||
: t('Batch testing models...')}
|
||||
</p>
|
||||
<p className='text-muted-foreground text-xs tabular-nums'>
|
||||
{t('{{completed}}/{{total}} completed', {
|
||||
completed: progress.completed,
|
||||
total: progress.total,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<Progress value={progressValue} />
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('{{success}} succeeded, {{failed}} failed', {
|
||||
success: progress.success,
|
||||
failed: progress.failed,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
if (!result || result.status === 'idle') {
|
||||
return (
|
||||
<StatusBadge label={t('Not tested')} variant='neutral' copyable={false} />
|
||||
)
|
||||
}
|
||||
|
||||
if (result.status === 'testing') {
|
||||
return (
|
||||
<StatusBadge variant='info' copyable={false}>
|
||||
<Loader2 className='size-3.5 shrink-0 animate-spin' />
|
||||
<span className='min-w-0 truncate leading-normal'>
|
||||
{t('Testing...')}
|
||||
</span>
|
||||
</StatusBadge>
|
||||
)
|
||||
}
|
||||
|
||||
if (result.status === 'success') {
|
||||
return (
|
||||
<StatusBadge label={t('Success')} variant='success' copyable={false} />
|
||||
)
|
||||
}
|
||||
|
||||
return <StatusBadge label={t('Failed')} variant='danger' copyable={false} />
|
||||
}
|
||||
|
||||
function TestStatusCell({
|
||||
function TestResultCell({
|
||||
result,
|
||||
model,
|
||||
onOpenDetails,
|
||||
@@ -1174,9 +1217,7 @@ function TestStatusCell({
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!result || result.status === 'idle') {
|
||||
return (
|
||||
<StatusBadge label={t('Not tested')} variant='neutral' copyable={false} />
|
||||
)
|
||||
return <span className='text-muted-foreground text-sm'>-</span>
|
||||
}
|
||||
|
||||
if (result.status === 'testing') {
|
||||
@@ -1189,20 +1230,17 @@ function TestStatusCell({
|
||||
}
|
||||
|
||||
if (result.status === 'success') {
|
||||
return (
|
||||
<div className='flex min-w-0 flex-col gap-1 text-xs'>
|
||||
<StatusBadge label={t('Success')} variant='success' copyable={false} />
|
||||
{typeof result.responseTime === 'number' && (
|
||||
<span className='text-muted-foreground truncate'>
|
||||
{formatResponseTime(result.responseTime, t)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
return typeof result.responseTime === 'number' ? (
|
||||
<span className='text-muted-foreground text-sm'>
|
||||
{formatResponseTime(result.responseTime, t)}
|
||||
</span>
|
||||
) : (
|
||||
<span className='text-muted-foreground text-sm'>-</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FailureStatusContent
|
||||
<FailureResultContent
|
||||
result={result}
|
||||
model={model}
|
||||
onOpenDetails={onOpenDetails}
|
||||
@@ -1210,7 +1248,7 @@ function TestStatusCell({
|
||||
)
|
||||
}
|
||||
|
||||
function FailureStatusContent({
|
||||
function FailureResultContent({
|
||||
result,
|
||||
model,
|
||||
onOpenDetails,
|
||||
@@ -1233,12 +1271,11 @@ function FailureStatusContent({
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-col gap-1.5 text-xs whitespace-normal'>
|
||||
<StatusBadge label={t('Failed')} variant='danger' copyable={false} />
|
||||
<p className='text-muted-foreground line-clamp-2 min-w-0 leading-snug wrap-break-word'>
|
||||
<div className='flex min-w-0 items-center gap-2 text-xs whitespace-normal'>
|
||||
<p className='text-muted-foreground line-clamp-2 min-w-0 flex-1 leading-snug wrap-break-word'>
|
||||
{summary}
|
||||
</p>
|
||||
<div className='flex min-w-0 flex-wrap items-center gap-1.5'>
|
||||
<div className='flex shrink-0 flex-wrap items-center justify-end gap-1.5'>
|
||||
{isModelPriceError && (
|
||||
<Button
|
||||
variant='outline'
|
||||
|
||||
Reference in New Issue
Block a user