From 10d73334547e3f4db46319912b9daf6d411ed8c4 Mon Sep 17 00:00:00 2001 From: ferreo Date: Mon, 29 Jul 2024 17:53:52 +0100 Subject: [PATCH] Optimise server calls to not duplicate --- src/components/pages/home.tsx | 53 +-- src/components/pages/packages.tsx | 362 +++++++++--------- src/hooks/usePackageData.ts | 88 +++++ .../pages => hooks}/useSearchParams.ts | 0 4 files changed, 279 insertions(+), 224 deletions(-) create mode 100644 src/hooks/usePackageData.ts rename src/{components/pages => hooks}/useSearchParams.ts (100%) diff --git a/src/components/pages/home.tsx b/src/components/pages/home.tsx index a00f48b..a53d8a9 100644 --- a/src/components/pages/home.tsx +++ b/src/components/pages/home.tsx @@ -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(null); - const [lastUpdated, setLastUpdated] = useState(''); + 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
Loading...
; + } + return (

Welcome to the PikaOS Package Builder!

@@ -66,8 +33,8 @@ const Home: React.FC = () => { {card.title} -

{card.value !== undefined ? card.value : 'Loading...'}

-

{card.description}

+

{card.value !== undefined ? card.value : 'N/A'}

+

{card.description}

))} diff --git a/src/components/pages/packages.tsx b/src/components/pages/packages.tsx index e1a7677..c1f406a 100644 --- a/src/components/pages/packages.tsx +++ b/src/components/pages/packages.tsx @@ -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([]); - 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) => { - 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 = () => {
-{/* Table */} -
-
-
- - - - Name - Version - Status - - - - {loading ? ( - - - Loading... - - - ) : ( - packages.map((pkg) => { - const firstPackage = Object.values(pkg.Packages)[0]; - return ( - - -
-
- {pkg.Name} - {firstPackage.Status} -
-
- {firstPackage.Version} - {firstPackage.NewVersion && ( - → {firstPackage.NewVersion} - )} -
-
-
- -
- {firstPackage.Version} - {firstPackage.NewVersion && ( - - → {firstPackage.NewVersion} - - )} -
-
- - {firstPackage.Status} - + {/* Table */} +
+
+
+
+ + + Name + Version + Status - ); - }) - )} - -
-
-
-
+ + + {loading ? ( + + + Loading... + + +) : Object.keys(packages).length === 0 ? ( + + + No packages found. + + +) : ( + Object.entries(packages).map(([pkgName, pkgInfo]) => { + const firstPackage = Object.values(pkgInfo.Packages)[0]; + return ( + + +
+
+ + {pkgName} + + + {firstPackage.Status} + +
+
+ + {firstPackage.Version} + + {firstPackage.NewVersion && ( + + → {firstPackage.NewVersion} + + )} +
+
+
+ +
+ + {firstPackage.Version} + + {firstPackage.NewVersion && ( + + → {firstPackage.NewVersion} + + )} +
+
+ + + {firstPackage.Status} + + +
+ ); + }) +)} +
+ + + + -{/* Sticky Footer with Pagination */} -
-
- - - - handlePageChange(Math.max(currentPage - 1, 1))} - /> - + {/* Sticky Footer with Pagination */} +
+
+ + + + handlePageChange(Math.max(currentPage - 1, 1))} + /> + - {currentPage > 2 && ( - - handlePageChange(1)}> - 1 - - - )} + {currentPage > 2 && ( + + handlePageChange(1)} + > + 1 + + + )} - {currentPage > 3 && ( - - - - )} + {currentPage > 3 && ( + + + + )} - {currentPage > 1 && ( - - handlePageChange(currentPage - 1)} - > - {currentPage - 1} - - - )} + {currentPage > 1 && ( + + handlePageChange(currentPage - 1)} + > + {currentPage - 1} + + + )} - - - {currentPage} - - + + + {currentPage} + + - {currentPage < totalPages && ( - - handlePageChange(currentPage + 1)} - > - {currentPage + 1} - - - )} + {currentPage < totalPages && ( + + handlePageChange(currentPage + 1)} + > + {currentPage + 1} + + + )} - {currentPage < totalPages - 2 && ( - - - - )} + {currentPage < totalPages - 2 && ( + + + + )} - {currentPage < totalPages - 1 && ( - - handlePageChange(totalPages)} - > - {totalPages} - - - )} + {currentPage < totalPages - 1 && ( + + handlePageChange(totalPages)} + > + {totalPages} + + + )} - - handlePageChange(Math.min(currentPage + 1, totalPages))} - /> - - - -
-
+ + + handlePageChange(Math.min(currentPage + 1, totalPages)) + } + /> + +
+
+
+
); }; diff --git a/src/hooks/usePackageData.ts b/src/hooks/usePackageData.ts new file mode 100644 index 0000000..00f13d7 --- /dev/null +++ b/src/hooks/usePackageData.ts @@ -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(null); + const [lastUpdated, setLastUpdated] = useState(''); + const [packages, setPackages] = useState([]); + 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; \ No newline at end of file diff --git a/src/components/pages/useSearchParams.ts b/src/hooks/useSearchParams.ts similarity index 100% rename from src/components/pages/useSearchParams.ts rename to src/hooks/useSearchParams.ts