mirror of
https://github.com/crocofied/CoreControl.git
synced 2025-12-17 15:36:50 +00:00
Update Uptime Pagination
This commit is contained in:
parent
e34407539a
commit
a320c04b92
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user