Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
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>
120 lines
4.9 KiB
TypeScript
120 lines
4.9 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* ServicesPanel — 服務目錄面板 (不含 AppLayout)
|
|
* ================================================
|
|
* Sprint 5: 從 /services/page.tsx 抽取
|
|
* 供原始頁面和整合頁面 (/observability) 共用
|
|
*
|
|
* 建立時間: 2026-04-09 (台北時區)
|
|
*/
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
|
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
|
|
|
|
interface ServiceItem {
|
|
name: string
|
|
host: string
|
|
status: string
|
|
cpu?: number
|
|
ram?: number
|
|
}
|
|
|
|
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'
|
|
}
|
|
|
|
export function ServicesPanel() {
|
|
const t = useTranslations('services')
|
|
const tc = useTranslations('common')
|
|
const [services, setServices] = useState<ServiceItem[]>([])
|
|
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 => {
|
|
const hosts: { name: string; services?: { name: string; status: string; cpu?: number; ram?: number }[] }[] = data?.hosts ?? []
|
|
const list: ServiceItem[] = []
|
|
hosts.forEach(h => {
|
|
(h.services ?? []).forEach(s => {
|
|
list.push({ name: s.name, host: h.name, status: s.status, cpu: s.cpu, ram: s.ram })
|
|
})
|
|
})
|
|
setServices(list)
|
|
})
|
|
.catch(() => setError(true))
|
|
.finally(() => setLoading(false))
|
|
}, [])
|
|
|
|
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>
|
|
|
|
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, overflow: 'hidden' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 700, padding: '10px 14px', borderBottom: '0.5px solid #e0ddd4', background: '#faf9f3', fontFamily: 'var(--font-body), monospace', color: '#141413' }}>
|
|
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', display: 'inline-block' }} />
|
|
{t('title')}
|
|
</div>
|
|
|
|
{loading && (
|
|
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
|
{tc('loading')}
|
|
</div>
|
|
)}
|
|
|
|
{!loading && error && (
|
|
<div style={{ padding: '32px', textAlign: 'center', color: '#f44336', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
|
{t('fetchError')}
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !error && services.length === 0 && (
|
|
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
|
{t('noServices')}
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !error && services.length > 0 && (
|
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
|
<thead>
|
|
<tr style={{ background: '#faf9f3' }}>
|
|
{[t('name'), t('host'), t('status'), t('cpu'), t('ram')].map(col => (
|
|
<th key={col} style={{ padding: '8px 14px', textAlign: 'left', fontWeight: 600, color: '#87867f', borderBottom: '0.5px solid #e0ddd4', fontSize: 11 }}>{col}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{services.map((s, i) => (
|
|
<tr key={i} style={{ borderBottom: '0.5px solid #e0ddd4' }}>
|
|
<td style={{ padding: '10px 14px', color: '#141413', fontWeight: 500 }}>{s.name}</td>
|
|
<td style={{ padding: '10px 14px', color: '#87867f' }}>{s.host}</td>
|
|
<td style={{ padding: '10px 14px' }}>
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
|
<span style={{ width: 6, height: 6, borderRadius: '50%', background: statusColor(s.status), display: 'inline-block' }} />
|
|
<span style={{ color: '#141413' }}>{s.status}</span>
|
|
</span>
|
|
</td>
|
|
<td style={{ padding: '10px 14px', color: '#141413' }}>{s.cpu != null ? `${s.cpu}%` : '--'}</td>
|
|
<td style={{ padding: '10px 14px', color: '#141413' }}>{s.ram != null ? `${s.ram}%` : '--'}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|