完善直播节点监控与边缘代理
This commit is contained in:
1 parent
058bb4b99f
commit
20cd99efa4
19 files changed
+401
-47
No files matched your search
@@ -1,11 +1,11 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Table, InputNumber, Switch, Typography, message, Tag, Statistic, Row, Col, Button, Progress } from 'antd'
|
||||
import { Table, Input, InputNumber, Switch, Typography, message, Tag, Statistic, Row, Col, Button, Progress } from 'antd'
|
||||
import { CheckCircleOutlined, CloseCircleOutlined, ReloadOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
import api from '../../api/client'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
interface NodeMetrics { cpu_percent: number | null; memory_percent: number | null; bandwidth_mbps: number | null; active_connections: number | null; hls_readers: number | null }
|
||||
interface NodeMetrics { cpu_percent: number | null; memory_percent: number | null; bandwidth_mbps: number | null; active_connections: number | null; hls_readers: number | null; network_in_mbps: number | null; network_out_mbps: number | null; viewer_count: number | null; frame_drop_indicators: number | null; packet_loss_indicators: number | null; media_metrics_available: boolean }
|
||||
interface NodeInfo { id: string; name: string; host: string; hls_port: number; api_port: number; weight: number; enabled: boolean; subnet: string; is_origin: boolean; online: boolean; metrics: NodeMetrics | null }
|
||||
interface StrategyInfo { strategy: string; multicast_enabled: boolean; multicast_available: boolean }
|
||||
|
||||
@@ -31,6 +31,12 @@ export default function NodeManager() {
|
||||
const node = nodes.find(n => n.id === nid); if (!node) return
|
||||
try { await api.put(`/nodes/${nid}`, { weight: node.weight, enabled: en }); setNodes(p => p.map(n => n.id === nid ? { ...n, enabled: en } : n)) } catch { message.error('更新失败') }
|
||||
}
|
||||
async function updateName(nid: string, name: string) {
|
||||
const trimmed = name.trim()
|
||||
if (!trimmed) { message.error('节点名称不能为空'); loadData(); return }
|
||||
try { await api.put(`/nodes/${nid}`, { name: trimmed }); setNodes(p => p.map(n => n.id === nid ? { ...n, name: trimmed } : n)); message.success('名称已更新') }
|
||||
catch { message.error('名称更新失败'); loadData() }
|
||||
}
|
||||
|
||||
const onlineCount = nodes.filter(n => n.online).length
|
||||
|
||||
@@ -42,8 +48,8 @@ export default function NodeManager() {
|
||||
boxShadow: `0 0 6px ${v ? 'rgba(91,140,90,0.3)' : 'rgba(181,74,74,0.3)'}` }} />
|
||||
),
|
||||
},
|
||||
{ title: '名称', dataIndex: 'name', key: 'name',
|
||||
render: (v: string, r: NodeInfo) => <span><Text strong>{v}</Text>{r.is_origin && <Tag color="warning" style={{ marginLeft: 8 }}>Origin</Tag>}</span> },
|
||||
{ title: '名称', dataIndex: 'name', key: 'name', width: 190,
|
||||
render: (v: string, r: NodeInfo) => <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Input defaultValue={v} size="small" onPressEnter={e => updateName(r.id, e.currentTarget.value)} onBlur={e => { if (e.currentTarget.value !== v) updateName(r.id, e.currentTarget.value) }} />{r.is_origin && <Tag color="warning" style={{ margin: 0 }}>主节点</Tag>}</span> },
|
||||
{ title: '地址', dataIndex: 'host', key: 'host', width: 130 },
|
||||
{ title: '子网', dataIndex: 'subnet', key: 'subnet', width: 130, render: (v: string) => <Text code style={{ fontSize: 12 }}>{v}</Text> },
|
||||
{ title: 'CPU', key: 'cpu', width: 80,
|
||||
@@ -58,6 +64,12 @@ export default function NodeManager() {
|
||||
: <Text type="secondary">--</Text> },
|
||||
{ title: '带宽', key: 'bw', width: 85,
|
||||
render: (_: any, r: NodeInfo) => r.metrics?.bandwidth_mbps != null ? <Text style={{ fontSize: 12 }}>{r.metrics.bandwidth_mbps.toFixed(1)} Mbps</Text> : <Text type="secondary">--</Text> },
|
||||
{ title: '网络入/出', key: 'network', width: 125,
|
||||
render: (_: any, r: NodeInfo) => r.metrics?.network_in_mbps != null && r.metrics?.network_out_mbps != null ? <Text style={{ fontSize: 12 }}>{r.metrics.network_in_mbps.toFixed(1)} / {r.metrics.network_out_mbps.toFixed(1)} Mbps</Text> : <Text type="secondary">不可用</Text> },
|
||||
{ title: '观众', key: 'viewers', width: 65,
|
||||
render: (_: any, r: NodeInfo) => r.metrics?.viewer_count != null ? r.metrics.viewer_count : <Text type="secondary">不可用</Text> },
|
||||
{ title: '媒体异常', key: 'media', width: 120,
|
||||
render: (_: any, r: NodeInfo) => r.metrics?.media_metrics_available ? <Text style={{ fontSize: 12 }}>丢帧 {r.metrics.frame_drop_indicators ?? 0} / 丢包 {r.metrics.packet_loss_indicators ?? 0}</Text> : <Text type="secondary">不可用</Text> },
|
||||
{ title: '连接', key: 'conn', width: 60,
|
||||
render: (_: any, r: NodeInfo) => r.metrics?.active_connections != null ? r.metrics.active_connections : <Text type="secondary">--</Text> },
|
||||
{ title: '权重', dataIndex: 'weight', key: 'weight', width: 120,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { Form, Input, Button, DatePicker, Typography, message, Space, Alert } from 'antd'
|
||||
import { CopyOutlined, ArrowLeftOutlined, CheckCircleOutlined } from '@ant-design/icons'
|
||||
import { createStream } from '../../api/streams'
|
||||
import axios from 'axios'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
const { TextArea } = Input
|
||||
@@ -18,8 +19,12 @@ export default function StreamForm() {
|
||||
const res = await createStream({ title: values.title, description: values.description, cover_url: values.cover_url, scheduled_at: values.scheduled_at?.toISOString() })
|
||||
setResult({ rtmp_url: res.rtmp_url, stream_key: res.stream_key })
|
||||
message.success('直播已创建')
|
||||
} catch { message.error('创建失败') }
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
const detail = axios.isAxiosError(error) ? error.response?.data?.detail : undefined
|
||||
message.error(typeof detail === 'string' ? detail : '创建失败,请稍后重试')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (result) {
|
||||
|
||||
Reference in new issue
Block a user