mirror of
https://github.com/crocofied/CoreControl.git
synced 2025-12-17 15:36:50 +00:00
Add custom items per page dropdowns
This commit is contained in:
parent
d81d8c04ad
commit
677d1c5a58
@ -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";
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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<boolean>(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<boolean>(initialIsGridLayout);
|
||||
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
|
||||
const customInputRef = useRef<HTMLInputElement>(null);
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(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<number>(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() {
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
)}
|
||||
</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 ? (
|
||||
<p className="text-muted-foreground">
|
||||
You must first add a server.
|
||||
@ -680,6 +806,11 @@ export default function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
|
||||
@ -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<number>(1)
|
||||
const [servers, setServers] = useState<Server[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [totalItems, setTotalItems] = useState<number>(0)
|
||||
|
||||
const [editId, setEditId] = useState<number | null>(null)
|
||||
const [editHost, setEditHost] = useState<boolean>(false)
|
||||
@ -161,11 +163,15 @@ export default function Dashboard() {
|
||||
const [monitoringInterval, setMonitoringInterval] = useState<NodeJS.Timeout | null>(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<boolean>(initialIsGridLayout);
|
||||
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
|
||||
const customInputRef = useRef<HTMLInputElement>(null);
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(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 (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
@ -491,6 +552,99 @@ export default function Dashboard() {
|
||||
<TooltipContent>{isGridLayout ? "Switch to list view" : "Switch to grid view"}</TooltipContent>
|
||||
</Tooltip>
|
||||
</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}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
@ -1862,6 +2016,13 @@ export default function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { Card, CardHeader } from "@/components/ui/card";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
@ -26,6 +25,12 @@ import {
|
||||
PaginationNext,
|
||||
PaginationLink,
|
||||
} 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 = {
|
||||
1: (timestamp: string) =>
|
||||
@ -84,8 +89,16 @@ export default function Uptime() {
|
||||
totalItems: 0
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
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) => {
|
||||
const getData = async (selectedTimespan: number, page: number, itemsPerPage: number) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await axios.post<{
|
||||
@ -93,7 +106,8 @@ export default function Uptime() {
|
||||
pagination: PaginationData;
|
||||
}>("/api/applications/uptime", {
|
||||
timespan: selectedTimespan,
|
||||
page
|
||||
page,
|
||||
itemsPerPage
|
||||
});
|
||||
|
||||
setData(response.data.data);
|
||||
@ -114,17 +128,48 @@ export default function Uptime() {
|
||||
const handlePrevious = () => {
|
||||
const newPage = Math.max(1, pagination.currentPage - 1);
|
||||
setPagination(prev => ({...prev, currentPage: newPage}));
|
||||
getData(timespan, newPage);
|
||||
getData(timespan, newPage, itemsPerPage);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
const newPage = Math.min(pagination.totalPages, pagination.currentPage + 1);
|
||||
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(() => {
|
||||
getData(timespan, 1);
|
||||
getData(timespan, 1, itemsPerPage);
|
||||
}, [timespan]);
|
||||
|
||||
return (
|
||||
@ -152,27 +197,116 @@ export default function Uptime() {
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<Toaster />
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-3xl font-bold">Uptime</span>
|
||||
<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]">
|
||||
<SelectValue placeholder="Select timespan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Last 1 hour</SelectItem>
|
||||
<SelectItem value="2">Last 1 day</SelectItem>
|
||||
<SelectItem value="3">Last 7 days</SelectItem>
|
||||
<SelectItem value="4">Last 30 days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<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
|
||||
value={String(timespan)}
|
||||
onValueChange={(v) => {
|
||||
setTimespan(Number(v) as 1 | 2 | 3 | 4);
|
||||
setPagination(prev => ({...prev, currentPage: 1}));
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select timespan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Last 1 hour</SelectItem>
|
||||
<SelectItem value="2">Last 1 day</SelectItem>
|
||||
<SelectItem value="3">Last 7 days</SelectItem>
|
||||
<SelectItem value="4">Last 30 days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 space-y-4">
|
||||
@ -260,6 +394,13 @@ export default function Uptime() {
|
||||
|
||||
{pagination.totalItems > 0 && !isLoading && (
|
||||
<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>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user