From 677d1c5a588283c48d042fb95bae6fe59acb6e6e Mon Sep 17 00:00:00 2001 From: headlessdev Date: Fri, 25 Apr 2025 23:34:52 +0200 Subject: [PATCH] Add custom items per page dropdowns --- app/api/applications/get/route.ts | 3 +- app/api/applications/uptime/route.ts | 4 +- app/api/servers/get/route.ts | 6 +- app/dashboard/applications/Applications.tsx | 137 +++++++++++++- app/dashboard/servers/Servers.tsx | 165 ++++++++++++++++- app/dashboard/uptime/Uptime.tsx | 189 +++++++++++++++++--- 6 files changed, 470 insertions(+), 34 deletions(-) diff --git a/app/api/applications/get/route.ts b/app/api/applications/get/route.ts index e0fd0d3..ac4bf44 100644 --- a/app/api/applications/get/route.ts +++ b/app/api/applications/get/route.ts @@ -40,7 +40,8 @@ export async function POST(request: NextRequest) { return NextResponse.json({ applications: applicationsWithServers, servers: servers_all, - maxPage + maxPage, + totalItems: totalCount }); } catch (error: unknown) { const message = error instanceof Error ? error.message : "Unknown error"; diff --git a/app/api/applications/uptime/route.ts b/app/api/applications/uptime/route.ts index 7c61f12..a42f545 100644 --- a/app/api/applications/uptime/route.ts +++ b/app/api/applications/uptime/route.ts @@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma"; interface RequestBody { timespan?: number; page?: number; + itemsPerPage?: number; } @@ -100,8 +101,7 @@ const getIntervalKey = (date: Date, timespan: number) => { export async function POST(request: NextRequest) { try { - const { timespan = 1, page = 1 }: RequestBody = await request.json(); - const itemsPerPage = 5; + const { timespan = 1, page = 1, itemsPerPage = 5 }: RequestBody = await request.json(); const skip = (page - 1) * itemsPerPage; // Get paginated and sorted applications diff --git a/app/api/servers/get/route.ts b/app/api/servers/get/route.ts index 7b7e156..08533c7 100644 --- a/app/api/servers/get/route.ts +++ b/app/api/servers/get/route.ts @@ -233,8 +233,9 @@ export async function POST(request: NextRequest) { // Only calculate maxPage when not requesting a specific server let maxPage = 1; + let totalHosts = 0; if (!serverId) { - const totalHosts = await prisma.server.count({ + totalHosts = await prisma.server.count({ where: { OR: [{ hostServer: 0 }, { hostServer: null }] } }); maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE); @@ -242,7 +243,8 @@ export async function POST(request: NextRequest) { return NextResponse.json({ servers: hostsWithVms, - maxPage + maxPage, + totalItems: totalHosts }); } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 500 }); diff --git a/app/dashboard/applications/Applications.tsx b/app/dashboard/applications/Applications.tsx index 0e46c10..8d28083 100644 --- a/app/dashboard/applications/Applications.tsx +++ b/app/dashboard/applications/Applications.tsx @@ -65,7 +65,7 @@ import { SelectValue, } from "@/components/ui/select"; import Cookies from "js-cookie"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import axios from "axios"; import { Tooltip, @@ -98,6 +98,7 @@ interface ApplicationsResponse { applications: Application[]; servers: Server[]; maxPage: number; + totalItems?: number; } export default function Dashboard() { @@ -126,11 +127,15 @@ export default function Dashboard() { const [isSearching, setIsSearching] = useState(false); const savedLayout = Cookies.get("layoutPreference-app"); + const savedItemsPerPage = Cookies.get("itemsPerPage-app"); const initialIsGridLayout = savedLayout === "grid"; - const initialItemsPerPage = initialIsGridLayout ? 15 : 5; + const defaultItemsPerPage = initialIsGridLayout ? 15 : 5; + const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage; const [isGridLayout, setIsGridLayout] = useState(initialIsGridLayout); const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage); + const customInputRef = useRef(null); + const debounceTimerRef = useRef(null); const toggleLayout = () => { const newLayout = !isGridLayout; @@ -140,7 +145,35 @@ export default function Dashboard() { path: "/", 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 () => { @@ -171,6 +204,9 @@ export default function Dashboard() { setApplications(response.data.applications); setServers(response.data.servers); setMaxPage(response.data.maxPage); + if (response.data.totalItems !== undefined) { + setTotalItems(response.data.totalItems); + } setLoading(false); } catch (error: any) { console.log(error.response?.data); @@ -178,6 +214,11 @@ export default function Dashboard() { } }; + // Calculate current range of items being displayed + const [totalItems, setTotalItems] = useState(0); + const startItem = (currentPage - 1) * itemsPerPage + 1; + const endItem = Math.min(currentPage * itemsPerPage, totalItems); + useEffect(() => { getApplications(); }, [currentPage, itemsPerPage]); @@ -308,6 +349,91 @@ export default function Dashboard() { )} + { + // 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()} + /> + items + + + + {servers.length === 0 ? (

You must first add a server. @@ -680,6 +806,11 @@ export default function Dashboard() { )}

+
+
+ {totalItems > 0 ? `Showing ${startItem}-${endItem} of ${totalItems} applications` : "No applications found"} +
+
diff --git a/app/dashboard/servers/Servers.tsx b/app/dashboard/servers/Servers.tsx index 51de441..7b09fc7 100644 --- a/app/dashboard/servers/Servers.tsx +++ b/app/dashboard/servers/Servers.tsx @@ -103,6 +103,7 @@ interface Server { interface GetServersResponse { servers: Server[] maxPage: number + totalItems: number } interface MonitoringData { @@ -136,6 +137,7 @@ export default function Dashboard() { const [maxPage, setMaxPage] = useState(1) const [servers, setServers] = useState([]) const [loading, setLoading] = useState(true) + const [totalItems, setTotalItems] = useState(0) const [editId, setEditId] = useState(null) const [editHost, setEditHost] = useState(false) @@ -161,11 +163,15 @@ export default function Dashboard() { const [monitoringInterval, setMonitoringInterval] = useState(null); const savedLayout = Cookies.get("layoutPreference-servers"); + const savedItemsPerPage = Cookies.get("itemsPerPage-servers"); const initialIsGridLayout = savedLayout === "grid"; - const initialItemsPerPage = initialIsGridLayout ? 6 : 4; + const defaultItemsPerPage = initialIsGridLayout ? 6 : 4; + const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage; const [isGridLayout, setIsGridLayout] = useState(initialIsGridLayout); const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage); + const customInputRef = useRef(null); + const debounceTimerRef = useRef(null); const toggleLayout = () => { const newLayout = !isGridLayout; @@ -175,7 +181,6 @@ export default function Dashboard() { path: "/", sameSite: "strict", }); - setItemsPerPage(newLayout ? 6 : 4); // Update itemsPerPage based on new layout }; const add = async () => { @@ -231,6 +236,7 @@ export default function Dashboard() { setServers(response.data.servers) console.log(response.data.servers) setMaxPage(response.data.maxPage) + setTotalItems(response.data.totalItems) setLoading(false) } catch (error: any) { 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 ( @@ -491,6 +552,99 @@ export default function Dashboard() { {isGridLayout ? "Switch to list view" : "Switch to grid view"} + + { + // Ä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()} + /> + items +
+ + + +