feat(web): /topology 升級為 React Flow 完整版 (串接真實 dashboard API)
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user