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';
interface CountResponse {
lastUpdateTime: string;
counts: PackageStats;
}
interface PackageStats {
stale: number;
missing: number;
built: number;
error: number;
queued: number;
building: number;
}
import usePackageData from '../../hooks/usePackageData';
const Home: React.FC = () => {
const [stats, setStats] = useState<PackageStats | null>(null);
const [lastUpdated, setLastUpdated] = useState<string>('');
const { stats, lastUpdated, loading, fetchStats } = usePackageData();
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]);
const statCards = [
{ 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' },
];
if (loading) {
return <div>Loading...</div>;
}
return (
<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>
@ -66,8 +33,8 @@ const Home: React.FC = () => {
<CardTitle>{card.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-4xl font-bold mb-2">{card.value !== undefined ? card.value : 'Loading...'}</p>
<p className="text-sm text-card-foreground" >{card.description}</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>
</CardContent>
</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 useSearchParams from "./useSearchParams";
import useSearchParams from "../../hooks/useSearchParams";
import usePackageData from "../../hooks/usePackageData";
import { Input } from "../ui/input";
import {
Select,
@ -27,79 +28,55 @@ import {
PaginationPrevious,
} from "../ui/pagination";
interface Package {
Name: string;
Packages: {
[key: string]: {
Version: string;
NewVersion: string;
Status: string;
};
};
}
const Packages: React.FC = () => {
const { getParam, setParam } = useSearchParams();
const [packages, setPackages] = useState<Package[]>([]);
const [totalCount, setTotalCount] = useState(0);
const [currentPage, setCurrentPage] = useState(
parseInt(getParam("page") || "1", 10)
);
const { packages, totalCount, loading, fetchPackages } = usePackageData();
const [currentPage, setCurrentPage] = useState(parseInt(getParam("page") || "1", 10));
const isMobile = useMediaQuery({ maxWidth: 767 });
const [pageSize, setPageSize] = useState(isMobile ? 100 : 250);
const [search, setSearch] = useState(getParam("search") || "");
const [filter, setFilter] = useState(getParam("filter") || "All");
const [loading, setLoading] = useState(true);
const isInitialMount = useRef(true);
const prevDependencies = useRef({ currentPage, pageSize, search, filter });
useEffect(() => {
setPageSize(isMobile ? 100 : 250);
}, [isMobile]);
useEffect(() => {
fetchPackages();
const loadPackages = useCallback(() => {
fetchPackages(currentPage, pageSize, search, filter);
setParam("page", currentPage.toString());
setParam("search", search);
setParam("filter", filter);
}, [currentPage, search, filter, pageSize]);
}, [currentPage, pageSize, search, filter, fetchPackages, setParam]);
const fetchPackages = async () => {
setLoading(true);
try {
const filterParam = filter === "All" ? "" : filter;
const response = await fetch(
`/api/packages?page=${currentPage}&pageSize=${pageSize}&search=${search}&filter=${filterParam}`
);
const data = await response.json();
setTotalCount(data.total);
setPackages(Object.values(data.packages));
} catch (error) {
setTotalCount(0);
console.error("Error fetching packages:", error);
} finally {
setLoading(false);
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
loadPackages();
} else {
const { currentPage: prevPage, pageSize: prevSize, search: prevSearch, filter: prevFilter } = prevDependencies.current;
if (currentPage !== prevPage || pageSize !== prevSize || search !== prevSearch || filter !== prevFilter) {
loadPackages();
prevDependencies.current = { currentPage, pageSize, search, filter };
}
}
};
}, [loadPackages, currentPage, pageSize, search, filter]);
const handleFilterChange = (value: string) => {
const newFilter = value === "All" ? "" : value;
setFilter(newFilter);
setCurrentPage(1);
setParam("filter", newFilter);
setParam("page", "1");
};
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newSearch = e.target.value;
setSearch(newSearch);
setSearch(e.target.value);
setCurrentPage(1);
setParam("search", newSearch);
setParam("page", "1");
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
setParam("page", page.toString());
};
const totalPages = Math.ceil(totalCount / pageSize);
@ -140,151 +117,174 @@ const Packages: React.FC = () => {
</div>
</div>
{/* Table */}
<div className="flex-grow overflow-hidden">
<div className="container mx-0 px-0 h-full">
<div className="h-full">
<Table className="w-full">
<TableHeader className="top-0 bg-background z-10 hidden md:table-header-group">
<TableRow>
<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">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>
{/* Table */}
<div className="flex-grow overflow-hidden">
<div className="container mx-0 px-0 h-full">
<div className="h-full">
<Table className="w-full">
<TableHeader className="top-0 bg-background z-10 hidden md:table-header-group">
<TableRow>
<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">Status</TableHead>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
</div>
</div>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={3} className="text-center">
Loading...
</TableCell>
</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 */}
<div className="sticky bottom-0 w-full bg-background border-t border-border py-4">
<div className="container mx-auto px-2">
<Pagination>
<PaginationContent className="flex flex-wrap justify-center items-center gap-1">
<PaginationItem>
<PaginationPrevious
size="default"
onClick={() => handlePageChange(Math.max(currentPage - 1, 1))}
/>
</PaginationItem>
{/* Sticky Footer with Pagination */}
<div className="sticky bottom-0 w-full bg-background border-t border-border py-4">
<div className="container mx-auto px-2">
<Pagination>
<PaginationContent className="flex flex-wrap justify-center items-center gap-1">
<PaginationItem>
<PaginationPrevious
size="default"
onClick={() => handlePageChange(Math.max(currentPage - 1, 1))}
/>
</PaginationItem>
{currentPage > 2 && (
<PaginationItem className="hidden sm:inline-block">
<PaginationLink size="default" onClick={() => handlePageChange(1)}>
1
</PaginationLink>
</PaginationItem>
)}
{currentPage > 2 && (
<PaginationItem className="hidden sm:inline-block">
<PaginationLink
size="default"
onClick={() => handlePageChange(1)}
>
1
</PaginationLink>
</PaginationItem>
)}
{currentPage > 3 && (
<PaginationItem className="hidden sm:inline-flex items-center">
<PaginationEllipsis />
</PaginationItem>
)}
{currentPage > 3 && (
<PaginationItem className="hidden sm:inline-flex items-center">
<PaginationEllipsis />
</PaginationItem>
)}
{currentPage > 1 && (
<PaginationItem>
<PaginationLink
size="default"
onClick={() => handlePageChange(currentPage - 1)}
>
{currentPage - 1}
</PaginationLink>
</PaginationItem>
)}
{currentPage > 1 && (
<PaginationItem>
<PaginationLink
size="default"
onClick={() => handlePageChange(currentPage - 1)}
>
{currentPage - 1}
</PaginationLink>
</PaginationItem>
)}
<PaginationItem>
<PaginationLink size="default" isActive>
{currentPage}
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink size="default" isActive>
{currentPage}
</PaginationLink>
</PaginationItem>
{currentPage < totalPages && (
<PaginationItem>
<PaginationLink
size="default"
onClick={() => handlePageChange(currentPage + 1)}
>
{currentPage + 1}
</PaginationLink>
</PaginationItem>
)}
{currentPage < totalPages && (
<PaginationItem>
<PaginationLink
size="default"
onClick={() => handlePageChange(currentPage + 1)}
>
{currentPage + 1}
</PaginationLink>
</PaginationItem>
)}
{currentPage < totalPages - 2 && (
<PaginationItem className="hidden sm:inline-flex items-center">
<PaginationEllipsis />
</PaginationItem>
)}
{currentPage < totalPages - 2 && (
<PaginationItem className="hidden sm:inline-flex items-center">
<PaginationEllipsis />
</PaginationItem>
)}
{currentPage < totalPages - 1 && (
<PaginationItem className="hidden sm:inline-block">
<PaginationLink
size="default"
onClick={() => handlePageChange(totalPages)}
>
{totalPages}
</PaginationLink>
</PaginationItem>
)}
{currentPage < totalPages - 1 && (
<PaginationItem className="hidden sm:inline-block">
<PaginationLink
size="default"
onClick={() => handlePageChange(totalPages)}
>
{totalPages}
</PaginationLink>
</PaginationItem>
)}
<PaginationItem>
<PaginationNext
size="default"
onClick={() => handlePageChange(Math.min(currentPage + 1, totalPages))}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</div>
<PaginationItem>
<PaginationNext
size="default"
onClick={() =>
handlePageChange(Math.min(currentPage + 1, totalPages))
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</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;