infra-report/src/app/page.tsx

243 lines
9.0 KiB
TypeScript

'use client';
import { useRef, useState } from 'react';
import { useReport } from '@/hooks/useReport';
import ReportHeader from '@/components/report/ReportHeader';
import SummaryCards from '@/components/report/SummaryCards';
import TimeSeriesChart from '@/components/charts/TimeSeriesChart';
import NetworkChart from '@/components/charts/NetworkChart';
import NodeTable from '@/components/report/NodeTable';
import NasTable from '@/components/report/NasTable';
import NetworkTable from '@/components/report/NetworkTable';
import AlertPanel from '@/components/report/AlertPanel';
import NetworkAlertPanel from '@/components/report/NetworkAlertPanel';
import ErrorBoundary from '@/components/ErrorBoundary';
function getDefaultRange() {
const end = new Date();
const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000);
return {
start: start.toISOString().slice(0, 10),
end: end.toISOString().slice(0, 10),
};
}
export default function Home() {
const defaultRange = getDefaultRange();
const [startDate, setStartDate] = useState(defaultRange.start);
const [endDate, setEndDate] = useState(defaultRange.end);
const [appliedStart, setAppliedStart] = useState(defaultRange.start);
const [appliedEnd, setAppliedEnd] = useState(defaultRange.end);
const { data, isLoading, error } = useReport(appliedStart, appliedEnd);
const reportRef = useRef<HTMLDivElement>(null);
const isDirty = startDate !== appliedStart || endDate !== appliedEnd;
const applyRange = () => {
setAppliedStart(startDate);
setAppliedEnd(endDate);
};
const handleExportPdf = async () => {
if (!reportRef.current) return;
const html2canvas = (await import('html2canvas')).default;
const { default: jsPDF } = await import('jspdf');
const canvas = await html2canvas(reportRef.current, { scale: 2, useCORS: true });
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF('p', 'mm', 'a4');
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
let heightLeft = pdfHeight;
let position = 0;
pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pdfHeight);
heightLeft -= pdf.internal.pageSize.getHeight();
while (heightLeft > 0) {
position -= pdf.internal.pageSize.getHeight();
pdf.addPage();
pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pdfHeight);
heightLeft -= pdf.internal.pageSize.getHeight();
}
pdf.save(`infra-report-${startDate}_${endDate}.pdf`);
};
const setPresetRange = (days: number) => {
const end = new Date();
const start = new Date(end.getTime() - days * 24 * 60 * 60 * 1000);
setStartDate(start.toISOString().slice(0, 10));
setEndDate(end.toISOString().slice(0, 10));
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-gray-500"> ...</div>
</div>
);
}
if (error || !data) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-red-500"> : {String(error)}</div>
</div>
);
}
return (
<main className="min-h-screen bg-gray-100 p-6">
<div ref={reportRef} className="max-w-7xl mx-auto space-y-6">
<ErrorBoundary fallback="ReportHeader 오류">
<ReportHeader
start={data.period.start}
end={data.period.end}
onExportPdf={handleExportPdf}
/>
</ErrorBoundary>
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="px-3 py-1.5 text-sm border rounded bg-white"
/>
<span className="text-gray-500">~</span>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="px-3 py-1.5 text-sm border rounded bg-white"
/>
</div>
<div className="flex gap-1">
{[1, 3, 7, 14, 30].map((d) => (
<button
key={d}
onClick={() => setPresetRange(d)}
className="px-3 py-1.5 text-sm rounded bg-white text-gray-700 hover:bg-gray-50 border"
>
{d}
</button>
))}
</div>
<button
onClick={applyRange}
disabled={!isDirty}
className={`px-4 py-1.5 text-sm rounded font-medium ${
isDirty
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
</button>
</div>
<ErrorBoundary fallback="SummaryCards 오류">
<SummaryCards
cpu={data.metrics.cpu}
memory={data.metrics.memory}
disk={data.metrics.disk}
ingress={data.metrics.ingress}
istio={data.metrics.istio}
/>
</ErrorBoundary>
<div className="grid grid-cols-[1fr_280px] gap-4">
<ErrorBoundary fallback="CPU 차트 오류">
<TimeSeriesChart title="CPU 사용률" instances={data.metrics.cpu} unit="%" />
</ErrorBoundary>
<ErrorBoundary fallback="CPU 주의 구간 오류">
<AlertPanel instances={data.metrics.cpu} type="cpu" height={300} />
</ErrorBoundary>
</div>
<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 && (
<div className="grid grid-cols-[1fr_280px] gap-4">
<ErrorBoundary fallback="NAS 차트 오류">
<TimeSeriesChart title="NAS 사용률" instances={data.metrics.nas} unit="%" />
</ErrorBoundary>
<ErrorBoundary fallback="NAS 주의 구간 오류">
<AlertPanel instances={data.metrics.nas} type="nas" height={300} />
</ErrorBoundary>
</div>
)}
<div className="grid grid-cols-[1fr_280px] gap-4">
<ErrorBoundary fallback="Ingress Network 차트 오류">
<NetworkChart title="Ingress Network Traffic" data={data.metrics.ingress} />
</ErrorBoundary>
<ErrorBoundary fallback="Ingress 주의 구간 오류">
<NetworkAlertPanel data={data.metrics.ingress} height={300} />
</ErrorBoundary>
</div>
<div className="grid grid-cols-[1fr_280px] gap-4">
<ErrorBoundary fallback="Istio Network 차트 오류">
<NetworkChart title="Istio Network Traffic" data={data.metrics.istio} />
</ErrorBoundary>
<ErrorBoundary fallback="Istio 주의 구간 오류">
<NetworkAlertPanel data={data.metrics.istio} height={300} />
</ErrorBoundary>
</div>
<ErrorBoundary fallback="NodeTable 오류">
<div>
<h2 className="text-lg font-semibold text-gray-800 mb-3"> </h2>
<NodeTable
nodes={data.nodes}
cpu={data.metrics.cpu}
memory={data.metrics.memory}
disk={data.metrics.disk}
/>
</div>
</ErrorBoundary>
{data.metrics.nas && data.metrics.nas.length > 0 && (
<ErrorBoundary fallback="NAS 상세 오류">
<div>
<h2 className="text-lg font-semibold text-gray-800 mb-3">NAS </h2>
<NasTable nas={data.metrics.nas} />
</div>
</ErrorBoundary>
)}
<ErrorBoundary fallback="Ingress 상세 오류">
<div>
<h2 className="text-lg font-semibold text-gray-800 mb-3">Ingress Network </h2>
<NetworkTable title="Ingress" data={data.metrics.ingress} />
</div>
</ErrorBoundary>
<ErrorBoundary fallback="Istio 상세 오류">
<div>
<h2 className="text-lg font-semibold text-gray-800 mb-3">Istio Network </h2>
<NetworkTable title="Istio" data={data.metrics.istio} />
</div>
</ErrorBoundary>
</div>
</main>
);
}