Add errored page, make menu a burger menu for space

This commit is contained in:
ferreo 2024-07-30 20:34:42 +01:00
parent 9c4ff25239
commit 6a7b9b3dc7
7 changed files with 420 additions and 23 deletions

30
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
@ -771,6 +772,35 @@
} }
} }
}, },
"node_modules/@radix-ui/react-checkbox": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.1.tgz",
"integrity": "sha512-0i/EKJ222Afa1FE0C6pNJxDq1itzcl3HChE9DwskA4th4KRse8ojx8a1nVcOjwJdbpDLcz7uol77yYnQNMHdKw==",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-presence": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-previous": "1.1.0",
"@radix-ui/react-use-size": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": { "node_modules/@radix-ui/react-collection": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",

View File

@ -12,6 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",

View File

@ -4,6 +4,7 @@ import { Route, Switch } from "wouter";
import Home from './components/pages/home'; import Home from './components/pages/home';
import Packages from './components/pages/packages'; import Packages from './components/pages/packages';
import Queue from './components/pages/queue'; import Queue from './components/pages/queue';
import Errored from './components/pages/errored';
function App() { function App() {
@ -14,6 +15,7 @@ function App() {
<Switch> <Switch>
<Route path="/queue" component={Queue} /> <Route path="/queue" component={Queue} />
<Route path="/packages" component={Packages} /> <Route path="/packages" component={Packages} />
<Route path="/errored" component={Errored} />
<Route component={Home}/> <Route component={Home}/>
</Switch> </Switch>
</> </>

View File

@ -11,7 +11,7 @@ import {
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, Menu, X } from "lucide-react";
import { Alert, AlertDescription } from "../ui/alert"; import { Alert, AlertDescription } from "../ui/alert";
const Header: React.FC = () => { const Header: React.FC = () => {
@ -21,12 +21,15 @@ const Header: React.FC = () => {
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 [isMenuOpen, setIsMenuOpen] = useState(false);
const checkLoginStatus = () => { const checkLoginStatus = async () => {
const ptCookie = document.cookie const r = await fetch("/api/isloggedin");
.split("; ") let isLoggedIn = false;
.find((row) => row.startsWith("pt=")); if (r.status === 200) {
setIsLoggedIn(!!ptCookie); isLoggedIn = true;
}
setIsLoggedIn(isLoggedIn);
}; };
useEffect(() => { useEffect(() => {
@ -77,23 +80,9 @@ const Header: React.FC = () => {
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> <Button variant="ghost" onClick={() => setIsMenuOpen(true)}>
<ul className="flex space-x-4"> <Menu className="h-6 w-6" />
<li> </Button>
<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>
</nav>
<div className="flex-grow flex justify-center"> <div className="flex-grow flex justify-center">
<Link href="/"> <Link href="/">
@ -168,6 +157,54 @@ const Header: React.FC = () => {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}
{/* Slide-out menu */}
<div
className={`fixed inset-y-0 left-0 w-64 bg-background shadow-lg transform ${
isMenuOpen ? "translate-x-0" : "-translate-x-full"
} transition-transform duration-300 ease-in-out z-50`}
>
<div className="p-4 relative">
<Button
variant="ghost"
onClick={() => setIsMenuOpen(false)}
className="absolute top-2 right-2 p-1"
>
<X className="h-6 w-6" />
</Button>
<nav className="mt-10">
<ul className="space-y-4">
<li>
<Link href="/" className="text-foreground hover:text-primary block">
Home
</Link>
</li>
<li>
<Link href="/queue" className="text-foreground hover:text-primary block">
Queue
</Link>
</li>
<li>
<Link href="/packages" className="text-foreground hover:text-primary block">
Packages
</Link>
</li>
<li>
<Link href="/errored" className="text-foreground hover:text-primary block">
Errored
</Link>
</li>
</ul>
</nav>
</div>
</div>
{/* Overlay to close menu when clicking outside */}
{isMenuOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40"
onClick={() => setIsMenuOpen(false)}
></div>
)}
</header> </header>
); );
}; };

View File

@ -0,0 +1,265 @@
import React, { useState, useEffect, useRef, useCallback } from "react";
import { useMediaQuery } from "react-responsive";
import usePackageData from "../../hooks/usePackageData";
import { useLocation } from "wouter";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../ui/table";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "../ui/dialog";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Checkbox } from "../ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
const Errored: React.FC = () => {
const { errPackages, loading, fetchErrPackages } = usePackageData();
const isMobile = useMediaQuery({ maxWidth: 767 });
const [, setPageSize] = useState(isMobile ? 100 : 250);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [, setLocation] = useLocation();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [, setSelectedPackage] = useState(null);
const [rebuildData, setRebuildData] = useState({
packageName: "",
version: "",
buildType: "v3",
rebuild: false,
});
const isInitialMount = useRef(true);
const checkLoginStatus = async () => {
const r = await fetch("/api/isloggedin");
let loggedIn = false;
if (r.status === 200) {
loggedIn = true;
}
setIsLoggedIn(loggedIn);
};
useEffect(() => {
setPageSize(isMobile ? 100 : 250);
}, [isMobile]);
const loadPackages = useCallback(() => {
fetchErrPackages();
}, [fetchErrPackages]);
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
loadPackages();
checkLoginStatus();
}
}, [loadPackages]);
const handleRebuildClick = (pkg: any) => {
const firstPackage = Object.values(pkg.Packages)[0] as any;
setSelectedPackage(pkg);
setRebuildData({
packageName: pkg.Name,
version: firstPackage.NewVersion || firstPackage.Version,
buildType: "lto",
rebuild: false,
});
setIsDialogOpen(true);
};
const handleDialogSubmit = async () => {
try {
const response = await fetch("/api/auth/triggerbuild", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(rebuildData),
});
const data = await response.json();
if (response.ok) {
setIsDialogOpen(false);
setLocation(`/queue?search=${rebuildData.packageName}`)
} else {
alert(data.error);
}
} catch (error) {
alert("An error occurred while submitting the build request.");
}
};
return (
<div className="flex flex-col min-h-screen">
{/* 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>
{isLoggedIn && <TableHead className="w-1/4 text-left">Action</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={isLoggedIn ? 4 : 3} className="text-center">
Loading...
</TableCell>
</TableRow>
) : Object.keys(errPackages).length === 0 ? (
<TableRow>
<TableCell colSpan={isLoggedIn ? 4 : 3} className="text-center">
No packages found.
</TableCell>
</TableRow>
) : (
errPackages.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>
{isLoggedIn && (
<TableCell className="py-2 md:py-4 text-right">
<Button onClick={() => handleRebuildClick(pkg)}>Rebuild</Button>
</TableCell>
)}
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
</div>
</div>
{/* Rebuild Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Rebuild Package</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="packageName" className="text-right">
Package Name
</label>
<Input
id="packageName"
value={rebuildData.packageName}
className="col-span-3"
readOnly
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="version" className="text-right">
Version
</label>
<Input
id="version"
value={rebuildData.version}
onChange={(e) =>
setRebuildData({ ...rebuildData, version: e.target.value })
}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="buildType" className="text-right">
Build Type
</label>
<Select
value={rebuildData.buildType}
onValueChange={(value) =>
setRebuildData({ ...rebuildData, buildType: value })
}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select build type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="v3">v3</SelectItem>
<SelectItem value="lto">lto</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="rebuild" className="text-right">
Rebuild
</label>
<Checkbox
id="rebuild"
checked={rebuildData.rebuild}
onCheckedChange={(checked: boolean) =>
setRebuildData({ ...rebuildData, rebuild: checked })
}
/>
</div>
</div>
<DialogFooter>
<Button onClick={handleDialogSubmit}>Submit</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
export default Errored;

View File

@ -0,0 +1,28 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "../../lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -34,8 +34,10 @@ const usePackageData = () => {
const [stats, setStats] = useState<PackageStats | null>(null); const [stats, setStats] = useState<PackageStats | null>(null);
const [lastUpdated, setLastUpdated] = useState<string>(''); const [lastUpdated, setLastUpdated] = useState<string>('');
const [packages, setPackages] = useState<PackageData[]>([]); const [packages, setPackages] = useState<PackageData[]>([]);
const [pkg, setPackage] = useState<PackageData | null>(null);
const [totalCount, setTotalCount] = useState(0); const [totalCount, setTotalCount] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [errPackages, setErrPackages] = useState<PackageData[]>([]);
const fetchStatsAttempted = useRef(false); const fetchStatsAttempted = useRef(false);
const fetchStats = useCallback(async () => { const fetchStats = useCallback(async () => {
@ -83,6 +85,34 @@ const usePackageData = () => {
setLoading(false); setLoading(false);
} }
}, []); }, []);
const fetchPackage = useCallback(async (name: string) => {
setLoading(true);
try {
const response = await fetch(`/api/package/${name}`);
const data: PackageData = await response.json();
setPackage(data);
} catch (error) {
console.error("Error fetching package:", error);
setPackage(null);
} finally {
setLoading(false);
}
}, []);
const fetchErrPackages = useCallback(async () => {
setLoading(true);
try {
const response = await fetch('/api/errpackages');
const data: PackageData[] = await response.json();
setErrPackages(data);
} catch (error) {
console.error("Error fetching errpackages:", error);
setErrPackages([]);
} finally {
setLoading(false);
}
}, []);
return { return {
stats, stats,
@ -92,6 +122,10 @@ const usePackageData = () => {
totalCount, totalCount,
loading, loading,
fetchPackages, fetchPackages,
pkg,
fetchPackage,
errPackages,
fetchErrPackages,
}; };
}; };