Files
awoooi/apps/web/src/components/panels/DeploymentsPanel.tsx
OG T 7934ade3a6
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
refactor(web): 全部 13 Panel 抽取完成 + 整合頁面雙重 AppLayout 修正
Panel 抽取 (13 個):
- MonitoringPanel, ApmPanel, ErrorsPanel, AppsPanel, ServicesPanel
- AutoRepairPanel, NeuralCommandPanel, DriftPanel
- DeploymentsPanel, TicketsPanel, CostPanel, ActionLogsPanel, BillingPanel

整合頁面更新 (全部使用 Panel,無雙重 AppLayout):
- /observability: 5 Panel
- /automation: 3 Panel
- /operations: 5 Panel

首席架構師 I2 問題已解決

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:05:37 +08:00

114 lines
5.5 KiB
TypeScript

'use client'
/**
* DeploymentsPanel — K3s 部署狀態面板 (不含 AppLayout)
* =====================================================
* Sprint 5: 從 /deployments/page.tsx 抽取
* 供原始頁面和整合頁面 (/operations) 共用
*
* 建立時間: 2026-04-09 (台北時區)
*/
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
interface HostService {
name: string
status: string
port: number | null
latency_ms: number | null
}
interface Host {
ip: string
name: string
role: string
status: string
services: HostService[]
last_check: string
}
const STATUS_COLOR: Record<string, string> = {
up: '#22C55E',
healthy: '#22C55E',
down: '#cc2200',
degraded: '#F59E0B',
unreachable: '#87867f',
}
export function DeploymentsPanel() {
const t = useTranslations('deployments')
const [hosts, setHosts] = useState<Host[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetch(`${API_BASE}/api/v1/dashboard`)
.then(r => r.json())
.then(data => { setHosts(data.hosts ?? []); setLoading(false) })
.catch(err => { setError(String(err)); setLoading(false) })
}, [])
const k3sHosts = hosts.filter(h => h.role === 'k3s' || h.ip.includes('120'))
const displayHosts = k3sHosts.length > 0 ? k3sHosts : hosts
return (
<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>
{loading ? (
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('loading')}</div>
) : error ? (
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, padding: '32px', textAlign: 'center', color: '#cc2200', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('error')}</div>
) : displayHosts.length === 0 ? (
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noDeployments')}</div>
) : (
displayHosts.map(host => (
<div key={host.ip} style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, overflow: 'hidden', marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', borderBottom: '0.5px solid #e0ddd4', background: '#faf9f3' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: STATUS_COLOR[host.status] ?? '#87867f', display: 'inline-block' }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>{host.name}</span>
<span style={{ fontSize: 11, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>{host.ip}</span>
</div>
<span style={{ fontSize: 10, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>
{host.last_check ? new Date(host.last_check).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei' }) : '—'}
</span>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
<thead>
<tr style={{ background: '#faf9f3' }}>
{[t('service'), t('port'), t('latency'), t('status')].map(col => (
<th key={col} style={{ padding: '6px 14px', textAlign: 'left', fontWeight: 600, color: '#87867f', borderBottom: '0.5px solid #e0ddd4', fontSize: 11 }}>{col}</th>
))}
</tr>
</thead>
<tbody>
{host.services.length === 0 ? (
<tr><td colSpan={4} style={{ padding: '16px 14px', textAlign: 'center', color: '#87867f', fontSize: 12 }}>{t('noDeployments')}</td></tr>
) : host.services.map((s, i) => (
<tr key={i} style={{ borderBottom: '0.5px solid #f0ede4' }}>
<td style={{ padding: '7px 14px', fontWeight: 500, color: '#141413' }}>{s.name}</td>
<td style={{ padding: '7px 14px', color: '#87867f' }}>{s.port ?? '—'}</td>
<td style={{ padding: '7px 14px', color: '#87867f' }}>{s.latency_ms != null ? `${s.latency_ms.toFixed(0)}ms` : '—'}</td>
<td style={{ padding: '7px 14px' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 11, fontWeight: 600, textTransform: 'uppercase', color: STATUS_COLOR[s.status] ?? '#87867f' }}>
<span style={{ width: 5, height: 5, borderRadius: '50%', background: STATUS_COLOR[s.status] ?? '#87867f', display: 'inline-block' }} />
{s.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
))
)}
</div>
)
}