Add errored page, make menu a burger menu for space
This commit is contained in:
parent
9c4ff25239
commit
6a7b9b3dc7
30
package-lock.json
generated
30
package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
|
||||
|
@ -12,6 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
|
@ -4,6 +4,7 @@ import { Route, Switch } from "wouter";
|
||||
import Home from './components/pages/home';
|
||||
import Packages from './components/pages/packages';
|
||||
import Queue from './components/pages/queue';
|
||||
import Errored from './components/pages/errored';
|
||||
|
||||
function App() {
|
||||
|
||||
@ -14,6 +15,7 @@ function App() {
|
||||
<Switch>
|
||||
<Route path="/queue" component={Queue} />
|
||||
<Route path="/packages" component={Packages} />
|
||||
<Route path="/errored" component={Errored} />
|
||||
<Route component={Home}/>
|
||||
</Switch>
|
||||
</>
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
import { Dialog, DialogContent, DialogTrigger } from "../ui/dialog";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { AlertCircle, Menu, X } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "../ui/alert";
|
||||
|
||||
const Header: React.FC = () => {
|
||||
@ -21,12 +21,15 @@ const Header: React.FC = () => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const checkLoginStatus = () => {
|
||||
const ptCookie = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("pt="));
|
||||
setIsLoggedIn(!!ptCookie);
|
||||
const checkLoginStatus = async () => {
|
||||
const r = await fetch("/api/isloggedin");
|
||||
let isLoggedIn = false;
|
||||
if (r.status === 200) {
|
||||
isLoggedIn = true;
|
||||
}
|
||||
setIsLoggedIn(isLoggedIn);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -77,23 +80,9 @@ const Header: React.FC = () => {
|
||||
|
||||
return (
|
||||
<header className="flex items-center justify-between p-4 bg-background text-foreground shadow-md">
|
||||
<nav>
|
||||
<ul className="flex space-x-4">
|
||||
<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>
|
||||
</nav>
|
||||
<Button variant="ghost" onClick={() => setIsMenuOpen(true)}>
|
||||
<Menu className="h-6 w-6" />
|
||||
</Button>
|
||||
|
||||
<div className="flex-grow flex justify-center">
|
||||
<Link href="/">
|
||||
@ -168,6 +157,54 @@ const Header: React.FC = () => {
|
||||
</DialogContent>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
265
src/components/pages/errored.tsx
Normal file
265
src/components/pages/errored.tsx
Normal 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;
|
28
src/components/ui/checkbox.tsx
Normal file
28
src/components/ui/checkbox.tsx
Normal 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 }
|
@ -34,8 +34,10 @@ const usePackageData = () => {
|
||||
const [stats, setStats] = useState<PackageStats | null>(null);
|
||||
const [lastUpdated, setLastUpdated] = useState<string>('');
|
||||
const [packages, setPackages] = useState<PackageData[]>([]);
|
||||
const [pkg, setPackage] = useState<PackageData | null>(null);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errPackages, setErrPackages] = useState<PackageData[]>([]);
|
||||
const fetchStatsAttempted = useRef(false);
|
||||
|
||||
const fetchStats = useCallback(async () => {
|
||||
@ -84,6 +86,34 @@ const usePackageData = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
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 {
|
||||
stats,
|
||||
lastUpdated,
|
||||
@ -92,6 +122,10 @@ const usePackageData = () => {
|
||||
totalCount,
|
||||
loading,
|
||||
fetchPackages,
|
||||
pkg,
|
||||
fetchPackage,
|
||||
errPackages,
|
||||
fetchErrPackages,
|
||||
};
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user