Add custom items per page dropdowns

This commit is contained in:
headlessdev 2025-04-25 23:34:52 +02:00
parent d81d8c04ad
commit 677d1c5a58
6 changed files with 470 additions and 34 deletions

View File

@ -40,7 +40,8 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ return NextResponse.json({
applications: applicationsWithServers, applications: applicationsWithServers,
servers: servers_all, servers: servers_all,
maxPage maxPage,
totalItems: totalCount
}); });
} catch (error: unknown) { } catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";

View File

@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma";
interface RequestBody { interface RequestBody {
timespan?: number; timespan?: number;
page?: number; page?: number;
itemsPerPage?: number;
} }
@ -100,8 +101,7 @@ const getIntervalKey = (date: Date, timespan: number) => {
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const { timespan = 1, page = 1 }: RequestBody = await request.json(); const { timespan = 1, page = 1, itemsPerPage = 5 }: RequestBody = await request.json();
const itemsPerPage = 5;
const skip = (page - 1) * itemsPerPage; const skip = (page - 1) * itemsPerPage;
// Get paginated and sorted applications // Get paginated and sorted applications

View File

@ -233,8 +233,9 @@ export async function POST(request: NextRequest) {
// Only calculate maxPage when not requesting a specific server // Only calculate maxPage when not requesting a specific server
let maxPage = 1; let maxPage = 1;
let totalHosts = 0;
if (!serverId) { if (!serverId) {
const totalHosts = await prisma.server.count({ totalHosts = await prisma.server.count({
where: { OR: [{ hostServer: 0 }, { hostServer: null }] } where: { OR: [{ hostServer: 0 }, { hostServer: null }] }
}); });
maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE); maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE);
@ -242,7 +243,8 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ return NextResponse.json({
servers: hostsWithVms, servers: hostsWithVms,
maxPage maxPage,
totalItems: totalHosts
}); });
} catch (error: any) { } catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ error: error.message }, { status: 500 });

View File

@ -65,7 +65,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import axios from "axios"; import axios from "axios";
import { import {
Tooltip, Tooltip,
@ -98,6 +98,7 @@ interface ApplicationsResponse {
applications: Application[]; applications: Application[];
servers: Server[]; servers: Server[];
maxPage: number; maxPage: number;
totalItems?: number;
} }
export default function Dashboard() { export default function Dashboard() {
@ -126,11 +127,15 @@ export default function Dashboard() {
const [isSearching, setIsSearching] = useState<boolean>(false); const [isSearching, setIsSearching] = useState<boolean>(false);
const savedLayout = Cookies.get("layoutPreference-app"); const savedLayout = Cookies.get("layoutPreference-app");
const savedItemsPerPage = Cookies.get("itemsPerPage-app");
const initialIsGridLayout = savedLayout === "grid"; const initialIsGridLayout = savedLayout === "grid";
const initialItemsPerPage = initialIsGridLayout ? 15 : 5; const defaultItemsPerPage = initialIsGridLayout ? 15 : 5;
const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage;
const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout); const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout);
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage); const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
const customInputRef = useRef<HTMLInputElement>(null);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const toggleLayout = () => { const toggleLayout = () => {
const newLayout = !isGridLayout; const newLayout = !isGridLayout;
@ -140,7 +145,35 @@ export default function Dashboard() {
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
}); });
setItemsPerPage(newLayout ? 15 : 5); // Don't automatically change itemsPerPage when layout changes
};
const handleItemsPerPageChange = (value: string) => {
// Clear any existing timer
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
// Set a new timer
debounceTimerRef.current = setTimeout(() => {
const newItemsPerPage = parseInt(value);
// Ensure the value is within the valid range
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
toast.error("Please enter a number between 1 and 100");
return;
}
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
setItemsPerPage(validatedValue);
setCurrentPage(1); // Reset to first page when changing items per page
Cookies.set("itemsPerPage-app", String(validatedValue), {
expires: 365,
path: "/",
sameSite: "strict",
});
}, 300); // 300ms delay
}; };
const add = async () => { const add = async () => {
@ -171,6 +204,9 @@ export default function Dashboard() {
setApplications(response.data.applications); setApplications(response.data.applications);
setServers(response.data.servers); setServers(response.data.servers);
setMaxPage(response.data.maxPage); setMaxPage(response.data.maxPage);
if (response.data.totalItems !== undefined) {
setTotalItems(response.data.totalItems);
}
setLoading(false); setLoading(false);
} catch (error: any) { } catch (error: any) {
console.log(error.response?.data); console.log(error.response?.data);
@ -178,6 +214,11 @@ export default function Dashboard() {
} }
}; };
// Calculate current range of items being displayed
const [totalItems, setTotalItems] = useState<number>(0);
const startItem = (currentPage - 1) * itemsPerPage + 1;
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
useEffect(() => { useEffect(() => {
getApplications(); getApplications();
}, [currentPage, itemsPerPage]); }, [currentPage, itemsPerPage]);
@ -308,6 +349,91 @@ export default function Dashboard() {
<LayoutGrid className="h-4 w-4" /> <LayoutGrid className="h-4 w-4" />
)} )}
</Button> </Button>
<Select
value={String(itemsPerPage)}
onValueChange={handleItemsPerPageChange}
onOpenChange={(open) => {
if (open && customInputRef.current) {
customInputRef.current.value = String(itemsPerPage);
}
}}
>
<SelectTrigger className="w-[140px]">
<SelectValue>
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'}
</SelectValue>
</SelectTrigger>
<SelectContent>
{![5, 10, 15, 20, 25].includes(itemsPerPage) ? (
<SelectItem value={String(itemsPerPage)}>
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'} (custom)
</SelectItem>
) : null}
<SelectItem value="5">5 items</SelectItem>
<SelectItem value="10">10 items</SelectItem>
<SelectItem value="15">15 items</SelectItem>
<SelectItem value="20">20 items</SelectItem>
<SelectItem value="25">25 items</SelectItem>
<div className="p-2 border-t mt-1">
<Label htmlFor="custom-items" className="text-xs font-medium">Custom (1-100)</Label>
<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) => {
// Don't immediately apply the change while typing
// Just validate the input for visual feedback
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) => {
// Apply the change when the input loses focus
const value = parseInt(e.target.value);
if (value >= 1 && value <= 100) {
handleItemsPerPageChange(e.target.value);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
// Clear any existing debounce timer to apply immediately
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
const value = parseInt((e.target as HTMLInputElement).value);
if (value >= 1 && value <= 100) {
// Apply change immediately on Enter
const validatedValue = Math.min(Math.max(value, 1), 100);
setItemsPerPage(validatedValue);
setCurrentPage(1);
Cookies.set("itemsPerPage-app", String(validatedValue), {
expires: 365,
path: "/",
sameSite: "strict",
});
// Close the dropdown
document.body.click();
}
}
}}
onClick={(e) => e.stopPropagation()}
/>
<span className="text-xs text-muted-foreground whitespace-nowrap">items</span>
</div>
</div>
</SelectContent>
</Select>
{servers.length === 0 ? ( {servers.length === 0 ? (
<p className="text-muted-foreground"> <p className="text-muted-foreground">
You must first add a server. You must first add a server.
@ -680,6 +806,11 @@ export default function Dashboard() {
</div> </div>
)} )}
<div className="pt-4 pb-4"> <div className="pt-4 pb-4">
<div className="flex justify-between items-center mb-2">
<div className="text-sm text-muted-foreground">
{totalItems > 0 ? `Showing ${startItem}-${endItem} of ${totalItems} applications` : "No applications found"}
</div>
</div>
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>

View File

@ -103,6 +103,7 @@ interface Server {
interface GetServersResponse { interface GetServersResponse {
servers: Server[] servers: Server[]
maxPage: number maxPage: number
totalItems: number
} }
interface MonitoringData { interface MonitoringData {
@ -136,6 +137,7 @@ export default function Dashboard() {
const [maxPage, setMaxPage] = useState<number>(1) const [maxPage, setMaxPage] = useState<number>(1)
const [servers, setServers] = useState<Server[]>([]) const [servers, setServers] = useState<Server[]>([])
const [loading, setLoading] = useState<boolean>(true) const [loading, setLoading] = useState<boolean>(true)
const [totalItems, setTotalItems] = useState<number>(0)
const [editId, setEditId] = useState<number | null>(null) const [editId, setEditId] = useState<number | null>(null)
const [editHost, setEditHost] = useState<boolean>(false) const [editHost, setEditHost] = useState<boolean>(false)
@ -161,11 +163,15 @@ export default function Dashboard() {
const [monitoringInterval, setMonitoringInterval] = useState<NodeJS.Timeout | null>(null); const [monitoringInterval, setMonitoringInterval] = useState<NodeJS.Timeout | null>(null);
const savedLayout = Cookies.get("layoutPreference-servers"); const savedLayout = Cookies.get("layoutPreference-servers");
const savedItemsPerPage = Cookies.get("itemsPerPage-servers");
const initialIsGridLayout = savedLayout === "grid"; const initialIsGridLayout = savedLayout === "grid";
const initialItemsPerPage = initialIsGridLayout ? 6 : 4; const defaultItemsPerPage = initialIsGridLayout ? 6 : 4;
const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage;
const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout); const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout);
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage); const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
const customInputRef = useRef<HTMLInputElement>(null);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const toggleLayout = () => { const toggleLayout = () => {
const newLayout = !isGridLayout; const newLayout = !isGridLayout;
@ -175,7 +181,6 @@ export default function Dashboard() {
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
}); });
setItemsPerPage(newLayout ? 6 : 4); // Update itemsPerPage based on new layout
}; };
const add = async () => { const add = async () => {
@ -231,6 +236,7 @@ export default function Dashboard() {
setServers(response.data.servers) setServers(response.data.servers)
console.log(response.data.servers) console.log(response.data.servers)
setMaxPage(response.data.maxPage) setMaxPage(response.data.maxPage)
setTotalItems(response.data.totalItems)
setLoading(false) setLoading(false)
} catch (error: any) { } catch (error: any) {
console.log(error.response) console.log(error.response)
@ -451,6 +457,61 @@ export default function Dashboard() {
}; };
}, []); }, []);
// Handler für benutzerdefinierte Zahleneingaben mit Verzögerung
const handleItemsPerPageChange = (value: string) => {
// Bestehenden Timer löschen
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
// Neuen Timer setzen
debounceTimerRef.current = setTimeout(() => {
const newItemsPerPage = parseInt(value);
// Sicherstellen, dass der Wert im gültigen Bereich liegt
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
toast.error("Bitte eine Zahl zwischen 1 und 100 eingeben");
return;
}
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
setItemsPerPage(validatedValue);
setCurrentPage(1); // Zurück zur ersten Seite
Cookies.set("itemsPerPage-servers", String(validatedValue), {
expires: 365,
path: "/",
sameSite: "strict",
});
// Daten mit neuer Paginierung abrufen
getServers();
}, 600); // 600ms Verzögerung für bessere Eingabe mehrziffriger Zahlen
};
// Handler für voreingestellte Werte aus dem Dropdown
const handlePresetItemsPerPageChange = (value: string) => {
// Für voreingestellte Werte sofort anwenden
const newItemsPerPage = parseInt(value);
// Nur Standardwerte hier verarbeiten
if ([4, 6, 10, 15, 20, 25].includes(newItemsPerPage)) {
setItemsPerPage(newItemsPerPage);
setCurrentPage(1); // Zurück zur ersten Seite
Cookies.set("itemsPerPage-servers", String(newItemsPerPage), {
expires: 365,
path: "/",
sameSite: "strict",
});
// Daten mit neuer Paginierung abrufen
getServers();
} else {
// Für benutzerdefinierte Werte den verzögerten Handler verwenden
handleItemsPerPageChange(value);
}
};
return ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar /> <AppSidebar />
@ -491,6 +552,99 @@ export default function Dashboard() {
<TooltipContent>{isGridLayout ? "Switch to list view" : "Switch to grid view"}</TooltipContent> <TooltipContent>{isGridLayout ? "Switch to list view" : "Switch to grid view"}</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<Select
value={String(itemsPerPage)}
onValueChange={handlePresetItemsPerPageChange}
onOpenChange={(open) => {
if (open && customInputRef.current) {
customInputRef.current.value = String(itemsPerPage);
}
}}
>
<SelectTrigger className="w-[140px]">
<SelectValue>
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'}
</SelectValue>
</SelectTrigger>
<SelectContent>
{![4, 6, 10, 15, 20, 25].includes(itemsPerPage) ? (
<SelectItem value={String(itemsPerPage)}>
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'} (custom)
</SelectItem>
) : null}
<SelectItem value="4">4 items</SelectItem>
<SelectItem value="6">6 items</SelectItem>
<SelectItem value="10">10 items</SelectItem>
<SelectItem value="15">15 items</SelectItem>
<SelectItem value="20">20 items</SelectItem>
<SelectItem value="25">25 items</SelectItem>
<div className="p-2 border-t mt-1">
<Label htmlFor="custom-items" className="text-xs font-medium">Custom (1-100)</Label>
<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) => {
// Änderung nicht sofort anwenden während des Tippens
// Nur visuelles Feedback für die Validierung
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) => {
// Änderung anwenden, wenn das Input den Fokus verliert
const value = parseInt(e.target.value);
if (value >= 1 && value <= 100) {
handleItemsPerPageChange(e.target.value);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
// Bestehenden Debounce-Timer löschen, um sofort anzuwenden
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
const value = parseInt((e.target as HTMLInputElement).value);
if (value >= 1 && value <= 100) {
// Änderung sofort bei Enter anwenden
const validatedValue = Math.min(Math.max(value, 1), 100);
setItemsPerPage(validatedValue);
setCurrentPage(1);
Cookies.set("itemsPerPage-servers", String(validatedValue), {
expires: 365,
path: "/",
sameSite: "strict",
});
// Kurze Verzögerung hinzufügen für bessere Reaktionsfähigkeit
setTimeout(() => {
getServers();
// Dropdown schließen
document.body.click();
}, 50);
}
}
}}
onClick={(e) => e.stopPropagation()}
/>
<span className="text-xs text-muted-foreground whitespace-nowrap">items</span>
</div>
</div>
</SelectContent>
</Select>
<AlertDialog onOpenChange={setIsAddDialogOpen}> <AlertDialog onOpenChange={setIsAddDialogOpen}>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button variant="outline" size="icon"> <Button variant="outline" size="icon">
@ -1862,6 +2016,13 @@ export default function Dashboard() {
</div> </div>
)} )}
<div className="pt-4 pb-4"> <div className="pt-4 pb-4">
<div className="flex justify-between items-center mb-2">
<div className="text-sm text-muted-foreground">
{totalItems > 0
? `Showing ${((currentPage - 1) * itemsPerPage) + 1}-${Math.min(currentPage * itemsPerPage, totalItems)} of ${totalItems} servers`
: "No servers found"}
</div>
</div>
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>

View File

@ -13,7 +13,6 @@ import {
SidebarProvider, SidebarProvider,
SidebarTrigger, SidebarTrigger,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { useEffect, useState } from "react";
import axios from "axios"; import axios from "axios";
import { Card, CardHeader } from "@/components/ui/card"; import { Card, CardHeader } from "@/components/ui/card";
import * as Tooltip from "@radix-ui/react-tooltip"; import * as Tooltip from "@radix-ui/react-tooltip";
@ -26,6 +25,12 @@ import {
PaginationNext, PaginationNext,
PaginationLink, PaginationLink,
} from "@/components/ui/pagination"; } from "@/components/ui/pagination";
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";
const timeFormats = { const timeFormats = {
1: (timestamp: string) => 1: (timestamp: string) =>
@ -85,7 +90,15 @@ export default function Uptime() {
}); });
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const getData = async (selectedTimespan: number, page: number) => { 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);
const getData = async (selectedTimespan: number, page: number, itemsPerPage: number) => {
setIsLoading(true); setIsLoading(true);
try { try {
const response = await axios.post<{ const response = await axios.post<{
@ -93,7 +106,8 @@ export default function Uptime() {
pagination: PaginationData; pagination: PaginationData;
}>("/api/applications/uptime", { }>("/api/applications/uptime", {
timespan: selectedTimespan, timespan: selectedTimespan,
page page,
itemsPerPage
}); });
setData(response.data.data); setData(response.data.data);
@ -114,17 +128,48 @@ export default function Uptime() {
const handlePrevious = () => { const handlePrevious = () => {
const newPage = Math.max(1, pagination.currentPage - 1); const newPage = Math.max(1, pagination.currentPage - 1);
setPagination(prev => ({...prev, currentPage: newPage})); setPagination(prev => ({...prev, currentPage: newPage}));
getData(timespan, newPage); getData(timespan, newPage, itemsPerPage);
}; };
const handleNext = () => { const handleNext = () => {
const newPage = Math.min(pagination.totalPages, pagination.currentPage + 1); const newPage = Math.min(pagination.totalPages, pagination.currentPage + 1);
setPagination(prev => ({...prev, currentPage: newPage})); setPagination(prev => ({...prev, currentPage: newPage}));
getData(timespan, newPage); getData(timespan, newPage, itemsPerPage);
};
const handleItemsPerPageChange = (value: string) => {
// Clear any existing timer
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
// Set a new timer
debounceTimerRef.current = setTimeout(() => {
const newItemsPerPage = parseInt(value);
// Ensure the value is within the valid range
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
toast.error("Please enter a number between 1 and 100");
return;
}
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
setItemsPerPage(validatedValue);
setPagination(prev => ({...prev, currentPage: 1})); // Reset to first page
Cookies.set("itemsPerPage-uptime", String(validatedValue), {
expires: 365,
path: "/",
sameSite: "strict",
});
// Fetch data with new pagination
getData(timespan, 1, validatedValue);
}, 300); // 300ms delay
}; };
useEffect(() => { useEffect(() => {
getData(timespan, 1); getData(timespan, 1, itemsPerPage);
}, [timespan]); }, [timespan]);
return ( return (
@ -152,9 +197,97 @@ export default function Uptime() {
</Breadcrumb> </Breadcrumb>
</div> </div>
</header> </header>
<Toaster />
<div className="p-6"> <div className="p-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-3xl font-bold">Uptime</span> <span className="text-3xl font-bold">Uptime</span>
<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>
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'}
</SelectValue>
</SelectTrigger>
<SelectContent>
{![5, 10, 15, 20, 25].includes(itemsPerPage) ? (
<SelectItem value={String(itemsPerPage)}>
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'} (custom)
</SelectItem>
) : null}
<SelectItem value="5">5 items</SelectItem>
<SelectItem value="10">10 items</SelectItem>
<SelectItem value="15">15 items</SelectItem>
<SelectItem value="20">20 items</SelectItem>
<SelectItem value="25">25 items</SelectItem>
<div className="p-2 border-t mt-1">
<Label htmlFor="custom-items" className="text-xs font-medium">Custom (1-100)</Label>
<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) => {
// Don't immediately apply the change while typing
// Just validate the input for visual feedback
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) => {
// Apply the change when the input loses focus
const value = parseInt(e.target.value);
if (value >= 1 && value <= 100) {
handleItemsPerPageChange(e.target.value);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
// Clear any existing debounce timer to apply immediately
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
const value = parseInt((e.target as HTMLInputElement).value);
if (value >= 1 && value <= 100) {
// Apply change immediately on Enter
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);
// Close the dropdown
document.body.click();
}
}
}}
onClick={(e) => e.stopPropagation()}
/>
<span className="text-xs text-muted-foreground whitespace-nowrap">items</span>
</div>
</div>
</SelectContent>
</Select>
<Select <Select
value={String(timespan)} value={String(timespan)}
onValueChange={(v) => { onValueChange={(v) => {
@ -174,6 +307,7 @@ export default function Uptime() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div>
<div className="pt-4 space-y-4"> <div className="pt-4 space-y-4">
{isLoading ? ( {isLoading ? (
@ -260,6 +394,13 @@ export default function Uptime() {
{pagination.totalItems > 0 && !isLoading && ( {pagination.totalItems > 0 && !isLoading && (
<div className="pt-4 pb-4"> <div className="pt-4 pb-4">
<div className="flex justify-between items-center mb-2">
<div className="text-sm text-muted-foreground">
{pagination.totalItems > 0
? `Showing ${((pagination.currentPage - 1) * itemsPerPage) + 1}-${Math.min(pagination.currentPage * itemsPerPage, pagination.totalItems)} of ${pagination.totalItems} items`
: "No items found"}
</div>
</div>
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>