import React, { useState } from "react"; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, Typography, Link, Box, Tab, Tabs, IconButton, Chip, FormControlLabel, Checkbox, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import DeleteIcon from "@mui/icons-material/Delete"; import RefreshIcon from "@mui/icons-material/Refresh"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import BuildLogsTab from "components/package/BuildLogsTab"; import ChangesTab from "components/package/ChangesTab"; import EventsTab from "components/package/EventsTab"; import AutoRefreshControl from "components/common/AutoRefreshControl"; import { useRepository } from "hooks/useRepository"; import { useAuth } from "hooks/useAuth"; import { useNotification } from "hooks/useNotification"; import { useAutoRefresh } from "hooks/useAutoRefresh"; import { Client } from "api/client/AhrimanClient"; import { ApiError } from "api/client/ApiError"; import { QueryKeys } from "api/QueryKeys"; import { StatusHeaderStyles } from "theme/status/StatusHeaderStyles"; import { formatTimestamp } from "components/common/formatTimestamp"; import type { AutoRefreshInterval } from "api/types/AutoRefreshInterval"; import type { Dependencies } from "api/types/Dependencies"; import type { PackageProperties } from "api/types/PackageProperties"; import type { PackageStatus } from "api/types/PackageStatus"; import type { Patch } from "api/types/Patch"; import type { RepositoryId } from "api/types/RepositoryId"; interface PackageInfoDialogProps { packageBase: string | null; open: boolean; onClose: () => void; autorefreshIntervals: AutoRefreshInterval[]; } function listToString(items: string[]): React.ReactNode { const unique = [...new Set(items)].sort(); return unique.map((item, i) => ( {item} {i < unique.length - 1 &&
}
)); } export default function PackageInfoDialog({ packageBase, open, onClose, autorefreshIntervals }: PackageInfoDialogProps): React.JSX.Element { const { current } = useRepository(); const { enabled: authEnabled, username } = useAuth(); const { showSuccess, showError } = useNotification(); const queryClient = useQueryClient(); const hasAuth = !authEnabled || username !== null; const [tabIndex, setTabIndex] = useState(0); const [refreshDb, setRefreshDb] = useState(true); const defaultInterval = autorefreshIntervals.find((i) => i.is_active)?.interval ?? 0; const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval); const repo = current as RepositoryId; const { data: packageData } = useQuery({ queryKey: packageBase && repo ? QueryKeys.package(packageBase, repo) : ["package-none"], queryFn: () => Client.fetchPackage(packageBase!, repo), enabled: !!packageBase && !!repo && open, refetchInterval: autoRefresh.refetchInterval, }); const { data: dependencies } = useQuery({ queryKey: packageBase && repo ? QueryKeys.dependencies(packageBase, repo) : ["deps-none"], queryFn: () => Client.fetchDependencies(packageBase!, repo), enabled: !!packageBase && !!repo && open, }); const { data: patches = [] } = useQuery({ queryKey: packageBase ? QueryKeys.patches(packageBase) : ["patches-none"], queryFn: () => Client.fetchPatches(packageBase!), enabled: !!packageBase && open, }); const description: PackageStatus | undefined = packageData?.[0]; const pkg = description?.package; const status = description?.status; const headerStyle = status ? StatusHeaderStyles[status.status] : {}; // Flatten depends from all sub-packages const allDepends: string[] = pkg ? Object.values(pkg.packages).flatMap((p: PackageProperties) => { const pkgNames = Object.keys(pkg.packages); const deps = (p.depends ?? []).filter((d: string) => !pkgNames.includes(d)); const makeDeps = (p.make_depends ?? []).filter((d: string) => !pkgNames.includes(d)).map((d: string) => `${d} (make)`); const optDeps = (p.opt_depends ?? []).filter((d: string) => !pkgNames.includes(d)).map((d: string) => `${d} (optional)`); return [...deps, ...makeDeps, ...optDeps]; }) : []; const implicitDepends: string[] = dependencies ? Object.values(dependencies.paths).flat() : []; const groups: string[] = pkg ? Object.values(pkg.packages).flatMap((p: PackageProperties) => p.groups ?? []) : []; const licenses: string[] = pkg ? Object.values(pkg.packages).flatMap((p: PackageProperties) => p.licenses ?? []) : []; const upstreamUrls: string[] = pkg ? [...new Set(Object.values(pkg.packages).map((p: PackageProperties) => p.url).filter((u): u is string => !!u))].sort() : []; const aurUrl = pkg?.remote.web_url; const packagesList: string[] = pkg ? Object.entries(pkg.packages).map(([name, p]) => `${name}${p.description ? ` (${p.description})` : ""}`) : []; const handleUpdate = async (): Promise => { if (!packageBase || !repo) { return; } try { await Client.addPackages(repo, { packages: [packageBase], refresh: refreshDb }); showSuccess("Success", `Run update for packages ${packageBase}`); } catch (e) { const detail = e instanceof ApiError ? e.detail : String(e); showError("Action failed", `Package update failed: ${detail}`); } }; const handleRemove = async (): Promise => { if (!packageBase || !repo) { return; } try { await Client.removePackages(repo, [packageBase]); showSuccess("Success", `Packages ${packageBase} have been removed`); onClose(); } catch (e) { const detail = e instanceof ApiError ? e.detail : String(e); showError("Action failed", `Could not remove package: ${detail}`); } }; const handleDeletePatch = async (key: string): Promise => { if (!packageBase) { return; } try { await Client.deletePatch(packageBase, key); void queryClient.invalidateQueries({ queryKey: QueryKeys.patches(packageBase) }); } catch (e) { const detail = e instanceof ApiError ? e.detail : String(e); showError("Action failed", `Could not delete variable: ${detail}`); } }; const handleReload = (): void => { if (!packageBase || !repo) { return; } void queryClient.invalidateQueries({ queryKey: QueryKeys.package(packageBase, repo) }); void queryClient.invalidateQueries({ queryKey: QueryKeys.logs(packageBase, repo) }); void queryClient.invalidateQueries({ queryKey: QueryKeys.changes(packageBase, repo) }); void queryClient.invalidateQueries({ queryKey: QueryKeys.events(repo, packageBase) }); void queryClient.invalidateQueries({ queryKey: QueryKeys.dependencies(packageBase, repo) }); void queryClient.invalidateQueries({ queryKey: QueryKeys.patches(packageBase) }); }; const handleClose = (): void => { setTabIndex(0); setRefreshDb(true); onClose(); }; return ( {pkg && status ? `${pkg.base} ${status.status} at ${formatTimestamp(status.timestamp)}` : packageBase ?? ""} {pkg && ( <> packages {listToString(packagesList)} version {pkg.version} packager {pkg.packager ?? ""} groups {listToString(groups)} licenses {listToString(licenses)} upstream {upstreamUrls.map((url) => ( {url} ))} AUR {aurUrl && ( AUR link )} depends {listToString(allDepends)} implicitly depends {listToString(implicitDepends)} {patches.length > 0 && ( Environment variables {patches.map((patch) => ( = {JSON.stringify(patch.value)} {hasAuth && ( void handleDeletePatch(patch.key)}> )} ))} )} setTabIndex(v)}> {tabIndex === 0 && packageBase && repo && ( )} {tabIndex === 1 && packageBase && repo && ( )} {tabIndex === 2 && packageBase && repo && ( )} )} {hasAuth && ( <> setRefreshDb(checked)} size="small" />} label="update pacman databases" /> )} ); }