first commit

This commit is contained in:
rayd1o
2026-03-05 11:46:58 +08:00
commit e7033775d8
20657 changed files with 1988940 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
import { useEffect, useState } from 'react'
import { Table, Tag, Card, Row, Col, Statistic, Button, Modal, Space, Descriptions } from 'antd'
import { AlertOutlined, InfoCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { useAuthStore } from '../../stores/auth'
interface Alert {
id: number
severity: 'critical' | 'warning' | 'info'
status: 'active' | 'acknowledged' | 'resolved'
datasource_name: string
message: string
created_at: string
acknowledged_at?: string
resolved_at?: string
}
function Alerts() {
const { token } = useAuthStore()
const [alerts, setAlerts] = useState<Alert[]>([])
const [loading, setLoading] = useState(false)
const [selectedAlert, setSelectedAlert] = useState<Alert | null>(null)
const [detailVisible, setDetailVisible] = useState(false)
const fetchAlerts = async () => {
setLoading(true)
try {
const res = await fetch('/api/v1/alerts', {
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
setAlerts(data.data || [])
} catch (error) {
console.error('Failed to fetch alerts:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchAlerts()
}, [token])
const handleAcknowledge = async (alertId: number) => {
try {
await fetch(`/api/v1/alerts/${alertId}/acknowledge`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
})
fetchAlerts()
} catch (error) {
console.error('Failed to acknowledge alert:', error)
}
}
const handleResolve = async (alertId: number) => {
try {
await fetch(`/api/v1/alerts/${alertId}/resolve`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ resolution: '已处理' }),
})
fetchAlerts()
} catch (error) {
console.error('Failed to resolve alert:', error)
}
}
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 60 },
{
title: '级别',
dataIndex: 'severity',
key: 'severity',
render: (s: string) => {
const colors: Record<string, string> = { critical: 'error', warning: 'warning', info: 'blue' }
const icons: Record<string, JSX.Element> = {
critical: <AlertOutlined />,
warning: <AlertOutlined />,
info: <InfoCircleOutlined />,
}
return (
<Tag color={colors[s]} icon={icons[s]}>
{s === 'critical' ? '严重' : s === 'warning' ? '警告' : '信息'}
</Tag>
)
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (s: string) => {
const colors: Record<string, string> = { active: 'red', acknowledged: 'orange', resolved: 'green' }
return (
<Tag color={colors[s]}>
{s === 'active' ? '待处理' : s === 'acknowledged' ? '已确认' : '已解决'}
</Tag>
)
},
},
{ title: '数据源', dataIndex: 'datasource_name', key: 'datasource_name' },
{ title: '消息', dataIndex: 'message', key: 'message', ellipsis: true },
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
render: (t: string) => new Date(t).toLocaleString('zh-CN'),
},
{
title: '操作',
key: 'action',
render: (_: unknown, record: Alert) => (
<Space>
{record.status === 'active' && (
<Button type="link" size="small" onClick={() => handleAcknowledge(record.id)}>
</Button>
)}
{record.status !== 'resolved' && (
<Button type="link" size="small" onClick={() => handleResolve(record.id)}>
</Button>
)}
<Button type="link" size="small" onClick={() => { setSelectedAlert(record); setDetailVisible(true); }}>
</Button>
</Space>
),
},
]
const stats = alerts.reduce(
(acc, alert) => {
if (alert.status === 'active') {
acc[alert.severity]++
}
return acc
},
{ critical: 0, warning: 0, info: 0 } as Record<string, number>
)
return (
<div>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={8}>
<Card>
<Statistic
title="严重告警"
value={stats.critical}
valueStyle={{ color: '#ff4d4f' }}
prefix={<AlertOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="警告"
value={stats.warning}
valueStyle={{ color: '#faad14' }}
prefix={<AlertOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic title="信息" value={stats.info} valueStyle={{ color: '#1890ff' }} prefix={<InfoCircleOutlined />} />
</Card>
</Col>
</Row>
<Card
title="告警列表"
extra={<Button icon={<ReloadOutlined />} onClick={fetchAlerts}></Button>}
>
<Table columns={columns} dataSource={alerts} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} />
</Card>
<Modal
title="告警详情"
open={detailVisible}
onCancel={() => setDetailVisible(false)}
footer={null}
width={600}
>
{selectedAlert && (
<Descriptions column={1} bordered>
<Descriptions.Item label="ID">{selectedAlert.id}</Descriptions.Item>
<Descriptions.Item label="级别">
<Tag color={selectedAlert.severity === 'critical' ? 'error' : selectedAlert.severity === 'warning' ? 'warning' : 'blue'}>
{selectedAlert.severity}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={selectedAlert.status === 'active' ? 'red' : selectedAlert.status === 'acknowledged' ? 'orange' : 'green'}>
{selectedAlert.status}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="数据源">{selectedAlert.datasource_name}</Descriptions.Item>
<Descriptions.Item label="消息">{selectedAlert.message}</Descriptions.Item>
<Descriptions.Item label="创建时间">{new Date(selectedAlert.created_at).toLocaleString('zh-CN')}</Descriptions.Item>
{selectedAlert.acknowledged_at && (
<Descriptions.Item label="确认时间">
{new Date(selectedAlert.acknowledged_at).toLocaleString('zh-CN')}
</Descriptions.Item>
)}
{selectedAlert.resolved_at && (
<Descriptions.Item label="解决时间">
{new Date(selectedAlert.resolved_at).toLocaleString('zh-CN')}
</Descriptions.Item>
)}
</Descriptions>
)}
</Modal>
</div>
)
}
export default Alerts

View File

@@ -0,0 +1,224 @@
import { useEffect, useState } from 'react'
import { Layout, Menu, Card, Row, Col, Statistic, Typography, Button, Tag, Spin } from 'antd'
import {
DashboardOutlined,
DatabaseOutlined,
UserOutlined,
SettingOutlined,
BarChartOutlined,
AlertOutlined,
WifiOutlined,
DisconnectOutlined,
ReloadOutlined,
} from '@ant-design/icons'
import { Link } from 'react-router-dom'
import { useAuthStore } from '../../stores/auth'
const { Header, Sider, Content } = Layout
const { Title, Text } = Typography
interface Stats {
total_datasources: number
active_datasources: number
tasks_today: number
success_rate: number
last_updated: string
alerts: {
critical: number
warning: number
info: number
}
}
function Dashboard() {
const { user, logout, token, clearAuth } = useAuthStore()
const [stats, setStats] = useState<Stats | null>(null)
const [loading, setLoading] = useState(true)
const [wsConnected, setWsConnected] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!token) return
const fetchStats = async () => {
try {
setLoading(true)
const res = await fetch('/api/v1/dashboard/stats', {
headers: { Authorization: `Bearer ${token}` },
})
if (res.status === 401) {
clearAuth()
window.location.href = '/'
return
}
const data = await res.json()
setStats(data)
setError(null)
} catch (err) {
setError('获取数据失败')
console.error(err)
} finally {
setLoading(false)
}
}
fetchStats()
}, [token])
useEffect(() => {
if (!token) return
let ws: WebSocket | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
const connectWs = () => {
try {
ws = new WebSocket(`ws://localhost:8000/ws?token=${token}`)
ws.onopen = () => {
setWsConnected(true)
ws?.send(JSON.stringify({ type: 'subscribe', data: { channels: ['dashboard'] } }))
}
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data)
if (msg.type === 'data_frame' && msg.channel === 'dashboard') {
setStats(msg.payload?.stats as Stats)
}
} catch (e) {
console.error('Parse WS message error:', e)
}
}
ws.onclose = () => {
setWsConnected(false)
reconnectTimer = setTimeout(connectWs, 3000)
}
ws.onerror = () => {
setWsConnected(false)
}
} catch (e) {
console.error('WS connect error:', e)
}
}
connectWs()
return () => {
ws?.close()
if (reconnectTimer) clearTimeout(reconnectTimer)
}
}, [token])
const handleLogout = () => {
logout()
window.location.href = '/'
}
const handleClearAuth = () => {
clearAuth()
window.location.href = '/'
}
const handleRetry = () => {
window.location.reload()
}
const menuItems = [
{ key: '/', icon: <DashboardOutlined />, label: <Link to="/"></Link> },
{ key: '/datasources', icon: <DatabaseOutlined />, label: <Link to="/datasources"></Link> },
{ key: '/data', icon: <BarChartOutlined />, label: <Link to="/data"></Link> },
{ key: '/users', icon: <UserOutlined />, label: <Link to="/users"></Link> },
{ key: '/settings', icon: <SettingOutlined />, label: '系统配置' },
]
if (loading && !stats) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<Spin size="large" tip="加载中..." />
</div>
)
}
return (
<Layout className="dashboard-layout">
<Sider width={240} className="dashboard-sider">
<div style={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Title level={4} style={{ color: 'white', margin: 0 }}></Title>
</div>
<Menu theme="dark" mode="inline" defaultSelectedKeys={['/']} items={menuItems} />
</Sider>
<Layout>
<Header className="dashboard-header" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Text strong>, {user?.username}</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{wsConnected ? (
<Tag icon={<WifiOutlined />} color="success"></Tag>
) : (
<Tag icon={<DisconnectOutlined />} color="default">线</Tag>
)}
<Button type="link" danger onClick={handleLogout}>退</Button>
<Button type="link" onClick={handleClearAuth}></Button>
<Button type="link" icon={<ReloadOutlined />} onClick={handleRetry}></Button>
</div>
</Header>
<Content className="dashboard-content">
{error && (
<Card style={{ marginBottom: 16, borderColor: '#ff4d4f' }}>
<Text style={{ color: '#ff4d4f' }}>{error}</Text>
</Card>
)}
<Row gutter={[16, 16]}>
<Col span={6}>
<Card>
<Statistic title="数据源总数" value={stats?.total_datasources || 0} prefix={<DatabaseOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="活跃数据源" value={stats?.active_datasources || 0} valueStyle={{ color: '#52c41a' }} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="今日任务" value={stats?.tasks_today || 0} prefix={<BarChartOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="成功率" value={stats?.success_rate || 0} suffix="%" valueStyle={{ color: '#1890ff' }} />
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={8}>
<Card>
<Statistic title="严重告警" value={stats?.alerts?.critical || 0} valueStyle={{ color: '#ff4d4f' }} prefix={<AlertOutlined />} />
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic title="警告" value={stats?.alerts?.warning || 0} valueStyle={{ color: '#faad14' }} prefix={<AlertOutlined />} />
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic title="提示" value={stats?.alerts?.info || 0} valueStyle={{ color: '#1890ff' }} prefix={<AlertOutlined />} />
</Card>
</Col>
</Row>
{stats?.last_updated && (
<div style={{ marginTop: 16, textAlign: 'center', color: '#8c8c8c' }}>
: {new Date(stats.last_updated).toLocaleString('zh-CN')}
{wsConnected && <Tag color="green" style={{ marginLeft: 8 }}></Tag>}
</div>
)}
</Content>
</Layout>
</Layout>
)
}
export default Dashboard

View File

@@ -0,0 +1,368 @@
import { useEffect, useState } from 'react'
import {
Table, Tag, Space, Card, Row, Col, Select, Input, Button,
Statistic, Modal, Descriptions, Spin, Empty, Tooltip
} from 'antd'
import {
DatabaseOutlined, GlobalOutlined, CloudServerOutlined,
AppstoreOutlined, EyeOutlined, SearchOutlined
} from '@ant-design/icons'
import axios from 'axios'
interface CollectedData {
id: number
source: string
source_id: string
data_type: string
name: string
title: string | null
description: string | null
country: string | null
city: string | null
latitude: string | null
longitude: string | null
value: string | null
unit: string | null
metadata: Record<string, any> | null
collected_at: string
reference_date: string | null
is_valid: number
}
interface Summary {
total_records: number
by_source: Record<string, Record<string, number>>
source_totals: Array<{ source: string; count: number }>
}
function DataList() {
const [data, setData] = useState<CollectedData[]>([])
const [loading, setLoading] = useState(false)
const [summary, setSummary] = useState<Summary | null>(null)
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [sourceFilter, setSourceFilter] = useState<string | undefined>()
const [typeFilter, setTypeFilter] = useState<string | undefined>()
const [countryFilter, setCountryFilter] = useState<string | undefined>()
const [searchText, setSearchText] = useState('')
const [sources, setSources] = useState<string[]>([])
const [types, setTypes] = useState<string[]>([])
const [countries, setCountries] = useState<string[]>([])
const [detailVisible, setDetailVisible] = useState(false)
const [detailData, setDetailData] = useState<CollectedData | null>(null)
const [detailLoading, setDetailLoading] = useState(false)
const fetchData = async () => {
setLoading(true)
try {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
})
if (sourceFilter) params.append('source', sourceFilter)
if (typeFilter) params.append('data_type', typeFilter)
if (countryFilter) params.append('country', countryFilter)
if (searchText) params.append('search', searchText)
const res = await axios.get(`/api/v1/collected?${params}`)
setData(res.data.data)
setTotal(res.data.total)
} catch (error) {
console.error('Failed to fetch data:', error)
} finally {
setLoading(false)
}
}
const fetchSummary = async () => {
try {
const res = await axios.get('/api/v1/collected/summary')
setSummary(res.data)
} catch (error) {
console.error('Failed to fetch summary:', error)
}
}
const fetchFilters = async () => {
try {
const [sourcesRes, typesRes, countriesRes] = await Promise.all([
axios.get('/api/v1/collected/sources'),
axios.get('/api/v1/collected/types'),
axios.get('/api/v1/collected/countries'),
])
setSources(sourcesRes.data.sources || [])
setTypes(typesRes.data.data_types || [])
setCountries(countriesRes.data.countries || [])
} catch (error) {
console.error('Failed to fetch filters:', error)
}
}
useEffect(() => {
fetchSummary()
fetchFilters()
}, [])
useEffect(() => {
fetchData()
}, [page, pageSize, sourceFilter, typeFilter, countryFilter])
const handleSearch = () => {
setPage(1)
fetchData()
}
const handleViewDetail = async (id: number) => {
setDetailVisible(true)
setDetailLoading(true)
try {
const res = await axios.get(`/api/v1/collected/${id}`)
setDetailData(res.data)
} catch (error) {
console.error('Failed to fetch detail:', error)
} finally {
setDetailLoading(false)
}
}
const getSourceIcon = (source: string) => {
const iconMap: Record<string, React.ReactNode> = {
'top500': <CloudServerOutlined />,
'huggingface_models': <AppstoreOutlined />,
'huggingface_datasets': <DatabaseOutlined />,
'huggingface_spaces': <AppstoreOutlined />,
'telegeography_cables': <GlobalOutlined />,
'epoch_ai_gpu': <CloudServerOutlined />,
}
return iconMap[source] || <DatabaseOutlined />
}
const getTypeColor = (type: string) => {
const colors: Record<string, string> = {
'supercomputer': 'red',
'model': 'blue',
'dataset': 'green',
'space': 'purple',
'submarine_cable': 'cyan',
'gpu_cluster': 'orange',
'ixp': 'magenta',
'network': 'gold',
'facility': 'lime',
}
return colors[type] || 'default'
}
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
render: (name: string, record: CollectedData) => (
<Tooltip title={name}>
<Button type="link" onClick={() => handleViewDetail(record.id)}>
{name}
</Button>
</Tooltip>
),
},
{
title: '数据源',
dataIndex: 'source',
key: 'source',
width: 150,
render: (source: string) => (
<Tag icon={getSourceIcon(source)}>{source}</Tag>
),
},
{
title: '类型',
dataIndex: 'data_type',
key: 'data_type',
width: 120,
render: (type: string) => (
<Tag color={getTypeColor(type)}>{type}</Tag>
),
},
{
title: '国家/地区',
dataIndex: 'country',
key: 'country',
width: 120,
},
{
title: '数值',
dataIndex: 'value',
key: 'value',
width: 120,
render: (value: string | null, record: CollectedData) => (
value ? `${value} ${record.unit || ''}` : '-'
),
},
{
title: '采集时间',
dataIndex: 'collected_at',
key: 'collected_at',
width: 180,
render: (time: string) => new Date(time).toLocaleString('zh-CN'),
},
{
title: '操作',
key: 'action',
width: 80,
render: (_: unknown, record: CollectedData) => (
<Button
type="link"
icon={<EyeOutlined />}
onClick={() => handleViewDetail(record.id)}
>
</Button>
),
},
]
return (
<div>
<h2></h2>
{/* Summary Cards */}
{summary && (
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic
title="总记录数"
value={summary.total_records}
prefix={<DatabaseOutlined />}
/>
</Card>
</Col>
{summary.source_totals.slice(0, 4).map((item) => (
<Col span={6} key={item.source}>
<Card>
<Statistic
title={item.source}
value={item.count}
prefix={getSourceIcon(item.source)}
/>
</Card>
</Col>
))}
</Row>
)}
{/* Filters */}
<Card style={{ marginBottom: 16 }}>
<Space wrap>
<Select
placeholder="数据源"
allowClear
style={{ width: 180 }}
value={sourceFilter}
onChange={(v) => { setSourceFilter(v); setPage(1); }}
options={sources.map(s => ({ label: s, value: s }))}
/>
<Select
placeholder="数据类型"
allowClear
style={{ width: 150 }}
value={typeFilter}
onChange={(v) => { setTypeFilter(v); setPage(1); }}
options={types.map(t => ({ label: t, value: t }))}
/>
<Select
placeholder="国家"
allowClear
style={{ width: 150 }}
value={countryFilter}
onChange={(v) => { setCountryFilter(v); setPage(1); }}
options={countries.map(c => ({ label: c, value: c }))}
/>
<Input
placeholder="搜索名称"
style={{ width: 200 }}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onPressEnter={handleSearch}
/>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
</Button>
</Space>
</Card>
{/* Data Table */}
<Table
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={{
current: page,
pageSize,
total,
onChange: (p, ps) => { setPage(p); setPageSize(ps); },
showSizeChanger: true,
showTotal: (t) => `${t}`,
}}
/>
{/* Detail Modal */}
<Modal
title="数据详情"
open={detailVisible}
onCancel={() => setDetailVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailVisible(false)}>
</Button>,
]}
width={700}
>
{detailLoading ? (
<div style={{ textAlign: 'center', padding: 40 }}>
<Spin size="large" />
</div>
) : detailData ? (
<Descriptions column={2} bordered>
<Descriptions.Item label="ID">{detailData.id}</Descriptions.Item>
<Descriptions.Item label="数据源">{detailData.source}</Descriptions.Item>
<Descriptions.Item label="数据类型">{detailData.data_type}</Descriptions.Item>
<Descriptions.Item label="原始ID">{detailData.source_id || '-'}</Descriptions.Item>
<Descriptions.Item label="名称" span={2}>{detailData.name}</Descriptions.Item>
<Descriptions.Item label="标题" span={2}>{detailData.title || '-'}</Descriptions.Item>
<Descriptions.Item label="描述" span={2}>{detailData.description || '-'}</Descriptions.Item>
<Descriptions.Item label="国家">{detailData.country || '-'}</Descriptions.Item>
<Descriptions.Item label="城市">{detailData.city || '-'}</Descriptions.Item>
<Descriptions.Item label="经度">{detailData.longitude || '-'}</Descriptions.Item>
<Descriptions.Item label="纬度">{detailData.latitude || '-'}</Descriptions.Item>
<Descriptions.Item label="数值">{detailData.value} {detailData.unit || ''}</Descriptions.Item>
<Descriptions.Item label="采集时间">
{new Date(detailData.collected_at).toLocaleString('zh-CN')}
</Descriptions.Item>
<Descriptions.Item label="参考日期">
{detailData.reference_date ? new Date(detailData.reference_date).toLocaleDateString('zh-CN') : '-'}
</Descriptions.Item>
<Descriptions.Item label="元数据" span={2}>
<pre style={{ margin: 0, maxHeight: 200, overflow: 'auto' }}>
{JSON.stringify(detailData.metadata || {}, null, 2)}
</pre>
</Descriptions.Item>
</Descriptions>
) : (
<Empty description="暂无数据" />
)}
</Modal>
</div>
)
}
export default DataList

View File

@@ -0,0 +1,672 @@
import { useEffect, 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
} from '@ant-design/icons'
import axios from 'axios'
interface BuiltInDataSource {
id: number
name: string
module: string
priority: string
frequency: string
is_active: boolean
collector_class: string
}
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<string, string>
config: Record<string, any>
collector_class: string
module: string
priority: string
frequency: string
}
function DataSources() {
const [activeTab, setActiveTab] = useState('builtin')
const [builtInSources, setBuiltInSources] = useState<BuiltInDataSource[]>([])
const [customSources, setCustomSources] = useState<CustomDataSource[]>([])
const [loading, setLoading] = useState(false)
const [drawerVisible, setDrawerVisible] = useState(false)
const [viewDrawerVisible, setViewDrawerVisible] = useState(false)
const [editingConfig, setEditingConfig] = useState<CustomDataSource | null>(null)
const [viewingSource, setViewingSource] = useState<ViewDataSource | null>(null)
const [testing, setTesting] = useState(false)
const [testResult, setTestResult] = useState<any>(null)
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)
}
}
useEffect(() => {
fetchData()
}, [])
const handleTrigger = async (id: number) => {
try {
await axios.post(`/api/v1/datasources/${id}/trigger`)
message.success('任务已触发')
} catch (error: unknown) {
const err = error as { response?: { data?: { detail?: string } } }
message.error(err.response?.data?.detail || '触发失败')
}
}
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 handleViewSource = async (source: BuiltInDataSource) => {
try {
const res = await axios.get(`/api/v1/datasources/${source.id}`)
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,
})
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 },
{
title: '名称',
dataIndex: 'name',
key: 'name',
render: (name: string, record: BuiltInDataSource) => (
<Button type="link" onClick={() => handleViewSource(record)}>
{name}
</Button>
),
},
{ title: '模块', dataIndex: 'module', key: 'module' },
{
title: '优先级',
dataIndex: 'priority',
key: 'priority',
render: (p: string) => <Tag color={p === 'P0' ? 'red' : 'orange'}>{p}</Tag>,
},
{ title: '频率', dataIndex: 'frequency', key: 'frequency' },
{
title: '状态',
dataIndex: 'is_active',
key: 'is_active',
render: (active: boolean) => (
<Tag color={active ? 'green' : 'red'}>{active ? '运行中' : '已暂停'}</Tag>
),
},
{
title: '操作',
key: 'action',
render: (_: unknown, record: BuiltInDataSource) => (
<Space>
<Button
type="link"
icon={<SyncOutlined />}
onClick={() => handleTrigger(record.id)}
>
</Button>
<Button
type="link"
icon={record.is_active ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
onClick={() => handleToggle(record.id, record.is_active)}
>
{record.is_active ? '禁用' : '启用'}
</Button>
</Space>
),
},
]
const customColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 60 },
{ title: '名称', dataIndex: 'name', key: 'name' },
{ title: '类型', dataIndex: 'source_type', key: 'source_type' },
{
title: '状态',
dataIndex: 'is_active',
key: 'is_active',
render: (active: boolean) => (
<Tag color={active ? 'green' : 'red'}>{active ? '启用' : '禁用'}</Tag>
),
},
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
{
title: '操作',
key: 'action',
render: (_: unknown, record: CustomDataSource) => (
<Space>
<Tooltip title="编辑">
<Button type="link" icon={<EditOutlined />} onClick={() => openDrawer(record)} />
</Tooltip>
<Tooltip title={record.is_active ? '禁用' : '启用'}>
<Button
type="link"
icon={record.is_active ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
onClick={() => handleToggleCustom(record.id, record.is_active)}
/>
</Tooltip>
<Popconfirm
title="确定删除此配置?"
onConfirm={() => handleDelete(record.id)}
>
<Tooltip title="删除">
<Button type="link" danger icon={<DeleteOutlined />} />
</Tooltip>
</Popconfirm>
</Space>
),
},
]
const tabItems = [
{
key: 'builtin',
label: '内置数据源',
children: (
<Table
columns={builtinColumns}
dataSource={builtInSources}
rowKey="id"
loading={loading}
pagination={false}
/>
),
},
{
key: 'custom',
label: (
<span>
<ApiOutlined />
</span>
),
children: (
<>
<div style={{ marginBottom: 16, textAlign: 'right' }}>
<Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
</Button>
</div>
{customSources.length === 0 ? (
<Empty description="暂无自定义数据源" />
) : (
<Table
columns={customColumns}
dataSource={customSources}
rowKey="id"
loading={loading}
pagination={false}
/>
)}
</>
),
},
]
return (
<div>
<h2></h2>
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
<Drawer
title={editingConfig ? '编辑数据源' : '添加数据源'}
width={600}
open={drawerVisible}
onClose={() => {
setDrawerVisible(false)
form.resetFields()
setEditingConfig(null)
setTestResult(null)
}}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button
icon={<ExperimentOutlined />}
loading={testing}
onClick={handleTest}
>
</Button>
<Space>
<Button onClick={() => setDrawerVisible(false)}></Button>
<Button type="primary" onClick={handleSave}>
</Button>
</Space>
</div>
}
>
<Form form={form} layout="vertical">
<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>
<Form.Item
name="endpoint"
label="接口地址"
rules={[{ required: true, message: '请输入接口地址' }]}
>
<Input placeholder="https://api.example.com/data" />
</Form.Item>
<Collapse
items={[
{
key: 'auth',
label: '认证配置',
children: (
<>
<Form.Item name="auth_type" label="认证方式">
<Select>
<Select.Option value="none"></Select.Option>
<Select.Option value="bearer">Bearer Token</Select.Option>
<Select.Option value="api_key">API Key</Select.Option>
<Select.Option value="basic">Basic Auth</Select.Option>
</Select>
</Form.Item>
<div>
<Form.Item noStyle shouldUpdate={(_, { auth_type }) => auth_type === 'bearer'}>
{({ getFieldValue }) => {
if (getFieldValue('auth_type') === 'bearer') {
return (
<Form.Item name={['auth_config', 'token']} label="Token">
<Input.Password placeholder="Bearer Token" />
</Form.Item>
)
}
return null
}}
</Form.Item>
<Form.Item noStyle shouldUpdate={(_, { auth_type }) => auth_type === 'api_key'}>
{({ getFieldValue }) => {
if (getFieldValue('auth_type') === 'api_key') {
return (
<>
<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', 'api_key']} label="API Key">
<Input.Password placeholder="API Key" />
</Form.Item>
</>
)
}
return null
}}
</Form.Item>
<Form.Item noStyle shouldUpdate={(_, { auth_type }) => auth_type === 'basic'}>
{({ getFieldValue }) => {
if (getFieldValue('auth_type') === 'basic') {
return (
<>
<Form.Item name={['auth_config', 'username']} label="用户名">
<Input placeholder="Username" />
</Form.Item>
<Form.Item name={['auth_config', 'password']} label="密码">
<Input.Password placeholder="Password" />
</Form.Item>
</>
)
}
return null
}}
</Form.Item>
</div>
</>
),
},
]}
/>
<Collapse
items={[
{
key: 'headers',
label: '请求头',
children: (
<Form.List name="headers">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Space key={key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
<Form.Item {...restField} name={[name, 'key']} rules={[{ required: true, message: 'Header键' }]}>
<Input placeholder="Content-Type" />
</Form.Item>
<Form.Item {...restField} name={[name, 'value']} rules={[{ required: true, message: 'Header值' }]}>
<Input placeholder="application/json" />
</Form.Item>
<Button type="link" danger onClick={() => remove(name)}></Button>
</Space>
))}
<Button type="dashed" onClick={() => add()} block>
</Button>
</>
)}
</Form.List>
),
},
]}
/>
<Collapse
items={[
{
key: 'config',
label: '高级配置',
children: (
<>
<Form.Item name={['config', 'timeout']} label="超时时间(秒)">
<InputNumber min={1} max={300} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name={['config', 'retry']} label="重试次数">
<InputNumber min={0} max={10} style={{ width: '100%' }} />
</Form.Item>
</>
),
},
]}
/>
{testResult && (
<div style={{
marginTop: 16,
padding: 16,
background: testResult.success ? '#f6ffed' : '#fff2f0',
border: `1px solid ${testResult.success ? '#b7eb8f' : '#ffa39e'}`,
borderRadius: 4,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
{testResult.success ? (
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: 18 }} />
) : (
<CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: 18 }} />
)}
<span style={{ fontWeight: 'bold' }}>
{testResult.success ? '连接成功' : '连接失败'}
</span>
</div>
{testResult.status_code && <div>: {testResult.status_code}</div>}
{testResult.response_time_ms && <div>: {testResult.response_time_ms.toFixed(0)}ms</div>}
{testResult.error && <div style={{ color: '#ff4d4f' }}>: {testResult.error}</div>}
{testResult.data_preview && (
<div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
: {testResult.data_preview}
</div>
)}
</div>
)}
</Form>
</Drawer>
<Drawer
title="查看数据源"
width={600}
open={viewDrawerVisible}
onClose={() => {
setViewDrawerVisible(false)
setViewingSource(null)
}}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button
icon={<ExperimentOutlined />}
loading={testing}
onClick={handleTest}
>
</Button>
<Space>
<Button onClick={() => setViewDrawerVisible(false)}></Button>
<Button
type="primary"
icon={<SyncOutlined />}
onClick={handleUpdateSource}
>
</Button>
</Space>
</div>
}
>
{viewingSource && (
<Form layout="vertical">
<Form.Item label="名称">
<Input value={viewingSource.name} disabled />
</Form.Item>
<Form.Item label="采集器">
<Input value={viewingSource.collector_class} disabled />
</Form.Item>
<Form.Item label="模块">
<Input value={viewingSource.module} disabled />
</Form.Item>
<Form.Item label="优先级">
<Input value={viewingSource.priority} disabled />
</Form.Item>
<Form.Item label="频率">
<Input value={viewingSource.frequency} disabled />
</Form.Item>
<Collapse
items={[
{
key: 'auth',
label: '认证配置',
children: (
<Form.Item label="认证方式">
<Input value={viewingSource.auth_type || 'none'} disabled />
</Form.Item>
),
},
{
key: 'headers',
label: '请求头',
children: viewingSource.headers && Object.keys(viewingSource.headers).length > 0 ? (
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, overflow: 'auto' }}>
{JSON.stringify(viewingSource.headers, null, 2)}
</pre>
) : (
<div style={{ color: '#999' }}></div>
),
},
{
key: 'config',
label: '高级配置',
children: viewingSource.config && Object.keys(viewingSource.config).length > 0 ? (
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, overflow: 'auto' }}>
{JSON.stringify(viewingSource.config, null, 2)}
</pre>
) : (
<div style={{ color: '#999' }}></div>
),
},
]}
/>
</Form>
)}
</Drawer>
</div>
)
}
export default DataSources

View File

@@ -0,0 +1,65 @@
import { useState } from 'react'
import { Input, Button, Form, message } from 'antd'
import { UserOutlined, LockOutlined } from '@ant-design/icons'
import { useAuthStore } from '../../stores/auth'
function Login() {
const [loading, setLoading] = useState(false)
const { login } = useAuthStore()
const onFinish = async (values: { username: string; password: string }) => {
setLoading(true)
try {
await login(values.username, values.password)
message.success('登录成功')
} catch (error: unknown) {
const err = error as { response?: { data?: { detail?: string } } }
message.error(err.response?.data?.detail || '登录失败')
} finally {
setLoading(false)
}
}
return (
<div className="login-container">
<div className="login-box">
<h1 style={{ textAlign: 'center', marginBottom: 24 }}></h1>
<Form name="login" onFinish={onFinish}>
<Form.Item
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input
prefix={<UserOutlined />}
placeholder="用户名"
size="large"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="密码"
size="large"
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
size="large"
block
loading={loading}
>
</Button>
</Form.Item>
</Form>
</div>
</div>
)
}
export default Login

View File

@@ -0,0 +1,421 @@
import { useState, useEffect } from 'react'
import {
Layout,
Menu,
Card,
Row,
Col,
Typography,
Button,
Form,
Input,
Switch,
Select,
Divider,
message,
Spin,
Tabs,
InputNumber,
} from 'antd'
import {
SettingOutlined,
DashboardOutlined,
DatabaseOutlined,
UserOutlined,
BellOutlined,
SafetyOutlined,
SaveOutlined,
} from '@ant-design/icons'
import { Link, useNavigate } from 'react-router-dom'
import { useAuthStore } from '../../stores/auth'
const { Header, Sider, Content } = Layout
const { Title, Text } = Typography
const { TabPane } = Tabs
interface SystemSettings {
system_name: string
refresh_interval: number
auto_refresh: boolean
data_retention_days: number
max_concurrent_tasks: number
}
interface NotificationSettings {
email_enabled: boolean
email_address: string
critical_alerts: boolean
warning_alerts: boolean
daily_summary: boolean
}
interface SecuritySettings {
session_timeout: number
max_login_attempts: number
password_policy: string
}
function Settings() {
const { user, logout, token, clearAuth } = useAuthStore()
const navigate = useNavigate()
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [systemSettings, setSystemSettings] = useState<SystemSettings>({
system_name: '智能星球',
refresh_interval: 60,
auto_refresh: true,
data_retention_days: 30,
max_concurrent_tasks: 5,
})
const [notificationSettings, setNotificationSettings] = useState<NotificationSettings>({
email_enabled: false,
email_address: '',
critical_alerts: true,
warning_alerts: true,
daily_summary: false,
})
const [securitySettings, setSecuritySettings] = useState<SecuritySettings>({
session_timeout: 60,
max_login_attempts: 5,
password_policy: 'medium',
})
const [form] = Form.useForm()
useEffect(() => {
if (!token) {
navigate('/')
return
}
fetchSettings()
}, [token, navigate])
const fetchSettings = async () => {
try {
setLoading(true)
const res = await fetch('/api/v1/settings/system', {
headers: { Authorization: `Bearer ${token}` },
})
if (res.status === 401) {
clearAuth()
navigate('/')
return
}
if (res.ok) {
const data = await res.json()
setSystemSettings(data.system || systemSettings)
setNotificationSettings(data.notifications || notificationSettings)
setSecuritySettings(data.security || securitySettings)
form.setFieldsValue({
...data.system,
...data.notifications,
...data.security,
})
}
} catch (err) {
message.error('获取设置失败')
console.error(err)
} finally {
setLoading(false)
}
}
const handleSaveSystem = async (values: any) => {
try {
setSaving(true)
const res = await fetch('/api/v1/settings/system', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(values),
})
if (res.ok) {
message.success('系统设置已保存')
setSystemSettings(values)
} else {
message.error('保存失败')
}
} catch (err) {
message.error('保存设置失败')
console.error(err)
} finally {
setSaving(false)
}
}
const handleSaveNotifications = async (values: any) => {
try {
setSaving(true)
const res = await fetch('/api/v1/settings/notifications', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(values),
})
if (res.ok) {
message.success('通知设置已保存')
setNotificationSettings(values)
} else {
message.error('保存失败')
}
} catch (err) {
message.error('保存设置失败')
console.error(err)
} finally {
setSaving(false)
}
}
const handleSaveSecurity = async (values: any) => {
try {
setSaving(true)
const res = await fetch('/api/v1/settings/security', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(values),
})
if (res.ok) {
message.success('安全设置已保存')
setSecuritySettings(values)
} else {
message.error('保存失败')
}
} catch (err) {
message.error('保存设置失败')
console.error(err)
} finally {
setSaving(false)
}
}
const handleLogout = () => {
logout()
navigate('/')
}
const menuItems = [
{ key: '/', icon: <DashboardOutlined />, label: <Link to="/"></Link> },
{ key: '/datasources', icon: <DatabaseOutlined />, label: <Link to="/datasources"></Link> },
{ key: '/users', icon: <UserOutlined />, label: <Link to="/users"></Link> },
{ key: '/settings', icon: <SettingOutlined />, label: '系统配置' },
]
if (loading && !token) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<Spin size="large" tip="加载中..." />
</div>
)
}
return (
<Layout className="dashboard-layout">
<Sider width={240} className="dashboard-sider">
<div style={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Title level={4} style={{ color: 'white', margin: 0 }}></Title>
</div>
<Menu theme="dark" mode="inline" defaultSelectedKeys={['/settings']} items={menuItems} />
</Sider>
<Layout>
<Header className="dashboard-header" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Text strong>, {user?.username}</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Button type="link" danger onClick={handleLogout}>退</Button>
</div>
</Header>
<Content className="dashboard-content">
<Title level={3}><SettingOutlined /> </Title>
<Tabs defaultActiveKey="system" tabPosition="left">
<TabPane
tab={<span><SettingOutlined /> </span>}
key="system"
>
<Card title="基本设置">
<Form
form={form}
layout="vertical"
onFinish={handleSaveSystem}
initialValues={systemSettings}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="system_name"
label="系统名称"
rules={[{ required: true, message: '请输入系统名称' }]}
>
<Input placeholder="智能星球" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="refresh_interval"
label="数据刷新间隔 (秒)"
>
<InputNumber min={10} max={3600} style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="data_retention_days"
label="数据保留天数"
>
<InputNumber min={1} max={365} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="max_concurrent_tasks"
label="最大并发任务数"
>
<InputNumber min={1} max={20} style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Form.Item
name="auto_refresh"
label="自动刷新"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={saving}>
</Button>
</Form.Item>
</Form>
</Card>
</TabPane>
<TabPane
tab={<span><BellOutlined /> </span>}
key="notifications"
>
<Card title="通知配置">
<Form
form={form}
layout="vertical"
onFinish={handleSaveNotifications}
initialValues={notificationSettings}
>
<Divider orientation="left"></Divider>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="email_enabled"
label="启用邮件通知"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="email_address"
label="通知邮箱"
rules={[{ type: 'email', message: '请输入有效的邮箱地址' }]}
>
<Input placeholder="admin@example.com" disabled={!notificationSettings.email_enabled} />
</Form.Item>
</Col>
</Row>
<Divider orientation="left"></Divider>
<Row gutter={16}>
<Col span={8}>
<Form.Item
name="critical_alerts"
label="严重告警"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="warning_alerts"
label="警告告警"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="daily_summary"
label="每日摘要"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
</Row>
<Form.Item>
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={saving}>
</Button>
</Form.Item>
</Form>
</Card>
</TabPane>
<TabPane
tab={<span><SafetyOutlined /> </span>}
key="security"
>
<Card title="安全配置">
<Form
form={form}
layout="vertical"
onFinish={handleSaveSecurity}
initialValues={securitySettings}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="session_timeout"
label="会话超时 (分钟)"
>
<InputNumber min={5} max={1440} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="max_login_attempts"
label="最大登录尝试次数"
>
<InputNumber min={1} max={10} style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Form.Item
name="password_policy"
label="密码策略"
>
<Select>
<Select.Option value="low"> (6)</Select.Option>
<Select.Option value="medium"> (8,)</Select.Option>
<Select.Option value="high"> (12,)</Select.Option>
</Select>
</Form.Item>
<Divider />
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={saving}>
</Button>
</Form>
</Card>
</TabPane>
</Tabs>
</Content>
</Layout>
</Layout>
)
}
export default Settings

View File

@@ -0,0 +1,153 @@
import { useEffect, useState } from 'react'
import { Table, Tag, Card, Row, Col, Statistic, Button } from 'antd'
import { ReloadOutlined, CheckCircleOutlined, CloseCircleOutlined, SyncOutlined } from '@ant-design/icons'
import { useAuthStore } from '../../stores/auth'
interface Task {
id: number
collector: string
status: 'success' | 'failed' | 'running' | 'pending'
records_processed: number
started_at: string
completed_at: string
duration_seconds: number
}
function Tasks() {
const { token } = useAuthStore()
const [tasks, setTasks] = useState<Task[]>([])
const [loading, setLoading] = useState(false)
const [stats, setStats] = useState({
total_today: 0,
success: 0,
failed: 0,
running: 0,
})
const fetchTasks = async () => {
setLoading(true)
try {
const res = await fetch('/api/v1/tasks', {
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
setTasks(data.data || [])
setStats(data.stats || stats)
} catch (error) {
console.error('Failed to fetch tasks:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchTasks()
}, [token])
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{
title: '收集器',
dataIndex: 'collector',
key: 'collector',
render: (c: string) => <Tag color="blue">{c}</Tag>,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const colors: Record<string, string> = {
success: 'success',
failed: 'error',
running: 'processing',
pending: 'default',
}
const icons: Record<string, JSX.Element> = {
success: <CheckCircleOutlined />,
failed: <CloseCircleOutlined />,
running: <SyncOutlined spin />,
pending: <SyncOutlined />,
}
return (
<Tag color={colors[status] || 'default'} icon={icons[status]}>
{status === 'success' ? '成功' : status === 'failed' ? '失败' : status === 'running' ? '运行中' : '等待中'}
</Tag>
)
},
},
{
title: '处理记录',
dataIndex: 'records_processed',
key: 'records_processed',
render: (n: number) => n.toLocaleString(),
},
{
title: '耗时',
dataIndex: 'duration_seconds',
key: 'duration_seconds',
render: (s: number) => s ? `${s.toFixed(2)}s` : '-',
},
{
title: '开始时间',
dataIndex: 'started_at',
key: 'started_at',
render: (t: string) => t ? new Date(t).toLocaleString('zh-CN') : '-',
},
]
const statusCounts = tasks.reduce(
(acc, task) => {
acc[task.status]++
return acc
},
{ success: 0, failed: 0, running: 0, pending: 0 } as Record<string, number>
)
const successRate = tasks.length > 0 ? (statusCounts.success / tasks.length) * 100 : 0
return (
<div>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card>
<Statistic title="今日任务" value={tasks.length} prefix={<ReloadOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="成功率"
value={successRate.toFixed(1)}
suffix="%"
valueStyle={{ color: successRate >= 90 ? '#52c41a' : '#faad14' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="成功" value={statusCounts.success} valueStyle={{ color: '#52c41a' }} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="失败" value={statusCounts.failed} valueStyle={{ color: '#ff4d4f' }} />
</Card>
</Col>
</Row>
<Card
title="任务历史"
extra={
<Button type="primary" icon={<ReloadOutlined />} onClick={fetchTasks}>
</Button>
}
>
<Table columns={columns} dataSource={tasks} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} />
</Card>
</div>
)
}
export default Tasks

View File

@@ -0,0 +1,157 @@
import { useEffect, useState } from 'react'
import { Table, Button, Tag, Space, message, Modal, Form, Input, Select } from 'antd'
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
import axios from 'axios'
interface User {
id: number
username: string
email: string
role: string
is_active: boolean
created_at: string
}
function Users() {
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(false)
const [modalVisible, setModalVisible] = useState(false)
const [editingUser, setEditingUser] = useState<User | null>(null)
const [form] = Form.useForm()
const fetchUsers = async () => {
setLoading(true)
try {
const res = await axios.get('/api/v1/users')
setUsers(res.data.data || [])
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchUsers()
}, [])
const handleAdd = () => {
setEditingUser(null)
form.resetFields()
setModalVisible(true)
}
const handleEdit = (user: User) => {
setEditingUser(user)
form.setFieldsValue(user)
setModalVisible(true)
}
const handleDelete = async (id: number) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除此用户吗?',
onOk: async () => {
await axios.delete(`/api/v1/users/${id}`)
message.success('删除成功')
fetchUsers()
},
})
}
const handleSubmit = async (values: Record<string, unknown>) => {
try {
if (editingUser) {
await axios.put(`/api/v1/users/${editingUser.id}`, values)
message.success('更新成功')
} else {
await axios.post('/api/v1/users', values)
message.success('创建成功')
}
setModalVisible(false)
fetchUsers()
} catch (error: unknown) {
const err = error as { response?: { data?: { detail?: string } } }
message.error(err.response?.data?.detail || '操作失败')
}
}
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '用户名', dataIndex: 'username', key: 'username' },
{ title: '邮箱', dataIndex: 'email', key: 'email' },
{
title: '角色',
dataIndex: 'role',
key: 'role',
render: (role: string) => {
const colors: Record<string, string> = {
super_admin: 'red',
admin: 'orange',
operator: 'blue',
viewer: 'green',
}
return <Tag color={colors[role] || 'default'}>{role}</Tag>
},
},
{
title: '状态',
dataIndex: 'is_active',
key: 'is_active',
render: (active: boolean) => (
<Tag color={active ? 'green' : 'red'}>{active ? '活跃' : '禁用'}</Tag>
),
},
{
title: '操作',
key: 'action',
render: (_: unknown, record: User) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}></Button>
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)}></Button>
</Space>
),
},
]
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<h2></h2>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}></Button>
</div>
<Table columns={columns} dataSource={users} rowKey="id" loading={loading} />
<Modal
title={editingUser ? '编辑用户' : '添加用户'}
open={modalVisible}
onCancel={() => setModalVisible(false)}
footer={null}
>
<Form form={form} onFinish={handleSubmit} layout="vertical">
<Form.Item name="username" label="用户名" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="email" label="邮箱" rules={[{ required: true, type: 'email' }]}>
<Input />
</Form.Item>
{!editingUser && (
<Form.Item name="password" label="密码" rules={[{ required: true, min: 8 }]}>
<Input.Password />
</Form.Item>
)}
<Form.Item name="role" label="角色" rules={[{ required: true }]}>
<Select>
<Select.Option value="super_admin"></Select.Option>
<Select.Option value="admin"></Select.Option>
<Select.Option value="operator"></Select.Option>
<Select.Option value="viewer"></Select.Option>
</Select>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block></Button>
</Form.Item>
</Form>
</Modal>
</div>
)
}
export default Users