mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 23:16:37 +08:00
[优化] 更新节点类型支持,添加隧道客户端,重构相关路由和配置
This commit is contained in:
@@ -746,39 +746,77 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
: '当前没有活动中的健康事件'
|
||||
}
|
||||
/>
|
||||
<SummaryStat
|
||||
label="当前窗口请求"
|
||||
value={
|
||||
trafficSummary
|
||||
? trafficSummary.request_count.toLocaleString('zh-CN')
|
||||
: '—'
|
||||
}
|
||||
hint={
|
||||
trafficSummary
|
||||
? `QPS ${trafficSummary.estimated_qps.toFixed(1)} · 错误率 ${trafficSummary.error_rate_percent.toFixed(1)}%`
|
||||
: '当前没有可展示的请求窗口摘要'
|
||||
}
|
||||
/>
|
||||
<SummaryStat
|
||||
label="容量压力"
|
||||
value={
|
||||
healthSummary?.has_capacity_risk ? '需要关注' : '正常范围'
|
||||
}
|
||||
hint={
|
||||
latestMetricSnapshot
|
||||
? `CPU ${formatPercent(latestMetricSnapshot.cpu_usage_percent)} · 存储 ${formatPercent(storageUsageRatio)}`
|
||||
: '当前没有资源快照'
|
||||
}
|
||||
/>
|
||||
<SummaryStat
|
||||
label="来源信号"
|
||||
value={topSourceCountry?.key ?? '—'}
|
||||
hint={
|
||||
topSourceCountry
|
||||
? `${topSourceCountry.value.toLocaleString('zh-CN')} 次请求`
|
||||
: '当前没有来源分布数据'
|
||||
}
|
||||
/>
|
||||
{node.node_type === 'tunnel_relay' ? (
|
||||
<>
|
||||
<SummaryStat
|
||||
label="隧道总数"
|
||||
value={
|
||||
observability?.relay_dashboard
|
||||
? `${observability.relay_dashboard.total_proxies}`
|
||||
: '—'
|
||||
}
|
||||
hint={
|
||||
observability?.relay_dashboard
|
||||
? `${observability.relay_dashboard.online_proxies} 个在线,${observability.relay_dashboard.offline_proxies} 个离线`
|
||||
: '暂无隧道统计'
|
||||
}
|
||||
/>
|
||||
<SummaryStat
|
||||
label="当前连接数"
|
||||
value={
|
||||
observability?.relay_dashboard
|
||||
? `${observability.relay_dashboard.total_connections}`
|
||||
: '—'
|
||||
}
|
||||
hint="隧道网络承载的活动连接总数"
|
||||
/>
|
||||
<SummaryStat
|
||||
label="活动客户端"
|
||||
value={
|
||||
observability?.relay_dashboard
|
||||
? `${observability.relay_dashboard.client_counts}`
|
||||
: '—'
|
||||
}
|
||||
hint="已连接并活跃的客户端数量"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SummaryStat
|
||||
label="当前窗口请求"
|
||||
value={
|
||||
trafficSummary
|
||||
? trafficSummary.request_count.toLocaleString('zh-CN')
|
||||
: '—'
|
||||
}
|
||||
hint={
|
||||
trafficSummary
|
||||
? `QPS ${trafficSummary.estimated_qps.toFixed(1)} · 错误率 ${trafficSummary.error_rate_percent.toFixed(1)}%`
|
||||
: '当前没有可展示的请求窗口摘要'
|
||||
}
|
||||
/>
|
||||
<SummaryStat
|
||||
label="容量压力"
|
||||
value={
|
||||
healthSummary?.has_capacity_risk ? '需要关注' : '正常范围'
|
||||
}
|
||||
hint={
|
||||
latestMetricSnapshot
|
||||
? `CPU ${formatPercent(latestMetricSnapshot.cpu_usage_percent)} · 存储 ${formatPercent(storageUsageRatio)}`
|
||||
: '当前没有资源快照'
|
||||
}
|
||||
/>
|
||||
<SummaryStat
|
||||
label="来源信号"
|
||||
value={topSourceCountry?.key ?? '—'}
|
||||
hint={
|
||||
topSourceCountry
|
||||
? `${topSourceCountry.value.toLocaleString('zh-CN')} 次请求`
|
||||
: '当前没有来源分布数据'
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-3">
|
||||
@@ -940,7 +978,8 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
)}
|
||||
</AppCard>
|
||||
|
||||
<AppCard title="网络流量">
|
||||
{node.node_type === 'edge_node' ? (
|
||||
<AppCard title="网络流量">
|
||||
{observabilityQuery.isLoading ? (
|
||||
<LoadingState />
|
||||
) : observabilityQuery.isError ? (
|
||||
@@ -1054,9 +1093,12 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
/>
|
||||
)}
|
||||
</AppCard>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-2">
|
||||
{node.node_type === 'edge_node' ? (
|
||||
<>
|
||||
<div className="grid gap-6 xl:grid-cols-2">
|
||||
<AppCard
|
||||
title="24 小时请求趋势"
|
||||
description="按小时聚合该节点的请求量和错误量。"
|
||||
@@ -1202,12 +1244,64 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</AppCard>
|
||||
</div>
|
||||
</AppCard>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{node.node_type === 'tunnel_relay' ? (
|
||||
<div className="grid gap-6 xl:grid-cols-1">
|
||||
<AppCard title="隧道运行状态" description="当前中继节点上活跃的代理通道。">
|
||||
{observability?.relay_dashboard?.proxies ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm text-[var(--foreground-secondary)]">
|
||||
<thead className="border-b border-[var(--border-default)]">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">代理名称</th>
|
||||
<th className="px-4 py-3 font-medium">类型</th>
|
||||
<th className="px-4 py-3 font-medium">状态</th>
|
||||
<th className="px-4 py-3 font-medium">客户端地址</th>
|
||||
<th className="px-4 py-3 font-medium">最近连接</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[var(--border-default)]">
|
||||
{observability.relay_dashboard.proxies.map((proxy) => (
|
||||
<tr key={proxy.name}>
|
||||
<td className="px-4 py-3 font-medium text-[var(--foreground-primary)]">
|
||||
{proxy.name}
|
||||
</td>
|
||||
<td className="px-4 py-3">{proxy.type}</td>
|
||||
<td className="px-4 py-3">
|
||||
<StatusBadge
|
||||
label={proxy.status === 'online' ? '在线' : '离线'}
|
||||
variant={proxy.status === 'online' ? 'success' : 'info'}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3">{proxy.client_addr}</td>
|
||||
<td className="px-4 py-3">
|
||||
{isMeaningfulTime(proxy.last_start_time)
|
||||
? formatRelativeTime(proxy.last_start_time)
|
||||
: '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="暂无通道信息"
|
||||
description="当前节点未上报映射通道状态,或尚未有客户端连接。"
|
||||
/>
|
||||
)}
|
||||
</AppCard>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
|
||||
<AppCard
|
||||
title="请求结构分布"
|
||||
{node.node_type === 'edge_node' ? (
|
||||
<AppCard
|
||||
title="请求结构分布"
|
||||
description="聚合最近 24 小时窗口上报,帮助判断错误集中在哪些状态码、流量集中在哪些域名。"
|
||||
>
|
||||
<div className="mb-6 grid gap-4 md:grid-cols-3">
|
||||
@@ -1273,6 +1367,7 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
) : null}
|
||||
|
||||
<AppCard
|
||||
title="健康事件时间线"
|
||||
@@ -1519,8 +1614,9 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
)}
|
||||
</AppCard>
|
||||
|
||||
<AppCard
|
||||
title="OpenResty 健康与控制"
|
||||
{node.node_type === 'edge_node' ? (
|
||||
<AppCard
|
||||
title="OpenResty 健康与控制"
|
||||
description="OpenResty 当前健康状态。"
|
||||
action={
|
||||
<div className="flex flex-wrap gap-3">
|
||||
@@ -1569,6 +1665,7 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
|
||||
<AppCard
|
||||
|
||||
@@ -37,7 +37,7 @@ type RegionOption = {
|
||||
|
||||
const nodeEditorSchema = z
|
||||
.object({
|
||||
type: z.enum(['edge_node', 'tunnel_relay']),
|
||||
type: z.enum(['edge_node', 'tunnel_relay', 'tunnel_client']),
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -48,6 +48,7 @@ const nodeEditorSchema = z
|
||||
auto_update_enabled: z.boolean(),
|
||||
relay_bind_port: z.string().trim(),
|
||||
relay_client_access_addr: z.string().trim(),
|
||||
relay_agent_access_addr: z.string().trim(),
|
||||
relay_client_proxy_url: z.string().trim(),
|
||||
geo_manual_override: z.boolean(),
|
||||
geo_region: z.string(),
|
||||
@@ -129,6 +130,7 @@ const defaultValues: NodeEditorValues = {
|
||||
auto_update_enabled: false,
|
||||
relay_bind_port: '7000',
|
||||
relay_client_access_addr: '',
|
||||
relay_agent_access_addr: '',
|
||||
relay_client_proxy_url: '',
|
||||
geo_manual_override: false,
|
||||
geo_region: '',
|
||||
@@ -218,6 +220,7 @@ function buildFormValues(node?: Partial<NodeItem> | null): NodeEditorValues {
|
||||
auto_update_enabled: node.auto_update_enabled ?? false,
|
||||
relay_bind_port: String(node.relay_bind_port ?? 7000),
|
||||
relay_client_access_addr: node.relay_client_access_addr ?? '',
|
||||
relay_agent_access_addr: node.relay_agent_access_addr ?? '',
|
||||
relay_client_proxy_url: node.relay_client_proxy_url ?? '',
|
||||
geo_manual_override: node.geo_manual_override ?? false,
|
||||
geo_region: node.geo_manual_override ? (node.geo_name ?? '') : '',
|
||||
@@ -242,6 +245,7 @@ function toPayload(values: NodeEditorValues): NodeMutationPayload {
|
||||
auto_update_enabled: values.auto_update_enabled,
|
||||
relay_bind_port: values.type === 'tunnel_relay' ? Number(values.relay_bind_port) : undefined,
|
||||
relay_client_access_addr: values.type === 'tunnel_relay' ? values.relay_client_access_addr.trim() : undefined,
|
||||
relay_agent_access_addr: values.type === 'tunnel_relay' ? values.relay_agent_access_addr.trim() : undefined,
|
||||
relay_client_proxy_url: values.type === 'tunnel_relay' ? values.relay_client_proxy_url.trim() : undefined,
|
||||
};
|
||||
|
||||
@@ -353,6 +357,7 @@ export function NodeEditorModal({
|
||||
<ResourceSelect {...form.register('type')} disabled={!!node}>
|
||||
<option value="edge_node">边缘节点 (Edge Node)</option>
|
||||
<option value="tunnel_relay">隧道中继节点 (Tunnel Relay)</option>
|
||||
<option value="tunnel_client">隧道客户端 (Tunnel Client)</option>
|
||||
</ResourceSelect>
|
||||
</ResourceField>
|
||||
|
||||
@@ -386,6 +391,14 @@ export function NodeEditorModal({
|
||||
<ResourceInput placeholder="例如: relay.example.com:7000" {...form.register('relay_client_access_addr')} />
|
||||
</ResourceField>
|
||||
|
||||
<ResourceField
|
||||
label="边缘节点接入地址 (Edge Node Access Addr)"
|
||||
hint="可选,用于 Edge Node 转发请求。默认与 Client Access Addr 或 IP + 绑定端口一致。"
|
||||
error={form.formState.errors.relay_agent_access_addr?.message}
|
||||
>
|
||||
<ResourceInput placeholder="例如: 10.0.0.1:7000" {...form.register('relay_agent_access_addr')} />
|
||||
</ResourceField>
|
||||
|
||||
<ResourceField
|
||||
label="代理地址 (Proxy URL)"
|
||||
hint="可选,下发给客户端。当客户端连接中继需要经过 HTTP 代理时使用。"
|
||||
|
||||
@@ -296,7 +296,7 @@ export function NodesPage() {
|
||||
<th className="px-3 py-3 font-medium">节点</th>
|
||||
<th className="px-3 py-3 font-medium">状态</th>
|
||||
<th className="px-3 py-3 font-medium">
|
||||
Agent / OpenResty
|
||||
Version
|
||||
</th>
|
||||
<th className="px-3 py-3 font-medium">运行健康</th>
|
||||
<th className="px-3 py-3 font-medium">当前版本</th>
|
||||
@@ -318,6 +318,10 @@ export function NodesPage() {
|
||||
<span className="rounded-full border border-blue-500/20 bg-blue-50 px-2 py-0.5 text-xs text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
|
||||
Relay
|
||||
</span>
|
||||
) : node.node_type === 'tunnel_client' ? (
|
||||
<span className="rounded-full border border-purple-500/20 bg-purple-50 px-2 py-0.5 text-xs text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
|
||||
Client
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded-full border border-gray-500/20 bg-gray-50 px-2 py-0.5 text-xs text-gray-700 dark:bg-gray-800/50 dark:text-gray-300">
|
||||
Edge
|
||||
@@ -339,9 +343,8 @@ export function NodesPage() {
|
||||
variant={getNodeStatusVariant(node.status)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-4 text-[var(--foreground-secondary)]">
|
||||
{node.agent_version || 'unknown'} /{' '}
|
||||
{node.nginx_version || 'unknown'}
|
||||
<td className="px-3 py-4 text-sm text-[var(--foreground-secondary)]">
|
||||
{node.agent_version || 'unknown'}
|
||||
</td>
|
||||
<td className="px-3 py-4">
|
||||
<div className="space-y-2">
|
||||
@@ -350,6 +353,8 @@ export function NodesPage() {
|
||||
label={getRelayStatusLabel(node.relay_status)}
|
||||
variant={getRelayStatusVariant(node.relay_status)}
|
||||
/>
|
||||
) : node.node_type === 'tunnel_client' ? (
|
||||
<span className="text-sm text-[var(--foreground-secondary)]">-</span>
|
||||
) : (
|
||||
<StatusBadge
|
||||
label={getOpenrestyStatusLabel(
|
||||
|
||||
Reference in New Issue
Block a user