Update Uptime Pagination

This commit is contained in:
headlessdev 2025-04-15 14:09:36 +02:00
parent e34407539a
commit a320c04b92
2 changed files with 216 additions and 158 deletions

View File

@ -2,8 +2,10 @@ import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
interface RequestBody { interface RequestBody {
timespan?: number; timespan?: number;
} page?: number;
}
const getTimeRange = (timespan: number) => { const getTimeRange = (timespan: number) => {
const now = new Date(); const now = new Date();
@ -84,62 +86,77 @@ const getIntervalKey = (date: Date, timespan: number) => {
}; };
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const { timespan = 1 }: RequestBody = await request.json(); const { timespan = 1, page = 1 }: RequestBody = await request.json();
const { start } = getTimeRange(timespan); const itemsPerPage = 5;
const skip = (page - 1) * itemsPerPage;
const applications = await prisma.application.findMany();
const uptimeHistory = await prisma.uptime_history.findMany({ // Get paginated and sorted applications
where: { const [applications, totalCount] = await Promise.all([
applicationId: { in: applications.map(app => app.id) }, prisma.application.findMany({
createdAt: { gte: start } skip,
}, take: itemsPerPage,
orderBy: { createdAt: "desc" } orderBy: { name: 'asc' }
}); }),
prisma.application.count()
const intervals = generateIntervals(timespan); ]);
const result = applications.map(app => { const applicationIds = applications.map(app => app.id);
const appChecks = uptimeHistory.filter(check => check.applicationId === app.id);
const checksMap = new Map<string, { failed: number; total: number }>(); // Get time range and intervals
const { start } = getTimeRange(timespan);
for (const check of appChecks) { const intervals = generateIntervals(timespan);
const intervalKey = getIntervalKey(check.createdAt, timespan);
const current = checksMap.get(intervalKey) || { failed: 0, total: 0 }; // Get uptime history for the filtered applications
current.total++; const uptimeHistory = await prisma.uptime_history.findMany({
if (!check.online) current.failed++; where: {
checksMap.set(intervalKey, current); applicationId: { in: applicationIds },
} createdAt: { gte: start }
},
const uptimeSummary = intervals.map(interval => { orderBy: { createdAt: "desc" }
const intervalKey = getIntervalKey(interval, timespan); });
const stats = checksMap.get(intervalKey);
// Process data for each application
if (!stats) { const result = applications.map(app => {
return { const appChecks = uptimeHistory.filter(check => check.applicationId === app.id);
timestamp: interval.toISOString(), const checksMap = new Map<string, { failed: number; total: number }>();
missing: true,
online: null for (const check of appChecks) {
}; const intervalKey = getIntervalKey(check.createdAt, timespan);
const current = checksMap.get(intervalKey) || { failed: 0, total: 0 };
current.total++;
if (!check.online) current.failed++;
checksMap.set(intervalKey, current);
} }
const uptimeSummary = intervals.map(interval => {
const intervalKey = getIntervalKey(interval, timespan);
const stats = checksMap.get(intervalKey);
return {
timestamp: intervalKey,
missing: !stats,
online: stats ? stats.failed < 3 : null
};
});
return { return {
timestamp: intervalKey, appName: app.name,
missing: false, appId: app.id,
online: stats.failed < 3 uptimeSummary
}; };
}); });
return { return NextResponse.json({
appName: app.name, data: result,
appId: app.id, pagination: {
uptimeSummary currentPage: page,
}; totalPages: Math.ceil(totalCount / itemsPerPage),
}); totalItems: totalCount
}
return NextResponse.json(result); });
} catch (error: unknown) { } catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 500 }); return NextResponse.json({ error: message }, { status: 500 });
} }
} }

View File

@ -63,41 +63,62 @@ interface UptimeData {
}[]; }[];
} }
interface PaginationData {
currentPage: number;
totalPages: number;
totalItems: number;
}
export default function Uptime() { export default function Uptime() {
const [data, setData] = useState<UptimeData[]>([]); const [data, setData] = useState<UptimeData[]>([]);
const [timespan, setTimespan] = useState<1 | 2 | 3>(1); const [timespan, setTimespan] = useState<1 | 2 | 3>(1);
const [currentPage, setCurrentPage] = useState(1); const [pagination, setPagination] = useState<PaginationData>({
const itemsPerPage = 5; currentPage: 1,
totalPages: 1,
totalItems: 0
});
const [isLoading, setIsLoading] = useState(false);
const maxPage = Math.ceil(data.length / itemsPerPage); const getData = async (selectedTimespan: number, page: number) => {
const paginatedData = data.slice( setIsLoading(true);
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
const getData = async (selectedTimespan: number) => {
try { try {
const response = await axios.post<UptimeData[]>("/api/applications/uptime", { const response = await axios.post<{
timespan: selectedTimespan data: UptimeData[];
pagination: PaginationData;
}>("/api/applications/uptime", {
timespan: selectedTimespan,
page
}); });
setData(response.data);
setCurrentPage(1); setData(response.data.data);
setPagination(response.data.pagination);
} catch (error) { } catch (error) {
console.error("Error:", error); console.error("Error:", error);
setData([]); setData([]);
setPagination({
currentPage: 1,
totalPages: 1,
totalItems: 0
});
} finally {
setIsLoading(false);
} }
}; };
const handlePrevious = () => { const handlePrevious = () => {
setCurrentPage((prev) => Math.max(1, prev - 1)); const newPage = Math.max(1, pagination.currentPage - 1);
setPagination(prev => ({...prev, currentPage: newPage}));
getData(timespan, newPage);
}; };
const handleNext = () => { const handleNext = () => {
setCurrentPage((prev) => Math.min(maxPage, prev + 1)); const newPage = Math.min(pagination.totalPages, pagination.currentPage + 1);
setPagination(prev => ({...prev, currentPage: newPage}));
getData(timespan, newPage);
}; };
useEffect(() => { useEffect(() => {
getData(timespan); getData(timespan, 1);
}, [timespan]); }, [timespan]);
return ( return (
@ -130,7 +151,11 @@ export default function Uptime() {
<span className="text-2xl font-semibold">Uptime</span> <span className="text-2xl font-semibold">Uptime</span>
<Select <Select
value={String(timespan)} value={String(timespan)}
onValueChange={(v) => setTimespan(Number(v) as 1 | 2 | 3)} onValueChange={(v) => {
setTimespan(Number(v) as 1 | 2 | 3);
setPagination(prev => ({...prev, currentPage: 1}));
}}
disabled={isLoading}
> >
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select timespan" /> <SelectValue placeholder="Select timespan" />
@ -142,115 +167,131 @@ export default function Uptime() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="pt-4 space-y-4">
{paginatedData.map((app) => {
const reversedSummary = [...app.uptimeSummary].reverse();
const startTime = reversedSummary[0]?.timestamp;
const endTime = reversedSummary[reversedSummary.length - 1]?.timestamp;
return ( <div className="pt-4 space-y-4">
<Card key={app.appId}> {isLoading ? (
<CardHeader> <div className="text-center py-8">Loading...</div>
<div className="flex flex-col gap-4"> ) : (
<div className="flex justify-between items-center"> data.map((app) => {
<span className="text-lg font-semibold">{app.appName}</span> const reversedSummary = [...app.uptimeSummary].reverse();
</div> const startTime = reversedSummary[0]?.timestamp;
const endTime = reversedSummary[reversedSummary.length - 1]?.timestamp;
<div className="flex flex-col gap-2">
<div className="flex justify-between text-sm text-muted-foreground"> return (
<span>{startTime ? timeFormats[timespan](startTime) : ""}</span> <Card key={app.appId}>
<span>{endTime ? timeFormats[timespan](endTime) : ""}</span> <CardHeader>
<div className="flex flex-col gap-4">
<div className="flex justify-between items-center">
<span className="text-lg font-semibold">{app.appName}</span>
</div> </div>
<Tooltip.Provider> <div className="flex flex-col gap-2">
<div <div className="flex justify-between text-sm text-muted-foreground">
className="grid gap-0.5 w-full overflow-x-auto pb-2" <span>{startTime ? timeFormats[timespan](startTime) : ""}</span>
style={{ <span>{endTime ? timeFormats[timespan](endTime) : ""}</span>
gridTemplateColumns: `repeat(${gridColumns[timespan]}, minmax(0, 1fr))`,
minWidth: `${gridColumns[timespan] * 24}px`
}}
>
{reversedSummary.map((entry) => (
<Tooltip.Root key={entry.timestamp}>
<Tooltip.Trigger asChild>
<div
className={`h-8 w-full rounded-sm border transition-colors ${
entry.missing
? "bg-gray-300 border-gray-400"
: entry.online
? "bg-green-500 border-green-600"
: "bg-red-500 border-red-600"
}`}
/>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
className="rounded bg-gray-900 px-2 py-1 text-white text-xs shadow-lg"
side="top"
>
<div className="flex flex-col gap-1">
<p className="font-medium">
{timespan === 2 ? (
timeFormats[2](entry.timestamp)
) : (
new Date(entry.timestamp).toLocaleString([], {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: timespan === 3 ? undefined : '2-digit',
hour12: false
})
)}
</p>
<p>
{entry.missing
? "No data"
: entry.online
? "Online"
: "Offline"}
</p>
</div>
<Tooltip.Arrow className="fill-gray-900" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
))}
</div> </div>
</Tooltip.Provider>
<Tooltip.Provider>
<div
className="grid gap-0.5 w-full overflow-x-auto pb-2"
style={{
gridTemplateColumns: `repeat(${gridColumns[timespan]}, minmax(0, 1fr))`,
minWidth: `${gridColumns[timespan] * 24}px`
}}
>
{reversedSummary.map((entry) => (
<Tooltip.Root key={entry.timestamp}>
<Tooltip.Trigger asChild>
<div
className={`h-8 w-full rounded-sm border transition-colors ${
entry.missing
? "bg-gray-300 border-gray-400"
: entry.online
? "bg-green-500 border-green-600"
: "bg-red-500 border-red-600"
}`}
/>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
className="rounded bg-gray-900 px-2 py-1 text-white text-xs shadow-lg"
side="top"
>
<div className="flex flex-col gap-1">
<p className="font-medium">
{timespan === 2 ? (
timeFormats[2](entry.timestamp)
) : (
new Date(entry.timestamp).toLocaleString([], {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: timespan === 3 ? undefined : '2-digit',
hour12: false
})
)}
</p>
<p>
{entry.missing
? "No data"
: entry.online
? "Online"
: "Offline"}
</p>
</div>
<Tooltip.Arrow className="fill-gray-900" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
))}
</div>
</Tooltip.Provider>
</div>
</div> </div>
</div> </CardHeader>
</CardHeader> </Card>
</Card> );
); })
})} )}
</div> </div>
{data.length > 0 && ( {pagination.totalItems > 0 && !isLoading && (
<div className="pt-4 pb-4"> <div className="pt-4 pb-4">
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
<PaginationPrevious <PaginationPrevious
onClick={handlePrevious} onClick={handlePrevious}
aria-disabled={currentPage === 1} aria-disabled={pagination.currentPage === 1 || isLoading}
className={currentPage === 1 ? "opacity-50 cursor-not-allowed" : ""} className={
pagination.currentPage === 1 || isLoading
? "opacity-50 cursor-not-allowed"
: "hover:cursor-pointer"
}
/> />
</PaginationItem> </PaginationItem>
<PaginationItem> <PaginationItem>
<span className="px-4"> <span className="px-4">
Page {currentPage} of {maxPage} Page {pagination.currentPage} of {pagination.totalPages}
</span> </span>
</PaginationItem> </PaginationItem>
<PaginationItem> <PaginationItem>
<PaginationNext <PaginationNext
onClick={handleNext} onClick={handleNext}
aria-disabled={currentPage === maxPage} aria-disabled={pagination.currentPage === pagination.totalPages || isLoading}
className={currentPage === maxPage ? "opacity-50 cursor-not-allowed" : ""} className={
pagination.currentPage === pagination.totalPages || isLoading
? "opacity-50 cursor-not-allowed"
: "hover:cursor-pointer"
}
/> />
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>
</Pagination> </Pagination>
<div className="text-center text-sm text-muted-foreground mt-2">
Showing {data.length} of {pagination.totalItems} applications
</div>
</div> </div>
)} )}
</div> </div>