Optimise server calls to not duplicate

This commit is contained in:
ferreo 2024-07-29 17:53:52 +01:00
parent cb46826770
commit 10d7333454
4 changed files with 279 additions and 224 deletions

View File

@ -1,50 +1,13 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import usePackageData from '../../hooks/usePackageData';
interface CountResponse {
lastUpdateTime: string;
counts: PackageStats;
}
interface PackageStats {
stale: number;
missing: number;
built: number;
error: number;
queued: number;
building: number;
}
const Home: React.FC = () => { const Home: React.FC = () => {
const [stats, setStats] = useState<PackageStats | null>(null); const { stats, lastUpdated, loading, fetchStats } = usePackageData();
const [lastUpdated, setLastUpdated] = useState<string>('');
useEffect(() => { useEffect(() => {
const fetchStats = async () => {
try {
const response = await fetch('/api/counts');
const data: CountResponse = await response.json();
const counts = data.counts;
setStats(counts);
const dt = new Date(data.lastUpdateTime);
const dateLocale = dt.toLocaleDateString(navigator.language, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
const timeLocale = dt.toLocaleTimeString(navigator.language, {
hour: '2-digit',
minute: '2-digit'
});
setLastUpdated(`${timeLocale} on ${dateLocale}`);
} catch (error) {
console.error('Failed to fetch stats:', error);
}
};
fetchStats(); fetchStats();
}, []); }, [fetchStats]);
const statCards = [ const statCards = [
{ title: 'Current', value: stats?.built, description: 'Successfully built packages' }, { title: 'Current', value: stats?.built, description: 'Successfully built packages' },
@ -55,6 +18,10 @@ const Home: React.FC = () => {
{ title: 'Error', value: stats?.error, description: 'Packages with build errors' }, { title: 'Error', value: stats?.error, description: 'Packages with build errors' },
]; ];
if (loading) {
return <div>Loading...</div>;
}
return ( return (
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8 text-center">Welcome to the PikaOS Package Builder!</h1> <h1 className="text-3xl font-bold mb-8 text-center">Welcome to the PikaOS Package Builder!</h1>
@ -66,8 +33,8 @@ const Home: React.FC = () => {
<CardTitle>{card.title}</CardTitle> <CardTitle>{card.title}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-4xl font-bold mb-2">{card.value !== undefined ? card.value : 'Loading...'}</p> <p className="text-4xl font-bold mb-2">{card.value !== undefined ? card.value : 'N/A'}</p>
<p className="text-sm text-card-foreground" >{card.description}</p> <p className="text-sm text-card-foreground">{card.description}</p>
</CardContent> </CardContent>
</Card> </Card>
))} ))}

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useRef, useCallback } from "react";
import { useMediaQuery } from "react-responsive"; import { useMediaQuery } from "react-responsive";
import useSearchParams from "./useSearchParams"; import useSearchParams from "../../hooks/useSearchParams";
import usePackageData from "../../hooks/usePackageData";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
import { import {
Select, Select,
@ -27,79 +28,55 @@ import {
PaginationPrevious, PaginationPrevious,
} from "../ui/pagination"; } from "../ui/pagination";
interface Package {
Name: string;
Packages: {
[key: string]: {
Version: string;
NewVersion: string;
Status: string;
};
};
}
const Packages: React.FC = () => { const Packages: React.FC = () => {
const { getParam, setParam } = useSearchParams(); const { getParam, setParam } = useSearchParams();
const [packages, setPackages] = useState<Package[]>([]); const { packages, totalCount, loading, fetchPackages } = usePackageData();
const [totalCount, setTotalCount] = useState(0); const [currentPage, setCurrentPage] = useState(parseInt(getParam("page") || "1", 10));
const [currentPage, setCurrentPage] = useState(
parseInt(getParam("page") || "1", 10)
);
const isMobile = useMediaQuery({ maxWidth: 767 }); const isMobile = useMediaQuery({ maxWidth: 767 });
const [pageSize, setPageSize] = useState(isMobile ? 100 : 250); const [pageSize, setPageSize] = useState(isMobile ? 100 : 250);
const [search, setSearch] = useState(getParam("search") || ""); const [search, setSearch] = useState(getParam("search") || "");
const [filter, setFilter] = useState(getParam("filter") || "All"); const [filter, setFilter] = useState(getParam("filter") || "All");
const [loading, setLoading] = useState(true);
const isInitialMount = useRef(true);
const prevDependencies = useRef({ currentPage, pageSize, search, filter });
useEffect(() => { useEffect(() => {
setPageSize(isMobile ? 100 : 250); setPageSize(isMobile ? 100 : 250);
}, [isMobile]); }, [isMobile]);
useEffect(() => { const loadPackages = useCallback(() => {
fetchPackages(); fetchPackages(currentPage, pageSize, search, filter);
setParam("page", currentPage.toString()); setParam("page", currentPage.toString());
setParam("search", search); setParam("search", search);
setParam("filter", filter); setParam("filter", filter);
}, [currentPage, search, filter, pageSize]); }, [currentPage, pageSize, search, filter, fetchPackages, setParam]);
useEffect(() => {
const fetchPackages = async () => { if (isInitialMount.current) {
setLoading(true); isInitialMount.current = false;
try { loadPackages();
const filterParam = filter === "All" ? "" : filter; } else {
const response = await fetch( const { currentPage: prevPage, pageSize: prevSize, search: prevSearch, filter: prevFilter } = prevDependencies.current;
`/api/packages?page=${currentPage}&pageSize=${pageSize}&search=${search}&filter=${filterParam}` if (currentPage !== prevPage || pageSize !== prevSize || search !== prevSearch || filter !== prevFilter) {
); loadPackages();
const data = await response.json(); prevDependencies.current = { currentPage, pageSize, search, filter };
setTotalCount(data.total); }
setPackages(Object.values(data.packages));
} catch (error) {
setTotalCount(0);
console.error("Error fetching packages:", error);
} finally {
setLoading(false);
} }
}; }, [loadPackages, currentPage, pageSize, search, filter]);
const handleFilterChange = (value: string) => { const handleFilterChange = (value: string) => {
const newFilter = value === "All" ? "" : value; const newFilter = value === "All" ? "" : value;
setFilter(newFilter); setFilter(newFilter);
setCurrentPage(1); setCurrentPage(1);
setParam("filter", newFilter);
setParam("page", "1");
}; };
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newSearch = e.target.value; setSearch(e.target.value);
setSearch(newSearch);
setCurrentPage(1); setCurrentPage(1);
setParam("search", newSearch);
setParam("page", "1");
}; };
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
setCurrentPage(page); setCurrentPage(page);
setParam("page", page.toString());
}; };
const totalPages = Math.ceil(totalCount / pageSize); const totalPages = Math.ceil(totalCount / pageSize);
@ -140,151 +117,174 @@ const Packages: React.FC = () => {
</div> </div>
</div> </div>
{/* Table */} {/* Table */}
<div className="flex-grow overflow-hidden"> <div className="flex-grow overflow-hidden">
<div className="container mx-0 px-0 h-full"> <div className="container mx-0 px-0 h-full">
<div className="h-full"> <div className="h-full">
<Table className="w-full"> <Table className="w-full">
<TableHeader className="top-0 bg-background z-10 hidden md:table-header-group"> <TableHeader className="top-0 bg-background z-10 hidden md:table-header-group">
<TableRow> <TableRow>
<TableHead className="w-1/2 text-left">Name</TableHead> <TableHead className="w-1/2 text-left">Name</TableHead>
<TableHead className="w-1/4 text-left">Version</TableHead> <TableHead className="w-1/4 text-left">Version</TableHead>
<TableHead className="w-1/4 text-left">Status</TableHead> <TableHead className="w-1/4 text-left">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={3} className="text-center">
Loading...
</TableCell>
</TableRow>
) : (
packages.map((pkg) => {
const firstPackage = Object.values(pkg.Packages)[0];
return (
<TableRow
key={pkg.Name}
className="md:table-row border-b last:border-b-0"
>
<TableCell className="w-full md:w-1/2 py-2 md:py-4">
<div className="flex flex-col h-full md:h-auto">
<div className="flex justify-between items-start mb-2 md:mb-0">
<span className="font-medium truncate mr-2">{pkg.Name}</span>
<span className="text-sm md:hidden truncate">{firstPackage.Status}</span>
</div>
<div className="md:hidden text-sm text-muted-foreground text-center">
<span className="truncate">{firstPackage.Version}</span>
{firstPackage.NewVersion && (
<span className="truncate block"> {firstPackage.NewVersion}</span>
)}
</div>
</div>
</TableCell>
<TableCell className="hidden md:table-cell md:w-1/4 py-4 text-left">
<div className="flex flex-col">
<span className="truncate">{firstPackage.Version}</span>
{firstPackage.NewVersion && (
<span className="text-sm text-muted-foreground truncate">
{firstPackage.NewVersion}
</span>
)}
</div>
</TableCell>
<TableCell className="hidden md:table-cell md:w-1/4 py-2 md:py-4 text-left">
<span className="truncate">{firstPackage.Status}</span>
</TableCell>
</TableRow> </TableRow>
); </TableHeader>
}) <TableBody>
)} {loading ? (
</TableBody> <TableRow>
</Table> <TableCell colSpan={3} className="text-center">
</div> Loading...
</div> </TableCell>
</div> </TableRow>
) : Object.keys(packages).length === 0 ? (
<TableRow>
<TableCell colSpan={3} className="text-center">
No packages found.
</TableCell>
</TableRow>
) : (
Object.entries(packages).map(([pkgName, pkgInfo]) => {
const firstPackage = Object.values(pkgInfo.Packages)[0];
return (
<TableRow
key={pkgName}
className="md:table-row border-b last:border-b-0"
>
<TableCell className="w-full md:w-1/2 py-2 md:py-4">
<div className="flex flex-col h-full md:h-auto">
<div className="flex justify-between items-start mb-2 md:mb-0">
<span className="font-medium truncate mr-2">
{pkgName}
</span>
<span className="text-sm md:hidden truncate">
{firstPackage.Status}
</span>
</div>
<div className="md:hidden text-sm text-muted-foreground text-center">
<span className="truncate">
{firstPackage.Version}
</span>
{firstPackage.NewVersion && (
<span className="truncate block">
{firstPackage.NewVersion}
</span>
)}
</div>
</div>
</TableCell>
<TableCell className="hidden md:table-cell md:w-1/4 py-4 text-left">
<div className="flex flex-col">
<span className="truncate">
{firstPackage.Version}
</span>
{firstPackage.NewVersion && (
<span className="text-sm text-muted-foreground truncate">
{firstPackage.NewVersion}
</span>
)}
</div>
</TableCell>
<TableCell className="hidden md:table-cell md:w-1/4 py-2 md:py-4 text-left">
<span className="truncate">
{firstPackage.Status}
</span>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
</div>
</div>
{/* Sticky Footer with Pagination */} {/* Sticky Footer with Pagination */}
<div className="sticky bottom-0 w-full bg-background border-t border-border py-4"> <div className="sticky bottom-0 w-full bg-background border-t border-border py-4">
<div className="container mx-auto px-2"> <div className="container mx-auto px-2">
<Pagination> <Pagination>
<PaginationContent className="flex flex-wrap justify-center items-center gap-1"> <PaginationContent className="flex flex-wrap justify-center items-center gap-1">
<PaginationItem> <PaginationItem>
<PaginationPrevious <PaginationPrevious
size="default" size="default"
onClick={() => handlePageChange(Math.max(currentPage - 1, 1))} onClick={() => handlePageChange(Math.max(currentPage - 1, 1))}
/> />
</PaginationItem> </PaginationItem>
{currentPage > 2 && ( {currentPage > 2 && (
<PaginationItem className="hidden sm:inline-block"> <PaginationItem className="hidden sm:inline-block">
<PaginationLink size="default" onClick={() => handlePageChange(1)}> <PaginationLink
1 size="default"
</PaginationLink> onClick={() => handlePageChange(1)}
</PaginationItem> >
)} 1
</PaginationLink>
</PaginationItem>
)}
{currentPage > 3 && ( {currentPage > 3 && (
<PaginationItem className="hidden sm:inline-flex items-center"> <PaginationItem className="hidden sm:inline-flex items-center">
<PaginationEllipsis /> <PaginationEllipsis />
</PaginationItem> </PaginationItem>
)} )}
{currentPage > 1 && ( {currentPage > 1 && (
<PaginationItem> <PaginationItem>
<PaginationLink <PaginationLink
size="default" size="default"
onClick={() => handlePageChange(currentPage - 1)} onClick={() => handlePageChange(currentPage - 1)}
> >
{currentPage - 1} {currentPage - 1}
</PaginationLink> </PaginationLink>
</PaginationItem> </PaginationItem>
)} )}
<PaginationItem> <PaginationItem>
<PaginationLink size="default" isActive> <PaginationLink size="default" isActive>
{currentPage} {currentPage}
</PaginationLink> </PaginationLink>
</PaginationItem> </PaginationItem>
{currentPage < totalPages && ( {currentPage < totalPages && (
<PaginationItem> <PaginationItem>
<PaginationLink <PaginationLink
size="default" size="default"
onClick={() => handlePageChange(currentPage + 1)} onClick={() => handlePageChange(currentPage + 1)}
> >
{currentPage + 1} {currentPage + 1}
</PaginationLink> </PaginationLink>
</PaginationItem> </PaginationItem>
)} )}
{currentPage < totalPages - 2 && ( {currentPage < totalPages - 2 && (
<PaginationItem className="hidden sm:inline-flex items-center"> <PaginationItem className="hidden sm:inline-flex items-center">
<PaginationEllipsis /> <PaginationEllipsis />
</PaginationItem> </PaginationItem>
)} )}
{currentPage < totalPages - 1 && ( {currentPage < totalPages - 1 && (
<PaginationItem className="hidden sm:inline-block"> <PaginationItem className="hidden sm:inline-block">
<PaginationLink <PaginationLink
size="default" size="default"
onClick={() => handlePageChange(totalPages)} onClick={() => handlePageChange(totalPages)}
> >
{totalPages} {totalPages}
</PaginationLink> </PaginationLink>
</PaginationItem> </PaginationItem>
)} )}
<PaginationItem> <PaginationItem>
<PaginationNext <PaginationNext
size="default" size="default"
onClick={() => handlePageChange(Math.min(currentPage + 1, totalPages))} onClick={() =>
/> handlePageChange(Math.min(currentPage + 1, totalPages))
</PaginationItem> }
</PaginationContent> />
</Pagination> </PaginationItem>
</div> </PaginationContent>
</div> </Pagination>
</div>
</div>
</div> </div>
); );
}; };

View File

@ -0,0 +1,88 @@
import { useState, useCallback, useRef } from 'react';
interface PackageStats {
stale: number;
missing: number;
built: number;
error: number;
queued: number;
building: number;
}
interface CountResponse {
lastUpdateTime: string;
counts: PackageStats;
}
interface PackageData {
Name: string;
Packages: {
[key: string]: {
Version: string;
NewVersion: string;
Status: string;
};
};
}
interface PackagesResponse {
total: number;
packages: PackageData[];
}
const usePackageData = () => {
const [stats, setStats] = useState<PackageStats | null>(null);
const [lastUpdated, setLastUpdated] = useState<string>('');
const [packages, setPackages] = useState<PackageData[]>([]);
const [totalCount, setTotalCount] = useState(0);
const [loading, setLoading] = useState(false);
const fetchStatsAttempted = useRef(false);
const fetchStats = useCallback(async () => {
if (fetchStatsAttempted.current || stats) return;
fetchStatsAttempted.current = true;
setLoading(true);
try {
const response = await fetch('/api/counts');
const data: CountResponse = await response.json();
setStats(data.counts);
const dt = new Date(data.lastUpdateTime);
setLastUpdated(dt.toLocaleString());
} catch (error) {
console.error('Failed to fetch stats:', error);
} finally {
setLoading(false);
}
}, [stats]);
const fetchPackages = useCallback(async (page: number, pageSize: number, search: string, filter: string) => {
setLoading(true);
try {
const filterParam = filter === "All" ? "" : filter;
const response = await fetch(
`/api/packages?page=${page}&pageSize=${pageSize}&search=${search}&filter=${filterParam}`
);
const data: PackagesResponse = await response.json();
setTotalCount(data.total);
setPackages(data.packages);
} catch (error) {
console.error("Error fetching packages:", error);
setPackages([]);
setTotalCount(0);
} finally {
setLoading(false);
}
}, []);
return {
stats,
lastUpdated,
fetchStats,
packages,
totalCount,
loading,
fetchPackages,
};
};
export default usePackageData;