feat: move alert panels next to corresponding charts with scroll
CI/CD / build-and-push (push) Has been cancelled
Details
CI/CD / build-and-push (push) Has been cancelled
Details
- Replace bottom AlertSection with per-chart AlertPanel - Each chart row: chart (flex) + alert panel (280px) side by side - Alert panel matches chart height, scrollable when content overflows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
078b724779
commit
0cae92ed63
|
|
@ -9,7 +9,7 @@ import NetworkChart from '@/components/charts/NetworkChart';
|
||||||
import NodeTable from '@/components/report/NodeTable';
|
import NodeTable from '@/components/report/NodeTable';
|
||||||
import NasTable from '@/components/report/NasTable';
|
import NasTable from '@/components/report/NasTable';
|
||||||
import NetworkTable from '@/components/report/NetworkTable';
|
import NetworkTable from '@/components/report/NetworkTable';
|
||||||
import AlertSection from '@/components/report/AlertSection';
|
import AlertPanel from '@/components/report/AlertPanel';
|
||||||
import ErrorBoundary from '@/components/ErrorBoundary';
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||||
|
|
||||||
function getDefaultRange() {
|
function getDefaultRange() {
|
||||||
|
|
@ -146,15 +146,30 @@ export default function Home() {
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
||||||
<ErrorBoundary fallback="CPU 차트 오류">
|
<div className="grid grid-cols-[1fr_280px] gap-4">
|
||||||
<TimeSeriesChart title="CPU 사용률" instances={data.metrics.cpu} unit="%" />
|
<ErrorBoundary fallback="CPU 차트 오류">
|
||||||
</ErrorBoundary>
|
<TimeSeriesChart title="CPU 사용률" instances={data.metrics.cpu} unit="%" />
|
||||||
<ErrorBoundary fallback="Memory 차트 오류">
|
</ErrorBoundary>
|
||||||
<TimeSeriesChart title="Memory 사용률" instances={data.metrics.memory} unit="%" />
|
<ErrorBoundary fallback="CPU 주의 구간 오류">
|
||||||
</ErrorBoundary>
|
<AlertPanel instances={data.metrics.cpu} type="cpu" height={300} />
|
||||||
<ErrorBoundary fallback="Disk 차트 오류">
|
</ErrorBoundary>
|
||||||
<TimeSeriesChart title="Disk 사용률" instances={data.metrics.disk} unit="%" />
|
</div>
|
||||||
</ErrorBoundary>
|
<div className="grid grid-cols-[1fr_280px] gap-4">
|
||||||
|
<ErrorBoundary fallback="Memory 차트 오류">
|
||||||
|
<TimeSeriesChart title="Memory 사용률" instances={data.metrics.memory} unit="%" />
|
||||||
|
</ErrorBoundary>
|
||||||
|
<ErrorBoundary fallback="Memory 주의 구간 오류">
|
||||||
|
<AlertPanel instances={data.metrics.memory} type="memory" height={300} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-[1fr_280px] gap-4">
|
||||||
|
<ErrorBoundary fallback="Disk 차트 오류">
|
||||||
|
<TimeSeriesChart title="Disk 사용률" instances={data.metrics.disk} unit="%" />
|
||||||
|
</ErrorBoundary>
|
||||||
|
<ErrorBoundary fallback="Disk 주의 구간 오류">
|
||||||
|
<AlertPanel instances={data.metrics.disk} type="disk" height={300} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
|
||||||
{data.metrics.nas && data.metrics.nas.length > 0 && (
|
{data.metrics.nas && data.metrics.nas.length > 0 && (
|
||||||
<ErrorBoundary fallback="NAS 차트 오류">
|
<ErrorBoundary fallback="NAS 차트 오류">
|
||||||
|
|
@ -205,16 +220,6 @@ export default function Home() {
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
||||||
<ErrorBoundary fallback="AlertSection 오류">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold text-gray-800 mb-3">주의 구간</h2>
|
|
||||||
<AlertSection
|
|
||||||
cpu={data.metrics.cpu}
|
|
||||||
memory={data.metrics.memory}
|
|
||||||
disk={data.metrics.disk}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { MetricInstance } from '@/types/metrics';
|
||||||
|
import { formatTimestamp, formatPercent } from '@/lib/formatters';
|
||||||
|
|
||||||
|
interface AlertPanelProps {
|
||||||
|
instances: MetricInstance[];
|
||||||
|
type: 'cpu' | 'memory' | 'disk';
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Alert {
|
||||||
|
level: 'warning' | 'critical';
|
||||||
|
instance: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
instance: inst.instance,
|
||||||
|
message: `최대 ${formatPercent(inst.stats.max)} 도달`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
inst.peaks.peaks.forEach((peak) => {
|
||||||
|
alerts.push({
|
||||||
|
level: 'warning',
|
||||||
|
instance: inst.instance,
|
||||||
|
message: `${formatTimestamp(peak.startTime)} ~ ${formatTimestamp(peak.endTime)} Peak (max: ${formatPercent(peak.maxValue)})`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (type === 'memory') {
|
||||||
|
if (inst.stats.max > 85) {
|
||||||
|
alerts.push({
|
||||||
|
level: inst.stats.max > 95 ? 'critical' : 'warning',
|
||||||
|
instance: inst.instance,
|
||||||
|
message: `최대 ${formatPercent(inst.stats.max)} 도달`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
inst.peaks.peaks.forEach((peak) => {
|
||||||
|
alerts.push({
|
||||||
|
level: 'warning',
|
||||||
|
instance: inst.instance,
|
||||||
|
message: `${formatTimestamp(peak.startTime)} ~ ${formatTimestamp(peak.endTime)} Peak (max: ${formatPercent(peak.maxValue)})`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (type === 'disk') {
|
||||||
|
if (inst.stats.max > 80) {
|
||||||
|
alerts.push({
|
||||||
|
level: inst.stats.max > 90 ? 'critical' : 'warning',
|
||||||
|
instance: inst.instance,
|
||||||
|
message: `사용률 ${formatPercent(inst.stats.max)} (증설 검토 필요)`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return alerts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AlertPanel({ instances, type, height = 300 }: 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">
|
||||||
|
<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">
|
||||||
|
<span className="font-medium text-gray-800">{alert.instance}</span>
|
||||||
|
<p className="text-gray-500">{alert.message}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue