添加服务状态和资源监控组件
This commit is contained in:
parent
6c954f6e57
commit
8a88fcde10
294
src/components/monitoring/ResourceMonitor.tsx
Normal file
294
src/components/monitoring/ResourceMonitor.tsx
Normal file
@ -0,0 +1,294 @@
|
||||
/**
|
||||
* 资源监控组件
|
||||
*
|
||||
* 显示系统资源使用情况,包括:
|
||||
* - CPU 使用率(带历史图表)
|
||||
* - 内存使用率(带历史图表)
|
||||
* - 磁盘使用率(带历史图表)
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { Cpu, HardDrive, MemoryStick, TrendingUp, TrendingDown, Minus } from 'lucide-react'
|
||||
import Card from '@/components/ui/Card'
|
||||
import { Title3, Body, Caption1 } from '@/components/ui/Typography'
|
||||
import ProgressBar from '@/components/ui/ProgressBar'
|
||||
|
||||
export interface ResourceData {
|
||||
current: number // 当前使用百分比
|
||||
history: number[] // 历史数据点(最近N个)
|
||||
total?: number // 总量(MB)
|
||||
used?: number // 已用(MB)
|
||||
free?: number // 空闲(MB)
|
||||
}
|
||||
|
||||
export interface ResourceMonitorProps {
|
||||
cpu?: ResourceData
|
||||
memory?: ResourceData
|
||||
disk?: ResourceData
|
||||
cpuCores?: number
|
||||
className?: string
|
||||
showHistory?: boolean
|
||||
historyPoints?: number
|
||||
}
|
||||
|
||||
export default function ResourceMonitor({
|
||||
cpu,
|
||||
memory,
|
||||
disk,
|
||||
cpuCores,
|
||||
className = '',
|
||||
showHistory = true,
|
||||
historyPoints = 20,
|
||||
}: ResourceMonitorProps) {
|
||||
return (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{/* CPU 监控 */}
|
||||
{cpu && (
|
||||
<ResourceCard
|
||||
icon={<Cpu className="w-5 h-5" />}
|
||||
title="CPU 使用率"
|
||||
subtitle={cpuCores ? `${cpuCores} 核心` : undefined}
|
||||
data={cpu}
|
||||
showHistory={showHistory}
|
||||
historyPoints={historyPoints}
|
||||
unit="%"
|
||||
warningThreshold={70}
|
||||
criticalThreshold={90}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 内存监控 */}
|
||||
{memory && (
|
||||
<ResourceCard
|
||||
icon={<MemoryStick className="w-5 h-5" />}
|
||||
title="内存使用率"
|
||||
subtitle={memory.total ? `${formatMB(memory.used || 0)} / ${formatMB(memory.total)}` : undefined}
|
||||
data={memory}
|
||||
showHistory={showHistory}
|
||||
historyPoints={historyPoints}
|
||||
unit="%"
|
||||
warningThreshold={75}
|
||||
criticalThreshold={90}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 磁盘监控 */}
|
||||
{disk && (
|
||||
<ResourceCard
|
||||
icon={<HardDrive className="w-5 h-5" />}
|
||||
title="磁盘使用率"
|
||||
subtitle={disk.total ? `${formatMB(disk.used || 0)} / ${formatMB(disk.total)}` : undefined}
|
||||
data={disk}
|
||||
showHistory={showHistory}
|
||||
historyPoints={historyPoints}
|
||||
unit="%"
|
||||
warningThreshold={80}
|
||||
criticalThreshold={95}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ResourceCardProps {
|
||||
icon: React.ReactNode
|
||||
title: string
|
||||
subtitle?: string
|
||||
data: ResourceData
|
||||
showHistory: boolean
|
||||
historyPoints: number
|
||||
unit: string
|
||||
warningThreshold: number
|
||||
criticalThreshold: number
|
||||
}
|
||||
|
||||
function ResourceCard({
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
data,
|
||||
showHistory,
|
||||
historyPoints,
|
||||
unit,
|
||||
warningThreshold,
|
||||
criticalThreshold,
|
||||
}: ResourceCardProps) {
|
||||
const getStatusType = (value: number) => {
|
||||
if (value >= criticalThreshold) return 'error'
|
||||
if (value >= warningThreshold) return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
const getTrend = () => {
|
||||
if (data.history.length < 2) return 'stable'
|
||||
const recent = data.history.slice(-5)
|
||||
const avg = recent.reduce((a, b) => a + b, 0) / recent.length
|
||||
const diff = data.current - avg
|
||||
if (diff > 5) return 'up'
|
||||
if (diff < -5) return 'down'
|
||||
return 'stable'
|
||||
}
|
||||
|
||||
const trend = getTrend()
|
||||
|
||||
return (
|
||||
<Card variant="subtle" className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div style={{ color: 'var(--color-label-secondary)' }}>{icon}</div>
|
||||
<div>
|
||||
<Title3>{title}</Title3>
|
||||
{subtitle && (
|
||||
<Caption1 style={{ color: 'var(--color-label-tertiary)' }}>
|
||||
{subtitle}
|
||||
</Caption1>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Body
|
||||
className="font-semibold"
|
||||
style={{
|
||||
color: getStatusType(data.current) === 'error'
|
||||
? 'var(--color-red)'
|
||||
: getStatusType(data.current) === 'warning'
|
||||
? 'var(--color-orange)'
|
||||
: 'var(--color-green)'
|
||||
}}
|
||||
>
|
||||
{data.current.toFixed(1)}{unit}
|
||||
</Body>
|
||||
{trend === 'up' && <TrendingUp className="w-4 h-4" style={{ color: 'var(--color-red)' }} />}
|
||||
{trend === 'down' && <TrendingDown className="w-4 h-4" style={{ color: 'var(--color-green)' }} />}
|
||||
{trend === 'stable' && <Minus className="w-4 h-4" style={{ color: 'var(--color-label-tertiary)' }} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 进度条 */}
|
||||
<ProgressBar
|
||||
value={data.current}
|
||||
type={getStatusType(data.current) as 'success' | 'warning' | 'error'}
|
||||
animated
|
||||
className="mb-2"
|
||||
/>
|
||||
|
||||
{/* 历史图表 */}
|
||||
{showHistory && data.history.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<MiniChart
|
||||
data={data.history.slice(-historyPoints)}
|
||||
warningThreshold={warningThreshold}
|
||||
criticalThreshold={criticalThreshold}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
interface MiniChartProps {
|
||||
data: number[]
|
||||
warningThreshold: number
|
||||
criticalThreshold: number
|
||||
}
|
||||
|
||||
function MiniChart({ data, warningThreshold, criticalThreshold }: MiniChartProps) {
|
||||
const height = 40
|
||||
const width = 200
|
||||
const padding = 2
|
||||
|
||||
if (data.length === 0) return null
|
||||
|
||||
const max = 100
|
||||
const min = 0
|
||||
const range = max - min
|
||||
|
||||
const points = data.map((value, index) => {
|
||||
const x = padding + (index / (data.length - 1 || 1)) * (width - padding * 2)
|
||||
const y = height - padding - ((value - min) / range) * (height - padding * 2)
|
||||
return `${x},${y}`
|
||||
}).join(' ')
|
||||
|
||||
const getColor = (value: number) => {
|
||||
if (value >= criticalThreshold) return 'var(--color-red)'
|
||||
if (value >= warningThreshold) return 'var(--color-orange)'
|
||||
return 'var(--color-green)'
|
||||
}
|
||||
|
||||
// 创建渐变区域
|
||||
const areaPoints = `${padding},${height - padding} ${points} ${width - padding},${height - padding}`
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<svg width="100%" height={height} viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none">
|
||||
{/* 警告线 */}
|
||||
<line
|
||||
x1={padding}
|
||||
y1={height - padding - (warningThreshold / 100) * (height - padding * 2)}
|
||||
x2={width - padding}
|
||||
y2={height - padding - (warningThreshold / 100) * (height - padding * 2)}
|
||||
stroke="var(--color-orange)"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="4,4"
|
||||
opacity="0.3"
|
||||
/>
|
||||
|
||||
{/* 危险线 */}
|
||||
<line
|
||||
x1={padding}
|
||||
y1={height - padding - (criticalThreshold / 100) * (height - padding * 2)}
|
||||
x2={width - padding}
|
||||
y2={height - padding - (criticalThreshold / 100) * (height - padding * 2)}
|
||||
stroke="var(--color-red)"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="4,4"
|
||||
opacity="0.3"
|
||||
/>
|
||||
|
||||
{/* 填充区域 */}
|
||||
<polygon
|
||||
points={areaPoints}
|
||||
fill={getColor(data[data.length - 1])}
|
||||
opacity="0.1"
|
||||
/>
|
||||
|
||||
{/* 折线 */}
|
||||
<polyline
|
||||
points={points}
|
||||
fill="none"
|
||||
stroke={getColor(data[data.length - 1])}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
{/* 当前值点 */}
|
||||
{data.length > 0 && (
|
||||
<circle
|
||||
cx={width - padding}
|
||||
cy={height - padding - ((data[data.length - 1] - min) / range) * (height - padding * 2)}
|
||||
r="3"
|
||||
fill={getColor(data[data.length - 1])}
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
<div className="flex justify-between mt-1">
|
||||
<Caption1 style={{ color: 'var(--color-label-quaternary)' }}>
|
||||
{data.length > 0 ? `最低 ${Math.min(...data).toFixed(0)}%` : ''}
|
||||
</Caption1>
|
||||
<Caption1 style={{ color: 'var(--color-label-quaternary)' }}>
|
||||
{data.length > 0 ? `最高 ${Math.max(...data).toFixed(0)}%` : ''}
|
||||
</Caption1>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatMB(mb: number): string {
|
||||
if (mb >= 1024) {
|
||||
return `${(mb / 1024).toFixed(1)} GB`
|
||||
}
|
||||
return `${mb.toFixed(0)} MB`
|
||||
}
|
||||
124
src/components/monitoring/ServiceStatusCard.tsx
Normal file
124
src/components/monitoring/ServiceStatusCard.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 服务状态卡片组件
|
||||
*
|
||||
* 显示单个服务的状态信息,包括:
|
||||
* - 服务名称和状态指示器
|
||||
* - 响应时间显示
|
||||
* - 错误信息显示
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { CheckCircle, XCircle, AlertTriangle, Loader2, Clock } from 'lucide-react'
|
||||
import Card from '@/components/ui/Card'
|
||||
import { Title3, Body, Caption1 } from '@/components/ui/Typography'
|
||||
import Badge from '@/components/ui/Badge'
|
||||
|
||||
export interface ServiceStatus {
|
||||
name: string
|
||||
status: 'healthy' | 'unhealthy' | 'degraded' | 'unknown' | 'loading'
|
||||
responseTime?: number // 毫秒
|
||||
lastChecked?: Date
|
||||
errorMessage?: string
|
||||
details?: Record<string, string | number | boolean>
|
||||
}
|
||||
|
||||
interface ServiceStatusCardProps {
|
||||
service: ServiceStatus
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function ServiceStatusCard({ service, className = '' }: ServiceStatusCardProps) {
|
||||
const getStatusIcon = () => {
|
||||
switch (service.status) {
|
||||
case 'healthy':
|
||||
return <CheckCircle className="w-6 h-6" style={{ color: 'var(--color-green)' }} />
|
||||
case 'unhealthy':
|
||||
return <XCircle className="w-6 h-6" style={{ color: 'var(--color-red)' }} />
|
||||
case 'degraded':
|
||||
return <AlertTriangle className="w-6 h-6" style={{ color: 'var(--color-orange)' }} />
|
||||
case 'loading':
|
||||
return <Loader2 className="w-6 h-6 animate-spin" style={{ color: 'var(--color-blue)' }} />
|
||||
default:
|
||||
return <AlertTriangle className="w-6 h-6" style={{ color: 'var(--color-label-tertiary)' }} />
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusBadge = () => {
|
||||
const statusMap = {
|
||||
healthy: { type: 'success' as const, label: '正常' },
|
||||
unhealthy: { type: 'error' as const, label: '异常' },
|
||||
degraded: { type: 'warning' as const, label: '降级' },
|
||||
unknown: { type: 'info' as const, label: '未知' },
|
||||
loading: { type: 'info' as const, label: '检查中' },
|
||||
}
|
||||
const config = statusMap[service.status]
|
||||
return <Badge type={config.type} variant="subtle">{config.label}</Badge>
|
||||
}
|
||||
|
||||
const getResponseTimeColor = () => {
|
||||
if (!service.responseTime) return 'var(--color-label-tertiary)'
|
||||
if (service.responseTime < 100) return 'var(--color-green)'
|
||||
if (service.responseTime < 500) return 'var(--color-orange)'
|
||||
return 'var(--color-red)'
|
||||
}
|
||||
|
||||
return (
|
||||
<Card variant="elevated" className={`p-4 ${className}`}>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{getStatusIcon()}
|
||||
<Title3>{service.name}</Title3>
|
||||
</div>
|
||||
{getStatusBadge()}
|
||||
</div>
|
||||
|
||||
{/* 响应时间 */}
|
||||
{service.responseTime !== undefined && (
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Clock className="w-4 h-4" style={{ color: 'var(--color-label-tertiary)' }} />
|
||||
<Body style={{ color: getResponseTimeColor() }}>
|
||||
{service.responseTime}ms
|
||||
</Body>
|
||||
<Caption1 style={{ color: 'var(--color-label-tertiary)' }}>
|
||||
响应时间
|
||||
</Caption1>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 最后检查时间 */}
|
||||
{service.lastChecked && (
|
||||
<Caption1 style={{ color: 'var(--color-label-tertiary)' }} className="mb-2">
|
||||
最后检查: {service.lastChecked.toLocaleTimeString()}
|
||||
</Caption1>
|
||||
)}
|
||||
|
||||
{/* 错误信息 */}
|
||||
{service.errorMessage && (
|
||||
<div
|
||||
className="mt-3 p-2 rounded-apple-sm"
|
||||
style={{ backgroundColor: 'var(--color-red-subtle)' }}
|
||||
>
|
||||
<Caption1 style={{ color: 'var(--color-red)' }}>
|
||||
{service.errorMessage}
|
||||
</Caption1>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 详细信息 */}
|
||||
{service.details && Object.keys(service.details).length > 0 && (
|
||||
<div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--color-separator)' }}>
|
||||
{Object.entries(service.details).map(([key, value]) => (
|
||||
<div key={key} className="flex justify-between items-center py-1">
|
||||
<Caption1 style={{ color: 'var(--color-label-tertiary)' }}>{key}</Caption1>
|
||||
<Caption1 style={{ color: 'var(--color-label-secondary)' }}>
|
||||
{typeof value === 'boolean' ? (value ? '是' : '否') : String(value)}
|
||||
</Caption1>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
9
src/components/monitoring/index.ts
Normal file
9
src/components/monitoring/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 监控组件导出
|
||||
*/
|
||||
|
||||
export { default as ServiceStatusCard } from './ServiceStatusCard'
|
||||
export type { ServiceStatus } from './ServiceStatusCard'
|
||||
|
||||
export { default as ResourceMonitor } from './ResourceMonitor'
|
||||
export type { ResourceData, ResourceMonitorProps } from './ResourceMonitor'
|
||||
Loading…
x
Reference in New Issue
Block a user