Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
be60e25a
authored
Jun 15, 2026
by
yyhhyyyyyy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(channels): refresh channel test dialog status
parent
aeea3fae
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
177 additions
and
12 deletions
+177
-12
web/default/src/components/data-table/core/data-table-row.tsx
+14
-2
web/default/src/components/data-table/core/data-table-view.tsx
+1
-0
web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx
+133
-7
web/default/src/features/channels/lib/channel-actions.ts
+28
-3
web/default/src/features/channels/types.ts
+1
-0
No files found.
web/default/src/components/data-table/core/data-table-row.tsx
View file @
be60e25a
...
...
@@ -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
...
...
web/default/src/components/data-table/core/data-table-view.tsx
View file @
be60e25a
...
...
@@ -320,6 +320,7 @@ function renderDefaultRow<TData>(
row=
{
row
}
className=
{
cn
(
props
.
tableBodyRowClassName
,
props
.
getRowClassName
?.(
row
))
}
getColumnClassName=
{
getColumnClassName
}
cellRenderColumns=
{
props
.
table
.
options
.
columns
}
/>
)
}
web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx
View file @
be60e25a
...
...
@@ -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...'
)
}
</>
)
:
(
...
...
web/default/src/features/channels/lib/channel-actions.ts
View file @
be60e25a
...
...
@@ -37,7 +37,7 @@ import {
updateChannelBalance
,
}
from
'../api'
import
{
CHANNEL_STATUS
,
ERROR_MESSAGES
,
SUCCESS_MESSAGES
}
from
'../constants'
import
type
{
CopyChannelParams
}
from
'../types'
import
type
{
C
hannelTestResponse
,
C
opyChannelParams
}
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_t
ime
)
onTestComplete
?.(
true
,
response
T
ime
)
}
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
}
}
}
...
...
web/default/src/features/channels/types.ts
View file @
be60e25a
...
...
@@ -143,6 +143,7 @@ export interface ChannelTestResponse {
success
:
boolean
message
?:
string
error_code
?:
string
time
?:
number
data
?:
{
response_time
?:
number
error
?:
string
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment