Add build queue
This commit is contained in:
parent
10d7333454
commit
a67969d7fe
@ -11,6 +11,7 @@ function App() {
|
|||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
<Switch>
|
<Switch>
|
||||||
|
<Route path="/queue" component={Queue} />
|
||||||
<Route path="/packages" component={Packages} />
|
<Route path="/packages" component={Packages} />
|
||||||
<Route component={Home}/>
|
<Route component={Home}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
@ -1,92 +1,108 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { Link, useLocation } from 'wouter';
|
import { Link, useLocation } from "wouter";
|
||||||
import { Button } from '../ui/button';
|
import { Button } from "../ui/button";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '../ui/dropdown-menu';
|
} from "../ui/dropdown-menu";
|
||||||
import { Dialog, DialogContent, DialogTrigger } from '../ui/dialog';
|
import { Dialog, DialogContent, DialogTrigger } from "../ui/dialog";
|
||||||
import { Input } from '../ui/input';
|
import { Input } from "../ui/input";
|
||||||
import { Label } from '../ui/label';
|
import { Label } from "../ui/label";
|
||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle } from "lucide-react";
|
||||||
import { Alert, AlertDescription } from '../ui/alert';
|
import { Alert, AlertDescription } from "../ui/alert";
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
const Header: React.FC = () => {
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
const [, setLocation] = useLocation();
|
const [, setLocation] = useLocation();
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState("");
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState("");
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
const checkLoginStatus = () => {
|
const checkLoginStatus = () => {
|
||||||
const ptCookie = document.cookie.split('; ').find(row => row.startsWith('pt='));
|
const ptCookie = document.cookie
|
||||||
|
.split("; ")
|
||||||
|
.find((row) => row.startsWith("pt="));
|
||||||
setIsLoggedIn(!!ptCookie);
|
setIsLoggedIn(!!ptCookie);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkLoginStatus();
|
checkLoginStatus();
|
||||||
window.addEventListener('focus', checkLoginStatus);
|
window.addEventListener("focus", checkLoginStatus);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('focus', checkLoginStatus);
|
window.removeEventListener("focus", checkLoginStatus);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('username', username);
|
formData.append("username", username);
|
||||||
formData.append('password', password);
|
formData.append("password", password);
|
||||||
|
|
||||||
const response = await fetch('/api/login', {
|
const response = await fetch("/api/login", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
throw new Error(errorData.message || 'Login failed');
|
throw new Error(errorData.message || "Login failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
checkLoginStatus();
|
checkLoginStatus();
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
setUsername('');
|
setUsername("");
|
||||||
setPassword('');
|
setPassword("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Login failed. Please try again.');
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Login failed. Please try again."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
document.cookie = 'pt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
document.cookie = "pt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||||
setIsLoggedIn(false);
|
setIsLoggedIn(false);
|
||||||
setLocation('/');
|
setLocation("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="flex items-center justify-between p-4 bg-background text-foreground shadow-md">
|
<header className="flex items-center justify-between p-4 bg-background text-foreground shadow-md">
|
||||||
<nav>
|
<nav>
|
||||||
<ul className="flex space-x-4">
|
<ul className="flex space-x-4">
|
||||||
<li><Link href="/" className="text-foreground hover:text-primary">Home</Link></li>
|
<li>
|
||||||
<li><Link href="/packages" className="text-foreground hover:text-primary">Packages</Link></li>
|
<Link href="/queue" className="text-foreground hover:text-primary">
|
||||||
|
Queue
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/packages"
|
||||||
|
className="text-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
Packages
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex-grow flex justify-center">
|
<div className="flex-grow flex justify-center">
|
||||||
|
<Link href="/">
|
||||||
<img
|
<img
|
||||||
src="https://git.pika-os.com/website/pika-branding/raw/branch/main/logos/pika-logo-text-dark.svg"
|
src="https://git.pika-os.com/website/pika-branding/raw/branch/main/logos/pika-logo-text-dark.svg"
|
||||||
alt="PikaOS Logo"
|
alt="PikaOS Logo"
|
||||||
className="h-16 w-auto max-w-full"
|
className="h-16 w-auto max-w-full"
|
||||||
/>
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
@ -98,18 +114,18 @@ const Header: React.FC = () => {
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem onSelect={() => setLocation('/profile')}>
|
<DropdownMenuItem onSelect={() => setLocation("/profile")}>
|
||||||
Profile
|
Profile
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onSelect={handleLogout}>
|
<DropdownMenuItem onSelect={handleLogout}>Logout</DropdownMenuItem>
|
||||||
Logout
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : (
|
) : (
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" onClick={() => setIsDialogOpen(true)}>Login</Button>
|
<Button variant="outline" onClick={() => setIsDialogOpen(true)}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-[425px] bg-background text-foreground">
|
<DialogContent className="sm:max-w-[425px] bg-background text-foreground">
|
||||||
<form onSubmit={handleLogin} className="space-y-4">
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
@ -137,12 +153,17 @@ const Header: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<Alert variant="destructive" className="mt-4 bg-destructive text-destructive-foreground">
|
<Alert
|
||||||
|
variant="destructive"
|
||||||
|
className="mt-4 bg-destructive text-destructive-foreground"
|
||||||
|
>
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<AlertDescription>{error}</AlertDescription>
|
<AlertDescription>{error}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<Button type="submit" className="w-full">Login</Button>
|
<Button type="submit" className="w-full">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
291
src/components/pages/queue.tsx
Normal file
291
src/components/pages/queue.tsx
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import useSearchParams from "../../hooks/useSearchParams";
|
||||||
|
import useQueueData from "../../hooks/useQueueData";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "../ui/select";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "../ui/table";
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from "../ui/pagination";
|
||||||
|
|
||||||
|
const Queue: React.FC = () => {
|
||||||
|
const { getParam, setParam } = useSearchParams();
|
||||||
|
const { packages, totalCount, loading, fetchPackages } = useQueueData();
|
||||||
|
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 isInitialMount = useRef(true);
|
||||||
|
const prevDependencies = useRef({ currentPage, pageSize, search, filter });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPageSize(isMobile ? 100 : 250);
|
||||||
|
}, [isMobile]);
|
||||||
|
|
||||||
|
const loadPackages = useCallback(() => {
|
||||||
|
fetchPackages(currentPage, pageSize, search, filter);
|
||||||
|
setParam("page", currentPage.toString());
|
||||||
|
setParam("search", search);
|
||||||
|
setParam("filter", filter);
|
||||||
|
}, [currentPage, pageSize, search, filter, fetchPackages, setParam]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(totalCount / pageSize);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
{/* Sticky Header */}
|
||||||
|
<div className="sticky top-0 z-10 bg-background border-b border-border">
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Search packages..."
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
className="max-w-sm"
|
||||||
|
/>
|
||||||
|
<Select value={filter} onValueChange={handleFilterChange}>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue placeholder="All" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="All">All</SelectItem>
|
||||||
|
{[
|
||||||
|
"Stale",
|
||||||
|
"Error",
|
||||||
|
"Missing",
|
||||||
|
"Current",
|
||||||
|
"Queued",
|
||||||
|
"Building",
|
||||||
|
].map((option) => (
|
||||||
|
<SelectItem key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
) : Object.keys(packages).length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={3} className="text-center">
|
||||||
|
No packages found.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
Object.entries(packages).map(([pkgName, pkgInfo]) => {
|
||||||
|
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} - {pkgInfo.Type}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm md:hidden truncate">
|
||||||
|
{pkgInfo.Status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="md:hidden text-sm text-muted-foreground text-center">
|
||||||
|
<span className="truncate">
|
||||||
|
{pkgInfo.BuildVersion}
|
||||||
|
</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">
|
||||||
|
{pkgInfo.BuildVersion}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden md:table-cell md:w-1/4 py-2 md:py-4 text-left">
|
||||||
|
<span className="truncate">{pkgInfo.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>
|
||||||
|
|
||||||
|
{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 > 1 && (
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink
|
||||||
|
size="default"
|
||||||
|
onClick={() => handlePageChange(currentPage - 1)}
|
||||||
|
>
|
||||||
|
{currentPage - 1}
|
||||||
|
</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 - 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
size="default"
|
||||||
|
onClick={() =>
|
||||||
|
handlePageChange(Math.min(currentPage + 1, totalPages))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Queue;
|
95
src/hooks/useQueueData.ts
Normal file
95
src/hooks/useQueueData.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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 QueueData {
|
||||||
|
Source: PackageData;
|
||||||
|
Type: string;
|
||||||
|
BuildVersion: string;
|
||||||
|
Status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PackageData {
|
||||||
|
Name: string;
|
||||||
|
Packages: {
|
||||||
|
[key: string]: {
|
||||||
|
Version: string;
|
||||||
|
NewVersion: string;
|
||||||
|
Status: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PackagesResponse {
|
||||||
|
total: number;
|
||||||
|
packages: Record<string, QueueData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usePackageData = () => {
|
||||||
|
const [stats, setStats] = useState<PackageStats | null>(null);
|
||||||
|
const [lastUpdated, setLastUpdated] = useState<string>('');
|
||||||
|
const [packages, setQueuePackages] = useState<Record<string, QueueData>>({});
|
||||||
|
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/queue?page=${page}&pageSize=${pageSize}&search=${search}&filter=${filterParam}`
|
||||||
|
);
|
||||||
|
const data: PackagesResponse = await response.json();
|
||||||
|
setTotalCount(data.total);
|
||||||
|
setQueuePackages(data.packages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching packages:", error);
|
||||||
|
setQueuePackages({});
|
||||||
|
setTotalCount(0);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stats,
|
||||||
|
lastUpdated,
|
||||||
|
fetchStats,
|
||||||
|
packages,
|
||||||
|
totalCount,
|
||||||
|
loading,
|
||||||
|
fetchPackages,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePackageData;
|
Loading…
Reference in New Issue
Block a user