Optimise server calls to not duplicate
This commit is contained in:
parent
cb46826770
commit
10d7333454
@ -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>
|
||||
))}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
88
src/hooks/usePackageData.ts
Normal file
88
src/hooks/usePackageData.ts
Normal 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;
|
Loading…
Reference in New Issue
Block a user