import { useEffect, useRef, useState } from 'react' import { Table, Tag, Space, message, Button, Form, Input, Select, Drawer, Tabs, Empty, Tooltip, Popconfirm, Collapse, InputNumber } from 'antd' import { PlayCircleOutlined, PauseCircleOutlined, PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, CheckCircleOutlined, CloseCircleOutlined, ExperimentOutlined, SyncOutlined, ClearOutlined } from '@ant-design/icons' import axios from 'axios' import AppLayout from '../../components/AppLayout/AppLayout' interface BuiltInDataSource { id: number name: string module: string priority: string frequency: string is_active: boolean collector_class: string last_run: string | null is_running: boolean task_id: number | null progress: number | null records_processed: number | null total_records: number | null } interface CustomDataSource { id: number name: string description: string | null source_type: string endpoint: string auth_type: string is_active: boolean created_at: string updated_at: string | null } interface ViewDataSource { id: number name: string description: string | null source_type: string endpoint: string auth_type: string headers: Record config: Record collector_class: string module: string priority: string frequency: string } function DataSources() { const [activeTab, setActiveTab] = useState('builtin') const [builtInSources, setBuiltInSources] = useState([]) const [customSources, setCustomSources] = useState([]) const [loading, setLoading] = useState(false) const [drawerVisible, setDrawerVisible] = useState(false) const [viewDrawerVisible, setViewDrawerVisible] = useState(false) const [editingConfig, setEditingConfig] = useState(null) const [viewingSource, setViewingSource] = useState(null) const [recordCount, setRecordCount] = useState(0) const [testing, setTesting] = useState(false) const [testResult, setTestResult] = useState(null) const builtinTableRegionRef = useRef(null) const customTableRegionRef = useRef(null) const [builtinTableHeight, setBuiltinTableHeight] = useState(360) const [customTableHeight, setCustomTableHeight] = useState(360) const [form] = Form.useForm() const fetchData = async () => { setLoading(true) try { const [builtinRes, customRes] = await Promise.all([ axios.get('/api/v1/datasources'), axios.get('/api/v1/datasources/configs') ]) setBuiltInSources(builtinRes.data.data || []) setCustomSources(customRes.data.data || []) } catch (error) { console.error('Failed to fetch data:', error) } finally { setLoading(false) } } const [taskProgress, setTaskProgress] = useState>({}) useEffect(() => { fetchData() }, []) useEffect(() => { const updateHeights = () => { const builtinRegionHeight = builtinTableRegionRef.current?.offsetHeight || 0 const customRegionHeight = customTableRegionRef.current?.offsetHeight || 0 setBuiltinTableHeight(Math.max(220, builtinRegionHeight - 56)) setCustomTableHeight(Math.max(220, customRegionHeight - 56)) } updateHeights() if (typeof ResizeObserver === 'undefined') { return undefined } const observer = new ResizeObserver(updateHeights) if (builtinTableRegionRef.current) observer.observe(builtinTableRegionRef.current) if (customTableRegionRef.current) observer.observe(customTableRegionRef.current) return () => observer.disconnect() }, [activeTab, builtInSources.length, customSources.length]) useEffect(() => { const runningSources = builtInSources.filter(s => s.is_running) if (runningSources.length === 0) return const interval = setInterval(async () => { const progressMap: Record = {} await Promise.all( runningSources.map(async (source) => { try { const res = await axios.get(`/api/v1/datasources/${source.id}/task-status`) progressMap[source.id] = { progress: res.data.progress || 0, is_running: res.data.is_running } } catch { progressMap[source.id] = { progress: 0, is_running: false } } }) ) setTaskProgress(prev => ({ ...prev, ...progressMap })) }, 2000) return () => clearInterval(interval) }, [builtInSources.map(s => s.id).join(',')]) const handleTrigger = async (id: number) => { try { await axios.post(`/api/v1/datasources/${id}/trigger`) message.success('任务已触发') // Trigger polling immediately setTaskProgress(prev => ({ ...prev, [id]: { progress: 0, is_running: true } })) // Also refresh data fetchData() // Also fetch the running task status pollTaskStatus(id) } catch (error: unknown) { const err = error as { response?: { data?: { detail?: string } } } message.error(err.response?.data?.detail || '触发失败') } } const pollTaskStatus = async (sourceId: number) => { const poll = async () => { try { const res = await axios.get(`/api/v1/datasources/${sourceId}/task-status`) const data = res.data setTaskProgress(prev => ({ ...prev, [sourceId]: { progress: data.progress || 0, is_running: data.is_running } })) // Keep polling while running if (data.is_running) { setTimeout(poll, 2000) } else { // Task completed - refresh data and clear this source from progress setTimeout(() => { setTaskProgress(prev => { const newState = { ...prev } delete newState[sourceId] return newState }) }, 1000) fetchData() } } catch { // Stop polling on error } } poll() } const handleToggle = async (id: number, current: boolean) => { const endpoint = current ? 'disable' : 'enable' try { await axios.post(`/api/v1/datasources/${id}/${endpoint}`) message.success(`${current ? '已禁用' : '已启用'}`) fetchData() } catch (error: unknown) { const err = error as { response?: { data?: { detail?: string } } } message.error(err.response?.data?.detail || '操作失败') } } const handleClearDataFromDrawer = async () => { if (!viewingSource) return try { const res = await axios.delete(`/api/v1/datasources/${viewingSource.id}/data`) message.success(res.data.message || '数据已删除') setViewDrawerVisible(false) fetchData() } catch (error: unknown) { const err = error as { response?: { data?: { detail?: string } } } message.error(err.response?.data?.detail || '删除数据失败') } } const handleViewSource = async (source: BuiltInDataSource) => { try { const [res, statsRes] = await Promise.all([ axios.get(`/api/v1/datasources/${source.id}`), axios.get(`/api/v1/datasources/${source.id}/stats`) ]) const data = res.data setViewingSource({ id: data.id, name: data.name, description: null, source_type: data.collector_class, endpoint: '', auth_type: 'none', headers: {}, config: {}, collector_class: data.collector_class, module: data.module, priority: data.priority, frequency: data.frequency, }) setRecordCount(statsRes.data.total_records || 0) setViewDrawerVisible(true) } catch (error) { message.error('获取数据源信息失败') } } const handleUpdateSource = async () => { if (!viewingSource) return try { await axios.post(`/api/v1/datasources/${viewingSource.id}/trigger`) message.success('已触发更新') setViewDrawerVisible(false) } catch (error: unknown) { const err = error as { response?: { data?: { detail?: string } } } message.error(err.response?.data?.detail || '更新失败') } } const handleTest = async () => { try { const values = await form.validateFields() setTesting(true) setTestResult(null) const res = await axios.post('/api/v1/datasources/configs/test', values) setTestResult(res.data) if (res.data.success) { message.success('连接测试成功') } else { message.error('连接测试失败') } } catch (error: unknown) { const err = error as { response?: { data?: { detail?: string; message?: string } } } message.error(err.response?.data?.message || err.response?.data?.detail || '测试失败') } finally { setTesting(false) } } const handleSave = async () => { try { const values = await form.validateFields() if (editingConfig) { await axios.put(`/api/v1/datasources/configs/${editingConfig.id}`, values) message.success('配置已更新') } else { await axios.post('/api/v1/datasources/configs', values) message.success('配置已创建') } setDrawerVisible(false) form.resetFields() setEditingConfig(null) setTestResult(null) fetchData() } catch (error: unknown) { const err = error as { response?: { data?: { detail?: string; message?: string } } } message.error(err.response?.data?.message || err.response?.data?.detail || '保存失败') } } const handleDelete = async (id: number) => { try { await axios.delete(`/api/v1/datasources/configs/${id}`) message.success('配置已删除') fetchData() } catch (error: unknown) { const err = error as { response?: { data?: { detail?: string } } } message.error(err.response?.data?.detail || '删除失败') } } const handleToggleCustom = async (id: number, current: boolean) => { try { await axios.put(`/api/v1/datasources/configs/${id}`, { is_active: !current }) message.success(`${current ? '已禁用' : '已启用'}`) fetchData() } catch (error: unknown) { const err = error as { response?: { data?: { detail?: string } } } message.error(err.response?.data?.detail || '操作失败') } } const openDrawer = (config?: CustomDataSource) => { 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) } const builtinColumns = [ { title: 'ID', dataIndex: 'id', key: 'id', width: 60, fixed: 'left' as const }, { title: '名称', dataIndex: 'name', key: 'name', width: 180, ellipsis: true, render: (name: string, record: BuiltInDataSource) => ( ), }, { title: '模块', dataIndex: 'module', key: 'module', width: 80 }, { title: '优先级', dataIndex: 'priority', key: 'priority', width: 80, render: (p: string) => {p}, }, { title: '频率', dataIndex: 'frequency', key: 'frequency', width: 80 }, { title: '最近采集', dataIndex: 'last_run', key: 'last_run', width: 140, render: (lastRun: string | null) => lastRun || '-', }, { title: '状态', dataIndex: 'is_active', key: 'is_active', width: 100, render: (_: unknown, record: BuiltInDataSource) => { const progress = taskProgress[record.id] if (progress?.is_running || record.is_running) { const pct = progress?.progress ?? record.progress ?? 0 return ( 采集中 {Math.round(pct)}% ) } return {record.is_active ? '运行中' : '已暂停'} }, }, { title: '操作', key: 'action', width: 200, fixed: 'right' as const, render: (_: unknown, record: BuiltInDataSource) => ( ), }, ] const customColumns = [ { title: 'ID', dataIndex: 'id', key: 'id', width: 60, fixed: 'left' as const }, { title: '名称', dataIndex: 'name', key: 'name', width: 150, ellipsis: true }, { title: '类型', dataIndex: 'source_type', key: 'source_type', width: 100 }, { title: '状态', dataIndex: 'is_active', key: 'is_active', width: 80, render: (active: boolean) => ( {active ? '启用' : '禁用'} ), }, { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 160 }, { title: '操作', key: 'action', width: 150, fixed: 'right' as const, render: (_: unknown, record: CustomDataSource) => (