@@ -19,6 +19,7 @@ import { useWebSocket } from '../../hooks/useWebSocket'
interface BuiltInDataSource {
id : number
source : string
name : string
module : string
priority : string
@@ -180,6 +181,19 @@ interface CustomDataSource {
updated_at : string | null
}
interface EditableDataSourceConfig {
id : number
name : string
description : string | null
source_type : string
endpoint : string
auth_type : string
auth_config : Record < string , any >
headers : Record < string , string >
config : Record < string , any >
is_active? : boolean
}
interface ViewDataSource {
id : number
name : string
@@ -205,6 +219,7 @@ function DataSources() {
const [ drawerVisible , setDrawerVisible ] = useState ( false )
const [ viewDrawerVisible , setViewDrawerVisible ] = useState ( false )
const [ editingConfig , setEditingConfig ] = useState < CustomDataSource | null > ( null )
const [ builtinEditingSource , setBuiltinEditingSource ] = useState < BuiltInDataSource | null > ( null )
const [ viewingSource , setViewingSource ] = useState < ViewDataSource | null > ( null )
const [ recordCount , setRecordCount ] = useState < number > ( 0 )
const [ testing , setTesting ] = useState ( false )
@@ -219,6 +234,81 @@ function DataSources() {
const [ customActionsCollapsed , customContainerRef ] = useCollapsedActions ( )
const [ form ] = Form . useForm ( )
const headersMapToList = useCallback ( ( headers? : Record < string , string > | null ) = > {
return Object . entries ( headers || { } )
. filter ( ( [ key , value ] ) = > key && value !== undefined && value !== null && String ( value ) . trim ( ) !== '' )
. map ( ( [ key , value ] ) = > ( { key , value } ) )
} , [ ] )
const headersListToMap = useCallback ( ( headers? : Array < { key? : string ; value? : string } > | Record < string , string > ) = > {
if ( ! headers ) return { }
if ( ! Array . isArray ( headers ) ) return headers
return headers . reduce < Record < string , string > > ( ( acc , item ) = > {
const key = item ? . key ? . trim ( )
const value = item ? . value ? . trim ( )
if ( ! key || value === undefined ) return acc
acc [ key ] = value
return acc
} , { } )
} , [ ] )
const applyConfigToForm = useCallback ( ( config? : Partial < EditableDataSourceConfig > | null ) = > {
form . setFieldsValue ( {
name : config?.name || '' ,
description : config?.description || '' ,
source_type : config?.source_type || 'http' ,
endpoint : config?.endpoint || '' ,
auth_type : config?.auth_type || 'none' ,
auth_config : config?.auth_config || { } ,
headers : headersMapToList ( config ? . headers || { } ) ,
config : config?.config || { timeout : 30 , retry : 3 } ,
} )
} , [ form , headersMapToList ] )
const loadConfigDetail = useCallback ( async ( configId : number ) = > {
const res = await axios . get < EditableDataSourceConfig > ( ` /api/v1/datasources/configs/ ${ configId } ` )
return res . data
} , [ ] )
const createDefaultConfigDraft = useCallback ( ( overrides? : Partial < EditableDataSourceConfig > ) = > ( {
source_type : 'http' ,
auth_type : 'none' ,
headers : { } ,
config : { timeout : 30 , retry : 3 } ,
. . . overrides ,
} ) , [ ] )
const getBuiltinOverrideDescription = useCallback (
( source? : Pick < BuiltInDataSource , 'name' > | null ) = >
source ? ` Built-in datasource override for ${ source . name } ` : undefined ,
[ ] ,
)
const createFormPayload = useCallback ( ( values : any ) = > ( {
. . . values ,
name : builtinEditingSource ? builtinEditingSource.source : values.name ,
description :
values.description ||
getBuiltinOverrideDescription ( builtinEditingSource ) ,
source_type : builtinEditingSource ? 'http' : values . source_type ,
headers : headersListToMap ( values . headers ) ,
} ) , [ builtinEditingSource , getBuiltinOverrideDescription , headersListToMap ] )
const closeDrawerAfterLoadError = useCallback ( (
errorMessage : string ,
options ? : { clearBuiltin? : boolean ; clearEditingConfig? : boolean } ,
) = > {
messageApi . error ( errorMessage )
setDrawerVisible ( false )
if ( options ? . clearBuiltin ) {
setBuiltinEditingSource ( null )
}
if ( options ? . clearEditingConfig ) {
setEditingConfig ( null )
}
} , [ messageApi ] )
const fetchData = useCallback ( async ( ) = > {
setLoading ( true )
try {
@@ -711,9 +801,11 @@ function DataSources() {
const handleViewSource = async ( source : BuiltInDataSource ) = > {
try {
const [ res , statsRes ] = await Promise . all ( [
const existingOverride = customSources . find ( ( item ) = > item . name === source . source )
const [ res , statsRes , overrideDetail ] = await Promise . all ( [
axios . get ( ` /api/v1/datasources/ ${ source . id } ` ) ,
axios . get ( ` /api/v1/datasources/ ${ source . id } /stats ` )
axios . get ( ` /api/v1/datasources/ ${ source . id } /stats ` ) ,
existingOverride ? loadConfigDetail ( existingOverride . id ) : Promise . resolve ( null ) ,
] )
const data = res . data
setViewingSource ( {
@@ -721,10 +813,10 @@ function DataSources() {
name : data.name ,
description : null ,
source_type : data.collector_class ,
endpoint : data. endpoint || '' ,
auth_type : 'none' ,
headers : { } ,
config : { } ,
endpoint : overrideDetail?.endpoint || data . endpoint || '' ,
auth_type : overrideDetail?.auth_type || 'none' ,
headers : overrideDetail?.headers || { } ,
config : overrideDetail?.config || { } ,
collector_class : data.collector_class ,
module : data.module ,
priority : data.priority ,
@@ -753,7 +845,8 @@ function DataSources() {
const values = await form . validateFields ( )
setTesting ( true )
setTestResult ( null )
const res = await axios . post ( '/api/v1/datasources/configs/test' , values )
const payload = createFormPayload ( values )
const res = await axios . post ( '/api/v1/datasources/configs/test' , payload )
setTestResult ( res . data )
if ( res . data . success ) {
messageApi . success ( '连接测试成功' )
@@ -771,16 +864,18 @@ function DataSources() {
const handleSave = async ( ) = > {
try {
const values = await form . validateFields ( )
const payload = createFormPayload ( values )
if ( editingConfig ) {
await axios . put ( ` /api/v1/datasources/configs/ ${ editingConfig . id } ` , values )
await axios . put ( ` /api/v1/datasources/configs/ ${ editingConfig . id } ` , payload )
messageApi . success ( '配置已更新' )
} else {
await axios . post ( '/api/v1/datasources/configs' , values )
await axios . post ( '/api/v1/datasources/configs' , payload )
messageApi . success ( '配置已创建' )
}
setDrawerVisible ( false )
form . resetFields ( )
setEditingConfig ( null )
setBuiltinEditingSource ( null )
setTestResult ( null )
fetchData ( )
} catch ( error : unknown ) {
@@ -800,6 +895,23 @@ function DataSources() {
}
}
const handleResetBuiltinOverride = async ( ) = > {
if ( ! builtinEditingSource || ! editingConfig ) return
try {
await axios . delete ( ` /api/v1/datasources/configs/ ${ editingConfig . id } ` )
messageApi . success ( ` 已恢复 ${ builtinEditingSource . name } 的默认配置 ` )
setDrawerVisible ( false )
form . resetFields ( )
setEditingConfig ( null )
setBuiltinEditingSource ( null )
setTestResult ( null )
fetchData ( )
} catch ( error : unknown ) {
const err = error as { response ? : { data ? : { detail? : string } } }
messageApi . error ( err . response ? . data ? . detail || '恢复默认失败' )
}
}
const handleToggleCustom = async ( id : number , current : boolean ) = > {
try {
await axios . put ( ` /api/v1/datasources/configs/ ${ id } ` , { is_active : ! current } )
@@ -811,24 +923,53 @@ function DataSources() {
}
}
const openDrawer = ( config? : CustomDataSource ) = > {
const openDrawer = async ( config? : CustomDataSource ) = > {
setBuiltinEditingSource ( null )
setEditingConfig ( config || null )
if ( config ) {
form . setFieldsValue ( {
. . . config ,
auth_config : { } ,
} )
} else {
form . resetFields ( )
form . setFieldsValue ( {
source_type : 'http' ,
auth_type : 'none' ,
config : { timeout : 30 , retry : 3 } ,
headers : { } ,
} )
}
setDrawerVisible ( true )
setTestResult ( null )
setDrawerVisible ( true )
if ( config ) {
try {
const detail = await loadConfigDetail ( config . id )
applyConfigToForm ( detail )
} catch {
closeDrawerAfterLoadError ( '获取配置详情失败' , { clearEditingConfig : true } )
}
return
}
form . resetFields ( )
applyConfigToForm ( createDefaultConfigDraft ( ) )
}
const openBuiltinConfigDrawer = async ( source : BuiltInDataSource ) = > {
setBuiltinEditingSource ( source )
setTestResult ( null )
setDrawerVisible ( true )
const existingOverride = customSources . find ( ( item ) = > item . name === source . source )
setEditingConfig ( existingOverride || null )
if ( existingOverride ) {
try {
const detail = await loadConfigDetail ( existingOverride . id )
applyConfigToForm ( detail )
} catch {
closeDrawerAfterLoadError ( '获取内置数据源配置失败' , {
clearBuiltin : true ,
clearEditingConfig : true ,
} )
}
return
}
form . resetFields ( )
applyConfigToForm ( createDefaultConfigDraft ( {
name : source.source ,
description : getBuiltinOverrideDescription ( source ) ,
endpoint : source.endpoint || '' ,
} ) )
}
const handleCopyLink = async ( value : string , successText : string ) = > {
@@ -945,12 +1086,18 @@ function DataSources() {
title : '操作' ,
key : 'action' ,
fixed : 'right' as const ,
width : builtinActionsCollapsed ? 40 : 164 ,
width : builtinActionsCollapsed ? 40 : 228 ,
onCell : ( ) = > actionCellProps ,
render : ( _ : unknown , record : BuiltInDataSource ) = > (
< TableActions
collapsed = { builtinActionsCollapsed }
items = { [
{
key : 'edit' ,
label : '编辑' ,
icon : < EditOutlined / > ,
onClick : ( ) = > { void openBuiltinConfigDrawer ( record ) } ,
} ,
{
key : 'trigger' ,
label : '触发' ,
@@ -967,6 +1114,14 @@ function DataSources() {
} ,
] }
>
< Button
type = "link"
size = "small"
icon = { < EditOutlined / > }
onClick = { ( ) = > { void openBuiltinConfigDrawer ( record ) } }
>
编 辑
< / Button >
< Button
type = "link"
size = "small"
@@ -1035,7 +1190,7 @@ function DataSources() {
key : 'edit' ,
label : '编辑' ,
icon : < EditOutlined / > ,
onClick : ( ) = > openDrawer ( record ) ,
onClick : ( ) = > { void openDrawer ( record ) } ,
} ,
{
key : 'toggle' ,
@@ -1059,7 +1214,7 @@ function DataSources() {
} ,
] }
>
< Button type = "link" size = "small" icon = { < EditOutlined / > } onClick = { ( ) = > openDrawer ( record ) } > 编 辑 < / Button >
< Button type = "link" size = "small" icon = { < EditOutlined / > } onClick = { ( ) = > { void openDrawer ( record ) } } > 编 辑 < / Button >
< Button
type = "link"
size = "small"
@@ -1171,7 +1326,7 @@ function DataSources() {
children : (
< div className = "page-shell__body data-source-custom-tab" ref = { customContainerRef } >
< div className = "data-source-custom-toolbar" >
< Button type = "primary" icon = { < PlusOutlined / > } onClick = { ( ) = > openDrawer ( ) } >
< Button type = "primary" icon = { < PlusOutlined / > } onClick = { ( ) = > { void openDrawer ( ) } } >
添 加 数 据 源
< / Button >
< / div >
@@ -1215,24 +1370,40 @@ function DataSources() {
< / div >
< Drawer
title = { editingConfig ? '编辑数据源' : '添加数据源' }
title = { builtinEditingSource ? ` 编辑内置数据源配置 · ${ builtinEditingSource . name } ` : editingConfig ? '编辑数据源' : '添加数据源' }
width = { 600 }
open = { drawerVisible }
onClose = { ( ) = > {
setDrawerVisible ( false )
form . resetFields ( )
setEditingConfig ( null )
setBuiltinEditingSource ( null )
setTestResult ( null )
} }
footer = {
< div style = { { display : 'flex' , justifyContent : 'space-between' } } >
< Button
icon = { < ExperimentOutlined / > }
loading = { testing }
onClick = { handleTest }
>
测 试 连 接
< / Button >
< Space >
{ builtinEditingSource && editingConfig ? (
< Popconfirm
title = "恢复内置默认配置?"
description = "这会删除当前 override, 并重新使用代码内置默认配置。"
okText = "恢复默认"
cancelText = "取消"
onConfirm = { handleResetBuiltinOverride }
>
< Button danger icon = { < ClearOutlined / > } >
恢 复 默 认
< / Button >
< / Popconfirm >
) : null }
< Button
icon = { < ExperimentOutlined / > }
loading = { testing }
onClick = { handleTest }
>
测 试 连 接
< / Button >
< / Space >
< Space >
< Button onClick = { ( ) = > setDrawerVisible ( false ) } > 取 消 < / Button >
< Button type = "primary" onClick = { handleSave } >
@@ -1243,29 +1414,46 @@ function DataSources() {
}
>
< Form form = { form } layout = "vertical" >
< Form.Item
name = "name"
label = "名称"
rules = { [ { required : true , message : '请输入名称' } ] }
>
< Input placeholder = "My API Data Source" / >
< / Form.Item >
{ builtinEditingSource ? (
< Card size = "small" bordered = { false } style = { { marginBottom : 16 , background : '#fafafa' } } >
< Row gutter = { [ 12 , 12 ] } >
< Col span = { 12 } >
< div style = { { marginBottom : 4 , color : '#8c8c8c' , fontSize : 12 } } > 内 置 数 据 源 < / div >
< Input value = { builtinEditingSource . name } disabled / >
< / Col >
< Col span = { 12 } >
< div style = { { marginBottom : 4 , color : '#8c8c8c' , fontSize : 12 } } > Collector Key < / div >
< Input value = { builtinEditingSource . source } disabled / >
< / Col >
< / Row >
< / Card >
) : (
< Form.Item
name = "name"
label = "名称"
rules = { [ { required : true , message : '请输入名称' } ] }
>
< Input placeholder = "My API Data Source" / >
< / Form.Item >
) }
< Form.Item name = "description" label = "描述" >
< Input.TextArea rows = { 2 } placeholder = "数据源描述" / >
< / Form.Item >
< Form.Item
name = "source_type"
label = "数据源类型 "
rules = { [ { required : true , message : '请选择类型' } ] }
>
< Select >
< Select.Option value = "http" > HTTP API < / Select.Option >
< Select.Option value = "api " > REST API < / Select.Option >
< Select.Option value = "database" > 数 据 库 < / Select.Option >
< / Select >
< / Form.Item >
{ builtinEditingSource ? null : (
< Form.Item
name = "source_type "
label = "数据源类型"
rules = { [ { required : true , message : '请选择类型' } ] }
>
< Select >
< Select.Option value = "http " > HTTP API < / Select.Option >
< Select.Option value = "api" > REST API < / Select.Option >
< Select.Option value = "database" > 数 据 库 < / Select.Option >
< / Select >
< / Form.Item >
) }
< Form.Item
name = "endpoint"
@@ -1276,6 +1464,7 @@ function DataSources() {
< / Form.Item >
< Collapse
className = "data-source-drawer-collapse"
items = { [
{
key : 'auth' ,
@@ -1311,6 +1500,12 @@ function DataSources() {
< Form.Item name = { [ 'auth_config' , 'key_name' ] } label = "Header名称" initialValue = "X-API-Key" >
< Input placeholder = "X-API-Key" / >
< / Form.Item >
< Form.Item name = { [ 'auth_config' , 'in' ] } label = "传递位置" initialValue = "header" >
< Select >
< Select.Option value = "header" > Header < / Select.Option >
< Select.Option value = "query" > Query Param < / Select.Option >
< / Select >
< / Form.Item >
< Form.Item name = { [ 'auth_config' , 'api_key' ] } label = "API Key" >
< Input.Password placeholder = "API Key" / >
< / Form.Item >
@@ -1345,6 +1540,7 @@ function DataSources() {
/ >
< Collapse
className = "data-source-drawer-collapse"
items = { [
{
key : 'headers' ,
@@ -1376,6 +1572,7 @@ function DataSources() {
/ >
< Collapse
className = "data-source-drawer-collapse"
items = { [
{
key : 'config' ,