diff --git a/package-lock.json b/package-lock.json index ad96cb1..8acbf48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 03e4391..266f566 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 3805b2a..77b1411 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { + diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx index 7a6b936..1c2ba7a 100644 --- a/src/components/header/header.tsx +++ b/src/components/header/header.tsx @@ -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 (
- +
@@ -168,6 +157,54 @@ const Header: React.FC = () => { )} + {/* Slide-out menu */} +
+
+ + +
+
+ + {/* Overlay to close menu when clicking outside */} + {isMenuOpen && ( +
setIsMenuOpen(false)} + >
+ )}
); }; diff --git a/src/components/pages/errored.tsx b/src/components/pages/errored.tsx new file mode 100644 index 0000000..93491df --- /dev/null +++ b/src/components/pages/errored.tsx @@ -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 ( +
+ {/* Table */} +
+
+
+ + + + Name + Version + Status + {isLoggedIn && Action} + + + + {loading ? ( + + + Loading... + + + ) : Object.keys(errPackages).length === 0 ? ( + + + No packages found. + + + ) : ( + errPackages.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} + + + {isLoggedIn && ( + + + + )} +
+ ); + }) + )} +
+
+
+
+
+ + {/* Rebuild Dialog */} + + + + Rebuild Package + +
+
+ + +
+
+ + + setRebuildData({ ...rebuildData, version: e.target.value }) + } + className="col-span-3" + /> +
+
+ + +
+
+ + + setRebuildData({ ...rebuildData, rebuild: checked }) + } + /> +
+
+ + + +
+
+
+ ); +}; + +export default Errored; \ No newline at end of file diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..32053fc --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/hooks/usePackageData.ts b/src/hooks/usePackageData.ts index 57669b6..0cf4b93 100644 --- a/src/hooks/usePackageData.ts +++ b/src/hooks/usePackageData.ts @@ -34,8 +34,10 @@ const usePackageData = () => { const [stats, setStats] = useState(null); const [lastUpdated, setLastUpdated] = useState(''); const [packages, setPackages] = useState([]); + const [pkg, setPackage] = useState(null); const [totalCount, setTotalCount] = useState(0); const [loading, setLoading] = useState(false); + const [errPackages, setErrPackages] = useState([]); const fetchStatsAttempted = useRef(false); const fetchStats = useCallback(async () => { @@ -83,6 +85,34 @@ const usePackageData = () => { 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 { stats, @@ -92,6 +122,10 @@ const usePackageData = () => { totalCount, loading, fetchPackages, + pkg, + fetchPackage, + errPackages, + fetchErrPackages, }; };