feat(web): /topology 升級為 React Flow 完整版 (串接真實 dashboard API)
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
OG T
2026-04-09 09:49:31 +08:00
parent 71437db0e9
commit c26c4030e4

View File

@@ -1,102 +1,56 @@
'use client'
/**
* 拓撲圖 Page
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
* @updated 2026-04-02 ogt - 升級為真實 UI串接 /api/v1/dashboard hosts
* 拓撲圖 Page — Sprint 5 React Flow 完整版
* ==========================================
* 嵌套群組拓撲圖 (elkjs + React Flow)
* 串接真實 /api/v1/dashboard API
* 零假數據
*
* @created 2026-04-01 ogt
* @updated 2026-04-09 Claude Code — Sprint 5 React Flow 完整升級
*/
import { useEffect, useState } from 'react'
import { useTranslations } from 'next-intl'
import { AppLayout } from '@/components/layout'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
interface HostItem {
name: string
status: string
cpu?: number
ram?: number
services?: { name: string; status: string }[]
}
const statusColor = (s: string) => {
if (s === 'healthy' || s === 'running') return '#4caf50'
if (s === 'warning') return '#ff9800'
if (s === 'critical' || s === 'error') return '#f44336'
return '#87867f'
}
import { ServiceTopology } from '@/components/topology'
export default function TopologyPage({ params }: { params: { locale: string } }) {
const t = useTranslations('topology')
const tc = useTranslations('common')
const [hosts, setHosts] = useState<HostItem[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
useEffect(() => {
setLoading(true)
setError(false)
fetch(`${API_BASE}/api/v1/dashboard`)
.then(r => r.json())
.then(data => { setHosts(data?.hosts ?? []) })
.catch(() => setError(true))
.finally(() => setLoading(false))
}, [])
return (
<AppLayout locale={params.locale}>
<div style={{ padding: '24px', background: '#f5f4ed', minHeight: '100%' }}>
<div style={{ marginBottom: '20px' }}>
<h1 style={{ fontSize: 18, fontWeight: 700, color: '#141413', margin: 0, fontFamily: 'var(--font-body), monospace' }}>{t('title')}</h1>
<p style={{ fontSize: 12, color: '#87867f', margin: '4px 0 0', fontFamily: 'var(--font-body), monospace' }}>{t('subtitle')}</p>
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 68px)' }}>
{/* 標題列 */}
<div style={{
padding: '12px 20px',
borderBottom: '0.5px solid #e0ddd4',
display: 'flex',
alignItems: 'center',
gap: 8,
background: '#faf9f3',
flexShrink: 0,
}}>
<h2 style={{
fontFamily: "'Syne', sans-serif",
fontSize: 18,
fontWeight: 800,
color: '#141413',
}}>
{t('title')}
</h2>
<span style={{ fontSize: 12, color: '#87867f' }}>{t('subtitle')}</span>
</div>
{loading && (
<div style={{ textAlign: 'center', padding: '40px', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{tc('loading')}</div>
)}
{!loading && error && (
<div style={{ textAlign: 'center', padding: '40px', color: '#f44336', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('fetchError')}</div>
)}
{!loading && !error && hosts.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noHosts')}</div>
)}
{!loading && !error && hosts.length > 0 && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: 16 }}>
{hosts.map((host, i) => (
<div key={i} style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, overflow: 'hidden' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px', borderBottom: '0.5px solid #e0ddd4', background: '#faf9f3' }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: statusColor(host.status), display: 'inline-block' }} />
<span style={{ fontSize: 13, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>{host.name}</span>
</div>
<div style={{ padding: '12px 14px', display: 'flex', gap: 16 }}>
<div>
<div style={{ fontSize: 10, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>{t('cpu')}</div>
<div style={{ fontSize: 14, fontWeight: 600, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>{host.cpu != null ? `${host.cpu}%` : '--'}</div>
</div>
<div>
<div style={{ fontSize: 10, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>{t('ram')}</div>
<div style={{ fontSize: 14, fontWeight: 600, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>{host.ram != null ? `${host.ram}%` : '--'}</div>
</div>
</div>
{(host.services ?? []).length > 0 && (
<div style={{ padding: '0 14px 12px' }}>
<div style={{ fontSize: 10, color: '#87867f', fontFamily: 'var(--font-body), monospace', marginBottom: 6 }}>{t('services')}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
{(host.services ?? []).map((svc, j) => (
<span key={j} style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, border: '0.5px solid #e0ddd4', color: '#141413', background: '#faf9f3', fontFamily: 'var(--font-body), monospace' }}>
<span style={{ display: 'inline-block', width: 5, height: 5, borderRadius: '50%', background: statusColor(svc.status), marginRight: 4, verticalAlign: 'middle' }} />
{svc.name}
</span>
))}
</div>
</div>
)}
</div>
))}
</div>
)}
{/* React Flow 拓撲圖 (全高度) */}
<div style={{ flex: 1 }}>
<ServiceTopology
mode="full"
showControls={true}
showMiniMap={true}
height="100%"
/>
</div>
</div>
</AppLayout>
)