434 lines
17 KiB
TypeScript
Raw Normal View History

2025-04-15 12:22:15 +02:00
import { AppSidebar } from "@/components/app-sidebar";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
2025-04-15 13:46:25 +02:00
import axios from "axios";
2025-04-15 12:22:15 +02:00
import { Card, CardHeader } from "@/components/ui/card";
2025-04-15 13:46:25 +02:00
import * as Tooltip from "@radix-ui/react-tooltip";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
2025-04-15 13:58:53 +02:00
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationPrevious,
PaginationNext,
2025-04-15 16:33:12 +02:00
PaginationLink,
2025-04-15 13:58:53 +02:00
} from "@/components/ui/pagination";
2025-04-25 23:34:52 +02:00
import { useState, useEffect, useRef } from "react";
import Cookies from "js-cookie";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
import { Toaster } from "@/components/ui/sonner";
2025-04-29 19:38:48 +02:00
import { useTranslations } from "next-intl";
2025-04-15 12:22:15 +02:00
2025-04-15 13:46:25 +02:00
const timeFormats = {
1: (timestamp: string) =>
new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
hour12: false
}),
2: (timestamp: string) =>
new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
hour12: false
}),
2025-04-15 13:46:25 +02:00
3: (timestamp: string) =>
new Date(timestamp).toLocaleDateString([], {
day: '2-digit',
month: 'short'
}),
4: (timestamp: string) =>
2025-04-15 13:46:25 +02:00
new Date(timestamp).toLocaleDateString([], {
day: '2-digit',
month: 'short'
})
};
2025-04-15 14:21:51 +02:00
const minBoxWidths = {
1: 20,
2: 20,
3: 24,
4: 24
2025-04-15 13:46:25 +02:00
};
2025-04-15 12:22:15 +02:00
2025-04-15 13:52:46 +02:00
interface UptimeData {
2025-04-15 13:58:53 +02:00
appName: string;
appId: number;
uptimeSummary: {
timestamp: string;
missing: boolean;
online: boolean | null;
}[];
}
2025-04-15 14:09:36 +02:00
interface PaginationData {
currentPage: number;
totalPages: number;
totalItems: number;
}
2025-04-15 12:22:15 +02:00
export default function Uptime() {
2025-04-29 19:38:48 +02:00
const t = useTranslations();
2025-04-15 13:58:53 +02:00
const [data, setData] = useState<UptimeData[]>([]);
const [timespan, setTimespan] = useState<1 | 2 | 3 | 4>(1);
2025-04-15 14:09:36 +02:00
const [pagination, setPagination] = useState<PaginationData>({
currentPage: 1,
totalPages: 1,
totalItems: 0
});
const [isLoading, setIsLoading] = useState(false);
2025-04-25 23:34:52 +02:00
const savedItemsPerPage = Cookies.get("itemsPerPage-uptime");
const defaultItemsPerPage = 5;
const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage;
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
const customInputRef = useRef<HTMLInputElement>(null);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
2025-04-15 13:58:53 +02:00
2025-04-25 23:34:52 +02:00
const getData = async (selectedTimespan: number, page: number, itemsPerPage: number) => {
2025-04-15 14:09:36 +02:00
setIsLoading(true);
2025-04-15 13:58:53 +02:00
try {
2025-04-15 14:09:36 +02:00
const response = await axios.post<{
data: UptimeData[];
pagination: PaginationData;
}>("/api/applications/uptime", {
timespan: selectedTimespan,
2025-04-25 23:34:52 +02:00
page,
itemsPerPage
2025-04-15 13:58:53 +02:00
});
2025-04-15 14:09:36 +02:00
setData(response.data.data);
setPagination(response.data.pagination);
2025-04-15 13:58:53 +02:00
} catch (error) {
console.error("Error:", error);
setData([]);
2025-04-15 14:09:36 +02:00
setPagination({
currentPage: 1,
totalPages: 1,
totalItems: 0
});
} finally {
setIsLoading(false);
2025-04-15 13:58:53 +02:00
}
};
const handlePrevious = () => {
2025-04-15 14:09:36 +02:00
const newPage = Math.max(1, pagination.currentPage - 1);
setPagination(prev => ({...prev, currentPage: newPage}));
2025-04-25 23:34:52 +02:00
getData(timespan, newPage, itemsPerPage);
2025-04-15 13:58:53 +02:00
};
2025-04-15 13:46:25 +02:00
2025-04-15 13:58:53 +02:00
const handleNext = () => {
2025-04-15 14:09:36 +02:00
const newPage = Math.min(pagination.totalPages, pagination.currentPage + 1);
setPagination(prev => ({...prev, currentPage: newPage}));
2025-04-25 23:34:52 +02:00
getData(timespan, newPage, itemsPerPage);
};
const handleItemsPerPageChange = (value: string) => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
const newItemsPerPage = parseInt(value);
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
2025-04-29 19:38:48 +02:00
toast.error(t('Uptime.Messages.NumberValidation'));
2025-04-25 23:34:52 +02:00
return;
}
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
setItemsPerPage(validatedValue);
2025-04-29 19:38:48 +02:00
setPagination(prev => ({...prev, currentPage: 1}));
2025-04-25 23:34:52 +02:00
Cookies.set("itemsPerPage-uptime", String(validatedValue), {
expires: 365,
path: "/",
sameSite: "strict",
});
getData(timespan, 1, validatedValue);
2025-04-29 19:38:48 +02:00
}, 300);
2025-04-15 13:58:53 +02:00
};
2025-04-15 13:46:25 +02:00
useEffect(() => {
2025-04-25 23:34:52 +02:00
getData(timespan, 1, itemsPerPage);
2025-04-15 13:46:25 +02:00
}, [timespan]);
2025-04-15 12:22:15 +02:00
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
2025-04-15 12:22:15 +02:00
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
2025-04-15 13:46:25 +02:00
<BreadcrumbPage>/</BreadcrumbPage>
2025-04-15 12:22:15 +02:00
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
2025-04-29 19:38:48 +02:00
<BreadcrumbPage>{t('Uptime.Breadcrumb.MyInfrastructure')}</BreadcrumbPage>
2025-04-15 12:22:15 +02:00
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
2025-04-29 19:38:48 +02:00
<BreadcrumbPage>{t('Uptime.Breadcrumb.Uptime')}</BreadcrumbPage>
2025-04-15 12:22:15 +02:00
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
2025-04-25 23:34:52 +02:00
<Toaster />
<div className="p-6">
2025-04-15 13:46:25 +02:00
<div className="flex justify-between items-center">
2025-04-29 19:38:48 +02:00
<span className="text-3xl font-bold">{t('Uptime.Title')}</span>
2025-04-25 23:34:52 +02:00
<div className="flex gap-2">
<Select
value={String(itemsPerPage)}
onValueChange={handleItemsPerPageChange}
onOpenChange={(open) => {
if (open && customInputRef.current) {
customInputRef.current.value = String(itemsPerPage);
}
}}
>
<SelectTrigger className="w-[140px]">
<SelectValue>
2025-04-29 19:38:48 +02:00
{itemsPerPage} {itemsPerPage === 1 ? t('Common.ItemsPerPage.item') : t('Common.ItemsPerPage.items')}
2025-04-25 23:34:52 +02:00
</SelectValue>
</SelectTrigger>
<SelectContent>
{![5, 10, 15, 20, 25].includes(itemsPerPage) ? (
<SelectItem value={String(itemsPerPage)}>
2025-04-29 19:38:48 +02:00
{itemsPerPage} {itemsPerPage === 1 ? t('Common.ItemsPerPage.item') : t('Common.ItemsPerPage.items')} (custom)
2025-04-25 23:34:52 +02:00
</SelectItem>
) : null}
2025-04-29 19:38:48 +02:00
<SelectItem value="5">{t('Common.ItemsPerPage.5')}</SelectItem>
<SelectItem value="10">{t('Common.ItemsPerPage.10')}</SelectItem>
<SelectItem value="15">{t('Common.ItemsPerPage.15')}</SelectItem>
<SelectItem value="20">{t('Common.ItemsPerPage.20')}</SelectItem>
<SelectItem value="25">{t('Common.ItemsPerPage.25')}</SelectItem>
2025-04-25 23:34:52 +02:00
<div className="p-2 border-t mt-1">
2025-04-29 19:38:48 +02:00
<Label htmlFor="custom-items" className="text-xs font-medium">{t('Common.ItemsPerPage.Custom')}</Label>
2025-04-25 23:34:52 +02:00
<div className="flex items-center gap-2 mt-1">
<Input
id="custom-items"
ref={customInputRef}
type="number"
min="1"
max="100"
className="h-8"
defaultValue={itemsPerPage}
onChange={(e) => {
const value = parseInt(e.target.value);
if (isNaN(value) || value < 1 || value > 100) {
e.target.classList.add("border-red-500");
} else {
e.target.classList.remove("border-red-500");
}
}}
onBlur={(e) => {
const value = parseInt(e.target.value);
if (value >= 1 && value <= 100) {
handleItemsPerPageChange(e.target.value);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
const value = parseInt((e.target as HTMLInputElement).value);
if (value >= 1 && value <= 100) {
const validatedValue = Math.min(Math.max(value, 1), 100);
setItemsPerPage(validatedValue);
setPagination(prev => ({...prev, currentPage: 1}));
Cookies.set("itemsPerPage-uptime", String(validatedValue), {
expires: 365,
path: "/",
sameSite: "strict",
});
getData(timespan, 1, validatedValue);
document.body.click();
}
}
}}
onClick={(e) => e.stopPropagation()}
/>
2025-04-29 19:38:48 +02:00
<span className="text-xs text-muted-foreground whitespace-nowrap">{t('Common.ItemsPerPage.items')}</span>
2025-04-25 23:34:52 +02:00
</div>
</div>
</SelectContent>
</Select>
<Select
value={String(timespan)}
onValueChange={(v) => {
setTimespan(Number(v) as 1 | 2 | 3 | 4);
setPagination(prev => ({...prev, currentPage: 1}));
}}
disabled={isLoading}
>
<SelectTrigger className="w-[180px]">
2025-04-29 19:38:48 +02:00
<SelectValue placeholder={t('Uptime.TimeRange.Select')} />
2025-04-25 23:34:52 +02:00
</SelectTrigger>
<SelectContent>
2025-04-29 19:38:48 +02:00
<SelectItem value="1">{t('Uptime.TimeRange.LastHour')}</SelectItem>
<SelectItem value="2">{t('Uptime.TimeRange.LastDay')}</SelectItem>
<SelectItem value="3">{t('Uptime.TimeRange.Last7Days')}</SelectItem>
<SelectItem value="4">{t('Uptime.TimeRange.Last30Days')}</SelectItem>
2025-04-25 23:34:52 +02:00
</SelectContent>
</Select>
</div>
2025-04-15 13:46:25 +02:00
</div>
2025-04-15 14:09:36 +02:00
2025-04-15 13:58:53 +02:00
<div className="pt-4 space-y-4">
2025-04-15 14:09:36 +02:00
{isLoading ? (
2025-04-29 19:38:48 +02:00
<div className="text-center py-8">{t('Uptime.Messages.Loading')}</div>
2025-04-15 14:09:36 +02:00
) : (
data.map((app) => {
const reversedSummary = [...app.uptimeSummary].reverse();
const startTime = reversedSummary[0]?.timestamp;
const endTime = reversedSummary[reversedSummary.length - 1]?.timestamp;
2025-04-15 13:46:25 +02:00
2025-04-15 14:09:36 +02:00
return (
<Card key={app.appId}>
<CardHeader>
<div className="flex flex-col gap-4">
<div className="flex justify-between items-center">
<span className="text-lg font-semibold">{app.appName}</span>
2025-04-15 12:32:49 +02:00
</div>
2025-04-15 13:46:25 +02:00
2025-04-15 14:09:36 +02:00
<div className="flex flex-col gap-2">
<div className="flex justify-between text-sm text-muted-foreground">
<span>{startTime ? timeFormats[timespan](startTime) : ""}</span>
<span>{endTime ? timeFormats[timespan](endTime) : ""}</span>
2025-04-15 13:46:25 +02:00
</div>
2025-04-15 14:09:36 +02:00
<Tooltip.Provider>
<div
2025-04-15 14:21:51 +02:00
className="grid gap-0.5 w-full pb-2"
2025-04-15 14:09:36 +02:00
style={{
2025-04-15 14:21:51 +02:00
gridTemplateColumns: `repeat(auto-fit, minmax(${minBoxWidths[timespan]}px, 1fr))`
2025-04-15 14:09:36 +02:00
}}
>
{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">
{new Date(entry.timestamp).toLocaleString([], {
year: 'numeric',
month: 'short',
day: timespan > 2 ? 'numeric' : undefined,
hour: '2-digit',
minute: timespan === 1 ? '2-digit' : undefined,
hour12: false
})}
2025-04-15 14:09:36 +02:00
</p>
<p>
{entry.missing
2025-04-29 19:38:48 +02:00
? t('Uptime.Status.NoData')
2025-04-15 14:09:36 +02:00
: entry.online
2025-04-29 19:38:48 +02:00
? t('Uptime.Status.Online')
: t('Uptime.Status.Offline')}
2025-04-15 14:09:36 +02:00
</p>
</div>
<Tooltip.Arrow className="fill-gray-900" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
))}
</div>
</Tooltip.Provider>
</div>
2025-04-15 13:46:25 +02:00
</div>
2025-04-15 14:09:36 +02:00
</CardHeader>
</Card>
);
})
)}
2025-04-15 13:46:25 +02:00
</div>
2025-04-15 14:09:36 +02:00
{pagination.totalItems > 0 && !isLoading && (
2025-04-15 13:58:53 +02:00
<div className="pt-4 pb-4">
2025-04-25 23:34:52 +02:00
<div className="flex justify-between items-center mb-2">
<div className="text-sm text-muted-foreground">
{pagination.totalItems > 0
2025-04-29 19:38:48 +02:00
? t('Uptime.Pagination.Showing', {
start: ((pagination.currentPage - 1) * itemsPerPage) + 1,
end: Math.min(pagination.currentPage * itemsPerPage, pagination.totalItems),
total: pagination.totalItems
})
: t('Uptime.Messages.NoItems')}
2025-04-25 23:34:52 +02:00
</div>
</div>
2025-04-15 13:58:53 +02:00
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={handlePrevious}
2025-04-15 14:09:36 +02:00
aria-disabled={pagination.currentPage === 1 || isLoading}
className={
pagination.currentPage === 1 || isLoading
? "opacity-50 cursor-not-allowed"
: "hover:cursor-pointer"
}
2025-04-15 13:58:53 +02:00
/>
</PaginationItem>
<PaginationItem>
2025-04-15 16:33:12 +02:00
<PaginationLink isActive>{pagination.currentPage}</PaginationLink>
2025-04-15 13:58:53 +02:00
</PaginationItem>
<PaginationItem>
<PaginationNext
onClick={handleNext}
2025-04-15 14:09:36 +02:00
aria-disabled={pagination.currentPage === pagination.totalPages || isLoading}
className={
pagination.currentPage === pagination.totalPages || isLoading
? "opacity-50 cursor-not-allowed"
: "hover:cursor-pointer"
}
2025-04-15 13:58:53 +02:00
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
2025-04-15 12:22:15 +02:00
</div>
</SidebarInset>
</SidebarProvider>
);
}