feat(web): 基礎架構主機卡點擊 → 詳情抽屜展開
Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 3m57s
E2E Health Check / e2e-health (push) Successful in 35s

點擊主機卡展開行內抽屜:
- CPU/RAM 大字顯示(含顏色警示:>80% 紅/>60% 橙)
- 完整服務清單(狀態點 + port + latency_ms)
- 相關事件(按 affected_services 過濾)
- ✕ 關閉 / 再點同卡收合
- 選中狀態:藍色邊框高亮

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-09 23:49:00 +08:00
parent 2897007014
commit 524423577a

View File

@@ -736,6 +736,7 @@ export default function Home({ params }: { params: { locale: string } }) {
// Sprint 5: 從 URL 讀取當前 Tab
const [activeTabId, setActiveTabId] = useState('overview')
const [infraView, setInfraView] = useState<'host' | 'topo'>('topo')
const [selectedHost, setSelectedHost] = useState<any>(null)
// I1 修正: popstate 取代 100ms 輪詢
useEffect(() => {
@@ -1008,12 +1009,21 @@ export default function Home({ params }: { params: { locale: string } }) {
{ name: tTopo('hostK3sMaster'), ip: '192.168.0.120', cpu: 45, ram: 60 },
{ name: tTopo('hostK3sWorker'), ip: '192.168.0.121', cpu: null as number | null, ram: null as number | null },
].map(h => {
// 嘗試從 API 取得真實數據
const apiHost = hosts.find(ah => ah.ip === h.ip)
const cpu = apiHost?.metrics?.cpu_percent ?? h.cpu
const ram = apiHost?.metrics?.memory_percent ?? h.ram
const isSelected = selectedHost?.ip === h.ip
return (
<div key={h.ip} style={{ border: '0.5px solid #e0ddd4', borderRadius: 8, padding: '8px 10px', background: '#faf9f3' }}>
<div
key={h.ip}
onClick={() => setSelectedHost(isSelected ? null : { ...h, cpu, ram, services: apiHost?.services ?? [], status: apiHost?.status ?? 'unknown', role: apiHost?.role })}
style={{
border: `0.5px solid ${isSelected ? '#4A90D9' : '#e0ddd4'}`,
borderRadius: 8, padding: '8px 10px',
background: isSelected ? 'rgba(74,144,217,0.05)' : '#faf9f3',
cursor: 'pointer', transition: 'all 0.15s',
}}
>
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 2 }}>{h.name}</div>
<div style={{ fontSize: 10, color: '#555550', fontFamily: "'JetBrains Mono', monospace" }}>{h.ip}</div>
<div style={{ display: 'flex', gap: 6, marginTop: 5 }}>
@@ -1037,6 +1047,83 @@ export default function Home({ params }: { params: { locale: string } }) {
})}
</div>
)}
{/* 主機詳情抽屜 — 點擊主機卡滑入 */}
{infraView === 'host' && selectedHost && (() => {
const sh = selectedHost
const relatedIncidents = incidents.filter(inc =>
inc.affected_services?.some(s => s.includes(sh.ip))
).slice(0, 3)
return (
<div style={{
margin: '0 14px 14px',
border: '0.5px solid #4A90D9',
borderRadius: 10,
overflow: 'hidden',
background: '#fff',
animation: 'slideDown 0.2s ease',
}}>
{/* 標頭 */}
<div style={{ background: 'rgba(74,144,217,0.06)', borderBottom: '0.5px solid #dbeafe', padding: '10px 14px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<div style={{ fontSize: 13, fontWeight: 700, color: '#141413' }}>{sh.name}</div>
<div style={{ fontSize: 10, color: '#87867f', fontFamily: "'JetBrains Mono', monospace" }}>{sh.ip}</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{sh.status && (
<span style={{ fontSize: 10, background: sh.status === 'healthy' ? 'rgba(34,197,94,0.12)' : 'rgba(204,34,0,0.1)', color: sh.status === 'healthy' ? '#22C55E' : '#cc2200', borderRadius: 4, padding: '2px 8px', fontWeight: 600 }}>
{sh.status}
</span>
)}
<button onClick={() => setSelectedHost(null)} style={{ border: 'none', background: 'none', cursor: 'pointer', fontSize: 14, color: '#87867f', padding: '0 2px' }}></button>
</div>
</div>
<div style={{ padding: '12px 14px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{/* 左Metrics 大字 + 服務清單 */}
<div>
<div style={{ display: 'flex', gap: 12, marginBottom: 10 }}>
{[{ label: 'CPU', val: sh.cpu }, { label: 'RAM', val: sh.ram }].map(m => {
const color = m.val != null ? (m.val > 80 ? '#cc2200' : m.val > 60 ? '#F59E0B' : '#22C55E') : '#b0ad9f'
return (
<div key={m.label} style={{ flex: 1, border: '0.5px solid #e0ddd4', borderRadius: 8, padding: '8px 10px', textAlign: 'center' }}>
<div style={{ fontSize: 9, color: '#87867f', fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5 }}>{m.label}</div>
<div style={{ fontSize: 24, fontWeight: 700, color, marginTop: 2 }}>{m.val != null ? `${m.val}%` : '--'}</div>
</div>
)
})}
</div>
{/* 服務清單 */}
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', color: '#87867f', marginBottom: 5 }}></div>
{sh.services.length === 0 ? (
<div style={{ fontSize: 11, color: '#b0ad9f' }}>--</div>
) : sh.services.map((svc: any, i: number) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '3px 0', borderBottom: '0.5px solid #f5f4ed', fontSize: 11 }}>
<div style={{ width: 5, height: 5, borderRadius: '50%', background: svc.status === 'up' ? '#22C55E' : '#cc2200', flexShrink: 0 }} />
<span style={{ flex: 1, color: '#141413' }}>{svc.name}</span>
{svc.port && <span style={{ color: '#87867f', fontFamily: "'JetBrains Mono', monospace", fontSize: 10 }}>:{svc.port}</span>}
{svc.latency_ms != null && <span style={{ color: '#87867f', fontSize: 10 }}>{svc.latency_ms}ms</span>}
</div>
))}
</div>
{/* 右:相關事件 */}
<div>
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', color: '#87867f', marginBottom: 5 }}></div>
{relatedIncidents.length === 0 ? (
<div style={{ fontSize: 11, color: '#b0ad9f' }}></div>
) : relatedIncidents.map((inc: any, i: number) => (
<div key={i} style={{ padding: '5px 8px', marginBottom: 4, borderRadius: 5, background: '#faf9f3', border: '0.5px solid #e0ddd4' }}>
<div style={{ fontSize: 10, fontWeight: 600, color: '#d97757', marginBottom: 2 }}>{inc.severity}</div>
<div style={{ fontSize: 11, color: '#141413', lineHeight: 1.3, fontFamily: "'JetBrains Mono', monospace" }}>{inc.incident_id}</div>
<div style={{ fontSize: 10, color: '#87867f', marginTop: 2 }}>{inc.status} · {inc.affected_services.slice(0,2).join(', ')}</div>
</div>
))}
</div>
</div>
</div>
)
})()}
</div>
{/* 監控工具 — figma-v2 style: 左彩色條 + 可點擊 + meta行 */}