124 lines
4.1 KiB
TypeScript
124 lines
4.1 KiB
TypeScript
'use client';
|
|
|
|
import type { MetricInstance } from '@/types/metrics';
|
|
import { formatTimestamp, formatPercent } from '@/lib/formatters';
|
|
|
|
interface AlertPanelProps {
|
|
instances: MetricInstance[];
|
|
type: 'cpu' | 'memory' | 'disk' | 'nas';
|
|
height?: number;
|
|
onZoomRange?: (startMs: number, endMs: number) => void;
|
|
}
|
|
|
|
interface Alert {
|
|
level: 'warning' | 'critical';
|
|
category: 'threshold' | 'peak';
|
|
instance: string;
|
|
message: string;
|
|
startMs?: number;
|
|
endMs?: number;
|
|
}
|
|
|
|
function generateAlerts(instances: MetricInstance[], type: string): Alert[] {
|
|
const alerts: Alert[] = [];
|
|
|
|
instances.forEach((inst) => {
|
|
if (type === 'cpu') {
|
|
if (inst.stats.max > 90) {
|
|
alerts.push({
|
|
level: 'critical',
|
|
category: 'threshold',
|
|
instance: inst.instance,
|
|
message: `최대 ${formatPercent(inst.stats.max)} 도달`,
|
|
});
|
|
}
|
|
} else if (type === 'memory') {
|
|
if (inst.stats.max > 80) {
|
|
alerts.push({
|
|
level: inst.stats.max > 90 ? 'critical' : 'warning',
|
|
category: 'threshold',
|
|
instance: inst.instance,
|
|
message: `최대 ${formatPercent(inst.stats.max)} 도달`,
|
|
});
|
|
}
|
|
} else if (type === 'disk' || type === 'nas') {
|
|
if (inst.stats.max > 80) {
|
|
alerts.push({
|
|
level: inst.stats.max > 90 ? 'critical' : 'warning',
|
|
category: 'threshold',
|
|
instance: inst.instance,
|
|
message: `사용률 ${formatPercent(inst.stats.max)} (증설 검토 필요)`,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Z-score peak alerts for all types
|
|
inst.peaks.peaks.forEach((peak) => {
|
|
alerts.push({
|
|
level: 'warning',
|
|
category: 'peak',
|
|
instance: inst.instance,
|
|
message: `${formatTimestamp(peak.startTime)} ~ ${formatTimestamp(peak.endTime)} Peak (max: ${formatPercent(peak.maxValue)})`,
|
|
startMs: peak.startTime * 1000,
|
|
endMs: peak.endTime * 1000,
|
|
});
|
|
});
|
|
});
|
|
|
|
return alerts;
|
|
}
|
|
|
|
export default function AlertPanel({ instances, type, height = 300, onZoomRange }: AlertPanelProps) {
|
|
const alerts = generateAlerts(instances, type);
|
|
|
|
if (alerts.length === 0) {
|
|
return (
|
|
<div className="bg-green-50 border border-green-200 rounded-lg p-3 flex items-center justify-center" style={{ height }}>
|
|
<p className="text-green-700 text-xs">특이사항 없음</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white border rounded-lg flex flex-col" style={{ height }}>
|
|
<div className="px-3 py-2 border-b bg-gray-50 flex-shrink-0">
|
|
<h4 className="text-xs font-medium text-gray-600">주의 구간 ({alerts.length}건)</h4>
|
|
</div>
|
|
<ul className="divide-y divide-gray-100 overflow-y-auto flex-1">
|
|
{alerts.map((alert, idx) => (
|
|
<li
|
|
key={idx}
|
|
className={`px-3 py-2 flex items-start gap-2 ${alert.startMs && onZoomRange ? 'cursor-pointer hover:bg-blue-50' : ''}`}
|
|
onClick={() => {
|
|
if (alert.startMs && alert.endMs && onZoomRange) {
|
|
onZoomRange(alert.startMs, alert.endMs);
|
|
}
|
|
}}
|
|
>
|
|
<span
|
|
className={`mt-1 inline-block w-1.5 h-1.5 rounded-full flex-shrink-0 ${
|
|
alert.level === 'critical' ? 'bg-red-500' : 'bg-yellow-500'
|
|
}`}
|
|
/>
|
|
<div className="text-xs flex-1">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="font-medium text-gray-800">{alert.instance}</span>
|
|
<span className={`inline-block px-1.5 py-0.5 rounded text-[10px] font-medium ${
|
|
alert.category === 'peak'
|
|
? 'bg-purple-100 text-purple-700'
|
|
: alert.level === 'critical'
|
|
? 'bg-red-100 text-red-700'
|
|
: 'bg-orange-100 text-orange-700'
|
|
}`}>
|
|
{alert.category === 'peak' ? 'PEAK' : alert.level === 'critical' ? 'CRITICAL' : 'WARNING'}
|
|
</span>
|
|
</div>
|
|
<p className="text-gray-500">{alert.message}</p>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|