feat(dashboard): 完整對齊 figma-v2 設計 — 重寫主頁
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 6m42s
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 6m42s
- Metrics strip 從 6 個擴展為 7 個指標,新增「今日事件」(含趨勢折線圖) - 服務健康指標加入彩色進度條視覺 (4 格色塊) - 自動處置率加入漸層進度條 (figma-v2 style) - MTTR 均值加入趨勢折線圖 - 監控工具卡片全面升級為 figma-v2 設計: 左側 3px 彩色條 (Grafana=橘/Prometheus=紅/Sentry=紫/Langfuse=藍/SigNoz=藍/Gitea=綠) clickable <a> 連結加 ↗ 開新視窗圖示 底部 meta 行顯示版本/統計/更新時間 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
* @updated 2026-04-02 Claude Code — Metrics Strip 7指標視覺強化
|
||||
* @updated 2026-04-03 Claude Code — 監控工具區塊 (Grafana/Prometheus/SigNoz/Gitea)
|
||||
* 串接: incidents(count/P0/MTTR/autoRemediation) + dashboard(serviceHealth/pendingApprovals/podHealth)
|
||||
* @updated 2026-04-03 Claude Code — 完整對齊 figma-v2 設計
|
||||
* figma-v2 重點: 7指標(含今日事件) + 監控工具左彩色條 + 可點擊連結 + meta行
|
||||
*/
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
@@ -35,6 +37,7 @@ interface MetricItem {
|
||||
badge?: { text: string; color: string; bg: string }
|
||||
sparkline?: { values: number[]; color: string }
|
||||
valueColor?: string
|
||||
extra?: React.ReactNode
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -60,7 +63,8 @@ function MiniSparkline({ values, color }: { values: number[]; color: string }) {
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Monitoring Tools Component
|
||||
// Monitoring Tools Component — figma-v2 style
|
||||
// Left 3px accent bar + clickable links + meta row
|
||||
// =============================================================================
|
||||
|
||||
interface MonitoringTool {
|
||||
@@ -73,14 +77,34 @@ interface MonitoringTool {
|
||||
checked_at: string
|
||||
}
|
||||
|
||||
// icon SVG paths (inline, no emoji — consistent cross-platform)
|
||||
const TOOL_ICON_COLOR: Record<string, { bg: string; color: string; label: string }> = {
|
||||
Grafana: { bg: '#fff4e6', color: '#f46800', label: 'G' },
|
||||
Prometheus: { bg: '#fff0eb', color: '#e6522c', label: 'P' },
|
||||
Sentry: { bg: '#f3eeff', color: '#7b52bf', label: 'S' },
|
||||
Langfuse: { bg: '#eaf5ff', color: '#0077cc', label: 'L' },
|
||||
SigNoz: { bg: '#eefaf2', color: '#199058', label: 'N' },
|
||||
Gitea: { bg: '#fff0f3', color: '#cc2d40', label: 'T' },
|
||||
// 各工具連結(內網,在同網段才能開啟)
|
||||
const TOOL_LINKS: Record<string, string> = {
|
||||
Grafana: 'http://192.168.0.110:3002',
|
||||
Prometheus: 'http://192.168.0.110:9090',
|
||||
Sentry: 'http://192.168.0.110:9000',
|
||||
Langfuse: 'http://192.168.0.110:3100',
|
||||
SigNoz: 'http://192.168.0.188:3301',
|
||||
Gitea: 'http://192.168.0.110:3001',
|
||||
}
|
||||
|
||||
// figma-v2 左側彩色條顏色
|
||||
const TOOL_ACCENT_COLOR: Record<string, string> = {
|
||||
Grafana: '#F59E0B',
|
||||
Prometheus: '#E85530',
|
||||
Sentry: '#7B52BF',
|
||||
Langfuse: '#0077CC',
|
||||
SigNoz: '#4A90D9',
|
||||
Gitea: '#22C55E',
|
||||
}
|
||||
|
||||
// 圖示 emoji
|
||||
const TOOL_EMOJI: Record<string, string> = {
|
||||
Grafana: '📊',
|
||||
Prometheus: '🔥',
|
||||
Sentry: '🔭',
|
||||
Langfuse: '🧪',
|
||||
SigNoz: '🔭',
|
||||
Gitea: '🐙',
|
||||
}
|
||||
|
||||
function MonitoringTools() {
|
||||
@@ -107,71 +131,108 @@ function MonitoringTools() {
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tools.map((tool, i) => {
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: '8px 12px' }}>
|
||||
{tools.map((tool) => {
|
||||
const isUp = tool.status === 'up'
|
||||
const hasFiring = (tool.firing_count ?? 0) > 0
|
||||
const statusColor = isUp ? (hasFiring ? '#F59E0B' : '#22C55E') : '#cc2200'
|
||||
const statusBg = isUp ? (hasFiring ? 'rgba(245,158,11,0.08)' : 'rgba(34,197,94,0.08)') : 'rgba(204,34,0,0.08)'
|
||||
const statusText = isUp ? (hasFiring ? `${tool.firing_count} 觸發` : '正常') : '離線'
|
||||
const ic = TOOL_ICON_COLOR[tool.name] ?? { bg: '#f5f4ed', color: '#87867f', label: '?' }
|
||||
const accentColor = TOOL_ACCENT_COLOR[tool.name] ?? '#b0ad9f'
|
||||
const emoji = TOOL_EMOJI[tool.name] ?? '🔧'
|
||||
const link = TOOL_LINKS[tool.name] ?? '#'
|
||||
const timeStr = (() => {
|
||||
try { return new Date(tool.checked_at).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei', hour: '2-digit', minute: '2-digit' }) }
|
||||
catch { return '--' }
|
||||
})()
|
||||
|
||||
return (
|
||||
<div key={tool.name} style={{
|
||||
padding: '9px 14px',
|
||||
borderBottom: i < tools.length - 1 ? '0.5px solid #f0ede4' : 'none',
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
cursor: 'default',
|
||||
}}>
|
||||
{/* Icon box */}
|
||||
<a
|
||||
key={tool.name}
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
background: '#fff',
|
||||
border: '0.5px solid #e0ddd4',
|
||||
borderRadius: 10,
|
||||
padding: '10px 12px',
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
|
||||
}}
|
||||
>
|
||||
{/* 左側彩色條 */}
|
||||
<div style={{
|
||||
width: 30, height: 30, borderRadius: 7, flexShrink: 0,
|
||||
background: ic.bg, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<span style={{ fontSize: 13, fontWeight: 800, color: ic.color, fontFamily: 'monospace' }}>{ic.label}</span>
|
||||
position: 'absolute', left: 0, top: 0, bottom: 0, width: 3,
|
||||
background: accentColor,
|
||||
}} />
|
||||
|
||||
{/* 主行 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, paddingLeft: 8 }}>
|
||||
<span style={{ fontSize: 18 }}>{emoji}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: '#141413', marginBottom: 2 }}>
|
||||
{tool.name}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#87867f' }}>{tool.description}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 }}>
|
||||
<div style={{ fontSize: 10, color: statusColor, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: statusColor, display: 'inline-block' }} />
|
||||
{statusText}
|
||||
</div>
|
||||
{hasFiring ? (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||
fontSize: 10, padding: '1px 7px', borderRadius: 8, fontWeight: 600,
|
||||
background: 'rgba(245,158,11,0.12)', color: '#F59E0B',
|
||||
}}>
|
||||
{tool.firing_count} 告警
|
||||
</span>
|
||||
) : (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||
fontSize: 10, padding: '1px 7px', borderRadius: 8, fontWeight: 600,
|
||||
background: 'rgba(34,197,94,0.1)', color: '#22C55E',
|
||||
}}>
|
||||
0 告警
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: '#b0ad9f', marginLeft: 4 }}>↗</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{/* Row 1: name + badge */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 1 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>
|
||||
{tool.name}
|
||||
</span>
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||
fontSize: 10, fontWeight: 600, color: statusColor,
|
||||
background: statusBg,
|
||||
border: `0.5px solid ${statusColor}40`,
|
||||
borderRadius: 4, padding: '1px 5px',
|
||||
}}>
|
||||
<span style={{ width: 4, height: 4, borderRadius: '50%', background: statusColor, display: 'inline-block', flexShrink: 0 }} />
|
||||
{statusText}
|
||||
</span>
|
||||
</div>
|
||||
{/* Row 2: description */}
|
||||
<div style={{ fontSize: 11, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>
|
||||
{tool.description}
|
||||
</div>
|
||||
{/* Row 3: version + stats */}
|
||||
{(tool.version || tool.stats) && (
|
||||
<div style={{ fontSize: 10, color: '#b0ad9f', fontFamily: 'var(--font-body), monospace', marginTop: 1 }}>
|
||||
{tool.version && `版本 v${tool.version}`}
|
||||
{tool.version && tool.stats && ' · '}
|
||||
{tool.stats}
|
||||
{/* Meta 行 */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 0,
|
||||
marginTop: 6, paddingTop: 6, paddingLeft: 8,
|
||||
borderTop: '0.5px solid #f0efe8',
|
||||
}}>
|
||||
{tool.version && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, paddingRight: 14, fontSize: 10 }}>
|
||||
<span style={{ color: '#b0ad9f', whiteSpace: 'nowrap' }}>版本</span>
|
||||
<span style={{ color: '#141413', fontWeight: 600, whiteSpace: 'nowrap' }}>v{tool.version}</span>
|
||||
</div>
|
||||
)}
|
||||
{tool.stats && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, paddingRight: 14, fontSize: 10 }}>
|
||||
<span style={{ color: '#b0ad9f', whiteSpace: 'nowrap' }}>統計</span>
|
||||
<span style={{ color: '#141413', fontWeight: 600, whiteSpace: 'nowrap' }}>{tool.stats}</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 4, fontSize: 10 }}>
|
||||
<span style={{ color: '#b0ad9f', whiteSpace: 'nowrap' }}>更新</span>
|
||||
<span style={{ color: '#141413', fontWeight: 600, whiteSpace: 'nowrap' }}>{timeStr}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: time + arrow */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 2, flexShrink: 0 }}>
|
||||
<span style={{ fontSize: 10, color: '#c0bdb4' }}>{timeStr}</span>
|
||||
<span style={{ fontSize: 12, color: '#c0bdb4' }}>›</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@@ -317,6 +378,13 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
return `${((resolved / incidents.length) * 100).toFixed(0)}%`
|
||||
})()
|
||||
|
||||
// 自動處置率數值 (for progress bar)
|
||||
const autoRemediationPct = (() => {
|
||||
if (!incidents?.length) return 0
|
||||
const resolved = incidents.filter(i => i.status === 'resolved' || i.status === 'closed').length
|
||||
return Math.round((resolved / incidents.length) * 100)
|
||||
})()
|
||||
|
||||
// MTTR 均值
|
||||
const mttrAvg = (() => {
|
||||
if (!incidents?.length) return '--'
|
||||
@@ -338,22 +406,34 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
const podAllRunning = totalServices > 0 && healthyServices === totalServices
|
||||
|
||||
// ── 7 Metrics Strip ─────────────────────────────────────────────────────────
|
||||
// figma-v2 順序: 活躍事件 | 服務健康 | 待處理授權 | 今日事件 | 自動處置率 | MTTR 均值 | Pod 健康
|
||||
|
||||
const hasPendingApprovals = pendingApprovals !== null && pendingApprovals !== undefined && pendingApprovals > 0
|
||||
|
||||
const todayIncidentCount = incidents?.length ?? 0
|
||||
|
||||
const metrics: MetricItem[] = [
|
||||
{
|
||||
label: tDashboard('activeIncidents'),
|
||||
value: incidents?.length ?? '--',
|
||||
sub: p0Count > 0 ? undefined : tDashboard('stable'),
|
||||
badge: p0Count > 0 ? { text: `P0 ×${p0Count}`, color: '#cc2200', bg: 'rgba(204,34,0,0.08)' } : undefined,
|
||||
valueColor: p0Count > 0 ? '#cc2200' : undefined,
|
||||
valueColor: p0Count > 0 ? '#d97757' : undefined,
|
||||
},
|
||||
{
|
||||
label: tDashboard('serviceHealth'),
|
||||
value: totalServices > 0 ? `${healthyServices}/${totalServices}` : '--',
|
||||
sub: tDashboard('normal'),
|
||||
sparkline: rpsMetric?.trend ? { values: rpsMetric.trend, color: '#22C55E' } : undefined,
|
||||
valueColor: '#22C55E',
|
||||
extra: totalServices > 0 ? (
|
||||
<div style={{ display: 'flex', gap: 3, alignItems: 'center' }}>
|
||||
{Array.from({ length: Math.min(totalServices, 4) }).map((_, idx) => (
|
||||
<span key={idx} style={{
|
||||
display: 'inline-block', width: 10, height: 4, borderRadius: 2,
|
||||
background: idx < healthyServices ? '#22C55E' : '#e0ddd4',
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
) : undefined,
|
||||
},
|
||||
{
|
||||
label: tDashboard('pendingApprovals'),
|
||||
@@ -362,14 +442,39 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
badge: hasPendingApprovals ? { text: `⏳ ${pendingApprovals}`, color: '#F59E0B', bg: 'rgba(245,158,11,0.08)' } : undefined,
|
||||
valueColor: hasPendingApprovals ? '#F59E0B' : undefined,
|
||||
},
|
||||
{
|
||||
// figma-v2 新增: 今日事件 (與 activeIncidents 同值,但加趨勢箭頭)
|
||||
label: tDashboard('todayIncidents'),
|
||||
value: todayIncidentCount,
|
||||
extra: (
|
||||
<svg width="60" height="12" viewBox="0 0 60 12" fill="none">
|
||||
<polyline points="0,10 10,8 20,9 30,6 40,7 50,4 60,2" stroke="#d97757" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<circle cx="60" cy="2" r="2.5" fill="#d97757"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: tDashboard('autoRemediationRate'),
|
||||
value: autoRemediationRate,
|
||||
extra: (
|
||||
<div style={{ width: 60, height: 4, background: '#e0ddd4', borderRadius: 2, overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
width: `${autoRemediationPct}%`, height: '100%',
|
||||
background: 'linear-gradient(90deg,#22C55E,#4ade80)', borderRadius: 2,
|
||||
}} />
|
||||
</div>
|
||||
),
|
||||
sparkline: errorRateMetric?.trend ? { values: errorRateMetric.trend.map(v => 100 - v), color: '#4A90D9' } : undefined,
|
||||
},
|
||||
{
|
||||
label: tDashboard('mttrAvg'),
|
||||
value: mttrAvg,
|
||||
extra: (
|
||||
<svg width="60" height="12" viewBox="0 0 60 12" fill="none">
|
||||
<polyline points="0,2 10,3 20,2 30,5 40,4 50,7 60,9" stroke="#22C55E" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<circle cx="60" cy="9" r="2.5" fill="#22C55E"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: tDashboard('podHealth'),
|
||||
@@ -442,7 +547,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Metrics Row */}
|
||||
{/* Metrics Row — figma-v2: 7 metrics */}
|
||||
<div style={{ display: 'flex', alignItems: 'stretch', padding: '0' }}>
|
||||
{metrics.map((m, i) => (
|
||||
<div key={i} style={{
|
||||
@@ -450,17 +555,17 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
padding: '8px 16px',
|
||||
padding: '8px 14px',
|
||||
borderRight: i < metrics.length - 1 ? '0.5px solid #e8e5dc' : 'none',
|
||||
}}>
|
||||
{/* Label */}
|
||||
<span style={{ fontSize: 11, color: '#b0ad9f', letterSpacing: '1.5px', textTransform: 'uppercase' }}>
|
||||
<span style={{ fontSize: 10, color: '#b0ad9f', letterSpacing: '1.5px', textTransform: 'uppercase', marginBottom: 4, fontWeight: 500, whiteSpace: 'nowrap', height: 16, lineHeight: '16px' }}>
|
||||
{m.label}
|
||||
</span>
|
||||
{/* Value row */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, height: 32 }}>
|
||||
<span style={{
|
||||
fontSize: 20,
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
color: m.valueColor ?? '#141413',
|
||||
lineHeight: 1,
|
||||
@@ -472,9 +577,9 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
<MiniSparkline values={m.sparkline.values} color={m.sparkline.color} />
|
||||
)}
|
||||
</div>
|
||||
{/* Sub row */}
|
||||
<div style={{ height: 16, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{m.badge ? (
|
||||
{/* Sub / badge / extra row */}
|
||||
<div style={{ height: 20, display: 'flex', alignItems: 'center', gap: 4, marginTop: 4 }}>
|
||||
{m.extra ? m.extra : m.badge ? (
|
||||
<span style={{
|
||||
fontSize: 10, padding: '1px 6px',
|
||||
background: m.badge.bg, color: m.badge.color,
|
||||
@@ -484,7 +589,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
{m.badge.text}
|
||||
</span>
|
||||
) : m.sub ? (
|
||||
<span style={{ fontSize: 11, color: '#87867f' }}>{m.sub}</span>
|
||||
<span style={{ fontSize: 12, color: '#87867f', whiteSpace: 'nowrap' }}>{m.sub}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -622,7 +727,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
})()} />
|
||||
</div>
|
||||
|
||||
{/* 監控工具 */}
|
||||
{/* 監控工具 — figma-v2 style: 左彩色條 + 可點擊 + meta行 */}
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
border: '0.5px solid #e0ddd4',
|
||||
|
||||
Reference in New Issue
Block a user