feat(web): 基礎架構主機卡點擊 → 詳情抽屜展開
點擊主機卡展開行內抽屜: - CPU/RAM 大字顯示(含顏色警示:>80% 紅/>60% 橙) - 完整服務清單(狀態點 + port + latency_ms) - 相關事件(按 affected_services 過濾) - ✕ 關閉 / 再點同卡收合 - 選中狀態:藍色邊框高亮 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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行 */}
|
||||
|
||||
Reference in New Issue
Block a user