refactor: reorder arguments in web ui

This commit is contained in:
2026-03-22 03:23:09 +02:00
parent d7984c12f0
commit 5e090cebdb
61 changed files with 547 additions and 578 deletions
@@ -19,7 +19,7 @@
*/
import RestoreIcon from "@mui/icons-material/Restore";
import { Box, IconButton, Tooltip } from "@mui/material";
import { DataGrid, type GridColDef, type GridRenderCellParams } from "@mui/x-data-grid";
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { ApiError } from "api/client/ApiError";
import { QueryKeys } from "hooks/QueryKeys";
@@ -29,36 +29,37 @@ import { useNotification } from "hooks/useNotification";
import type { RepositoryId } from "models/RepositoryId";
import type React from "react";
import { useCallback, useMemo } from "react";
import { DETAIL_TABLE_PROPS } from "utils";
interface ArtifactsTabProps {
currentVersion: string;
packageBase: string;
repository: RepositoryId;
currentVersion: string;
}
interface ArtifactRow {
id: string;
version: string;
packager: string;
packages: string[];
version: string;
}
const staticColumns: GridColDef<ArtifactRow>[] = [
{ field: "version", headerName: "version", flex: 1, align: "right", headerAlign: "right" },
{ align: "right", field: "version", flex: 1, headerAlign: "right", headerName: "version" },
{
field: "packages",
headerName: "packages",
flex: 2,
renderCell: (params: GridRenderCellParams<ArtifactRow>) =>
headerName: "packages",
renderCell: params =>
<Box sx={{ whiteSpace: "pre-line" }}>{params.row.packages.join("\n")}</Box>,
},
{ field: "packager", headerName: "packager", flex: 1 },
{ field: "packager", flex: 1, headerName: "packager" },
];
export default function ArtifactsTab({
currentVersion,
packageBase,
repository,
currentVersion,
}: ArtifactsTabProps): React.JSX.Element {
const client = useClient();
const queryClient = useQueryClient();
@@ -66,17 +67,17 @@ export default function ArtifactsTab({
const { showSuccess, showError } = useNotification();
const { data: rows = [] } = useQuery<ArtifactRow[]>({
queryKey: QueryKeys.artifacts(packageBase, repository),
enabled: !!packageBase,
queryFn: async () => {
const packages = await client.fetch.fetchPackageArtifacts(packageBase, repository);
return packages.map(artifact => ({
id: artifact.version,
version: artifact.version,
packager: artifact.packager ?? "",
packages: Object.keys(artifact.packages).sort(),
version: artifact.version,
})).reverse();
},
enabled: !!packageBase,
queryKey: QueryKeys.artifacts(packageBase, repository),
});
const handleRollback = useCallback(async (version: string): Promise<void> => {
@@ -96,32 +97,23 @@ export default function ArtifactsTab({
field: "actions",
filterable: false,
headerName: "",
width: 60,
renderCell: (params: GridRenderCellParams<ArtifactRow>) =>
renderCell: params =>
<Tooltip title={params.row.version === currentVersion ? "Current version" : "Rollback to this version"}>
<span>
<IconButton
size="small"
disabled={params.row.version === currentVersion}
onClick={() => void handleRollback(params.row.version)}
size="small"
>
<RestoreIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>,
width: 60,
} satisfies GridColDef<ArtifactRow>] : [],
], [isAuthorized, currentVersion, handleRollback]);
return <Box sx={{ mt: 1 }}>
<DataGrid
rows={rows}
columns={columns}
density="compact"
disableColumnSorting
disableRowSelectionOnClick
getRowHeight={() => "auto"}
pageSizeOptions={[10, 25]}
sx={{ height: 400, mt: 1 }}
/>
<DataGrid columns={columns} getRowHeight={() => "auto"} rows={rows} {...DETAIL_TABLE_PROPS} />
</Box>;
}
@@ -29,16 +29,16 @@ import type { RepositoryId } from "models/RepositoryId";
import React, { useEffect, useMemo, useState } from "react";
interface Logs {
version: string;
processId: string;
created: number;
logs: string;
processId: string;
version: string;
}
interface BuildLogsTabProps {
packageBase: string;
repository: RepositoryId;
refreshInterval: number;
repository: RepositoryId;
}
function convertLogs(records: LogRecord[], filter?: (record: LogRecord) => boolean): string {
@@ -50,17 +50,17 @@ function convertLogs(records: LogRecord[], filter?: (record: LogRecord) => boole
export default function BuildLogsTab({
packageBase,
repository,
refreshInterval,
repository,
}: BuildLogsTabProps): React.JSX.Element {
const client = useClient();
const [selectedVersionKey, setSelectedVersionKey] = useState<string | null>(null);
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const { data: allLogs } = useQuery<LogRecord[]>({
queryKey: QueryKeys.logs(packageBase, repository),
queryFn: () => client.fetch.fetchPackageLogs(packageBase, repository),
enabled: !!packageBase,
queryFn: () => client.fetch.fetchPackageLogs(packageBase, repository),
queryKey: QueryKeys.logs(packageBase, repository),
refetchInterval: refreshInterval > 0 ? refreshInterval : false,
});
@@ -84,13 +84,13 @@ export default function BuildLogsTab({
return Object.values(grouped)
.sort((left, right) => right.minCreated - left.minCreated)
.map(record => ({
version: record.version,
processId: record.process_id,
created: record.minCreated,
logs: convertLogs(
allLogs,
right => record.version === right.version && record.process_id === right.process_id,
),
processId: record.process_id,
version: record.version,
}));
}, [allLogs]);
@@ -110,13 +110,13 @@ export default function BuildLogsTab({
// Refresh active version logs
const { data: versionLogs } = useQuery<LogRecord[]>({
queryKey: QueryKeys.logsVersion(packageBase, repository, activeVersion?.version ?? "", activeVersion?.processId ?? ""),
placeholderData: keepPreviousData,
queryFn: activeVersion
? () => client.fetch.fetchPackageLogs(
packageBase, repository, activeVersion.version, activeVersion.processId,
)
: skipToken,
placeholderData: keepPreviousData,
queryKey: QueryKeys.logsVersion(packageBase, repository, activeVersion?.version ?? "", activeVersion?.processId ?? ""),
refetchInterval: refreshInterval > 0 ? refreshInterval : false,
});
@@ -143,25 +143,25 @@ export default function BuildLogsTab({
return <Box sx={{ display: "flex", gap: 1, mt: 1 }}>
<Box>
<Button
size="small"
aria-label="Select version"
startIcon={<ListIcon />}
onClick={event => setAnchorEl(event.currentTarget)}
size="small"
startIcon={<ListIcon />}
/>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
open={Boolean(anchorEl)}
>
{versions.map((logs, index) =>
<MenuItem
key={`${logs.version}-${logs.processId}`}
selected={index === activeIndex}
onClick={() => {
setSelectedVersionKey(`${logs.version}-${logs.processId}`);
setAnchorEl(null);
resetScroll();
}}
selected={index === activeIndex}
>
<Typography variant="body2">{new Date(logs.created * 1000).toISOStringShort()}</Typography>
</MenuItem>,
@@ -174,10 +174,10 @@ export default function BuildLogsTab({
<Box sx={{ flex: 1 }}>
<CodeBlock
preRef={preRef}
content={displayedLogs}
height={400}
onScroll={handleScroll}
preRef={preRef}
/>
</Box>
</Box>;
@@ -30,5 +30,5 @@ interface ChangesTabProps {
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
const data = usePackageChanges(packageBase, repository);
return <CodeBlock language="diff" content={data?.changes ?? ""} height={400} />;
return <CodeBlock content={data?.changes ?? ""} height={400} language="diff" />;
}
+11 -18
View File
@@ -27,6 +27,7 @@ import type { Event } from "models/Event";
import type { RepositoryId } from "models/RepositoryId";
import type React from "react";
import { useMemo } from "react";
import { DETAIL_TABLE_PROPS } from "utils";
interface EventsTabProps {
packageBase: string;
@@ -34,44 +35,36 @@ interface EventsTabProps {
}
interface EventRow {
id: number;
timestamp: string;
event: string;
id: number;
message: string;
timestamp: string;
}
const columns: GridColDef<EventRow>[] = [
{ field: "timestamp", headerName: "date", width: 180, align: "right", headerAlign: "right" },
{ field: "event", headerName: "event", flex: 1 },
{ field: "message", headerName: "description", flex: 2 },
{ align: "right", field: "timestamp", headerAlign: "right", headerName: "date", width: 180 },
{ field: "event", flex: 1, headerName: "event" },
{ field: "message", flex: 2, headerName: "description" },
];
export default function EventsTab({ packageBase, repository }: EventsTabProps): React.JSX.Element {
const client = useClient();
const { data: events = [] } = useQuery<Event[]>({
queryKey: QueryKeys.events(repository, packageBase),
queryFn: () => client.fetch.fetchPackageEvents(repository, packageBase, 30),
enabled: !!packageBase,
queryFn: () => client.fetch.fetchPackageEvents(repository, packageBase, 30),
queryKey: QueryKeys.events(repository, packageBase),
});
const rows = useMemo<EventRow[]>(() => events.map((event, index) => ({
id: index,
timestamp: new Date(event.created * 1000).toISOStringShort(),
event: event.event,
id: index,
message: event.message ?? "",
timestamp: new Date(event.created * 1000).toISOStringShort(),
})), [events]);
return <Box sx={{ mt: 1 }}>
<EventDurationLineChart events={events} />
<DataGrid
rows={rows}
columns={columns}
density="compact"
disableColumnSorting
disableRowSelectionOnClick
pageSizeOptions={[10, 25]}
sx={{ height: 400, mt: 1 }}
/>
<DataGrid columns={columns} rows={rows} {...DETAIL_TABLE_PROPS} />
</Box>;
}
@@ -23,11 +23,11 @@ import type { Package } from "models/Package";
import React from "react";
interface PackageDetailsGridProps {
pkg: Package;
dependencies?: Dependencies;
pkg: Package;
}
export default function PackageDetailsGrid({ pkg, dependencies }: PackageDetailsGridProps): React.JSX.Element {
export default function PackageDetailsGrid({ dependencies, pkg }: PackageDetailsGridProps): React.JSX.Element {
const packagesList = Object.entries(pkg.packages)
.map(([name, properties]) => `${name}${properties.description ? ` (${properties.description})` : ""}`);
@@ -65,50 +65,50 @@ export default function PackageDetailsGrid({ pkg, dependencies }: PackageDetails
return <>
<Grid container spacing={1} sx={{ mt: 1 }}>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">packages</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{packagesList.unique().join("\n")}</Typography></Grid>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">version</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{pkg.version}</Typography></Grid>
<Grid size={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">packages</Typography></Grid>
<Grid size={{ md: 5, xs: 8 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{packagesList.unique().join("\n")}</Typography></Grid>
<Grid size={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">version</Typography></Grid>
<Grid size={{ md: 5, xs: 8 }}><Typography variant="body2">{pkg.version}</Typography></Grid>
</Grid>
<Grid container spacing={1} sx={{ mt: 0.5 }}>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">packager</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{pkg.packager ?? ""}</Typography></Grid>
<Grid size={{ xs: 4, md: 1 }} />
<Grid size={{ xs: 8, md: 5 }} />
<Grid size={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">packager</Typography></Grid>
<Grid size={{ md: 5, xs: 8 }}><Typography variant="body2">{pkg.packager ?? ""}</Typography></Grid>
<Grid size={{ md: 1, xs: 4 }} />
<Grid size={{ md: 5, xs: 8 }} />
</Grid>
<Grid container spacing={1} sx={{ mt: 0.5 }}>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">groups</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{groups.unique().join("\n")}</Typography></Grid>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">licenses</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{licenses.unique().join("\n")}</Typography></Grid>
<Grid size={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">groups</Typography></Grid>
<Grid size={{ md: 5, xs: 8 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{groups.unique().join("\n")}</Typography></Grid>
<Grid size={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">licenses</Typography></Grid>
<Grid size={{ md: 5, xs: 8 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{licenses.unique().join("\n")}</Typography></Grid>
</Grid>
<Grid container spacing={1} sx={{ mt: 0.5 }}>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">upstream</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}>
<Grid size={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">upstream</Typography></Grid>
<Grid size={{ md: 5, xs: 8 }}>
{upstreamUrls.map(url =>
<Link key={url} href={url} target="_blank" rel="noopener noreferrer" underline="hover" display="block" variant="body2">
<Link display="block" href={url} key={url} rel="noopener noreferrer" target="_blank" underline="hover" variant="body2">
{url}
</Link>,
)}
</Grid>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">AUR</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}>
<Grid size={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">AUR</Typography></Grid>
<Grid size={{ md: 5, xs: 8 }}>
<Typography variant="body2">
{aurUrl &&
<Link href={aurUrl} target="_blank" rel="noopener noreferrer" underline="hover">AUR link</Link>
<Link href={aurUrl} rel="noopener noreferrer" target="_blank" underline="hover">AUR link</Link>
}
</Typography>
</Grid>
</Grid>
<Grid container spacing={1} sx={{ mt: 0.5 }}>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">depends</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{allDepends.join("\n")}</Typography></Grid>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">implicitly depends</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{implicitDepends.unique().join("\n")}</Typography></Grid>
<Grid size={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">depends</Typography></Grid>
<Grid size={{ md: 5, xs: 8 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{allDepends.join("\n")}</Typography></Grid>
<Grid size={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">implicitly depends</Typography></Grid>
<Grid size={{ md: 5, xs: 8 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{implicitDepends.unique().join("\n")}</Typography></Grid>
</Grid>
</>;
}
@@ -27,29 +27,29 @@ import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
import type React from "react";
interface PackageInfoActionsProps {
autoRefreshInterval: number;
autoRefreshIntervals: AutoRefreshInterval[];
isAuthorized: boolean;
isHeld: boolean;
onHoldToggle: () => void;
refreshDatabase: boolean;
onRefreshDatabaseChange: (checked: boolean) => void;
onUpdate: () => void;
onRemove: () => void;
autoRefreshIntervals: AutoRefreshInterval[];
autoRefreshInterval: number;
onAutoRefreshIntervalChange: (interval: number) => void;
onHoldToggle: () => void;
onRefreshDatabaseChange: (checked: boolean) => void;
onRemove: () => void;
onUpdate: () => void;
refreshDatabase: boolean;
}
export default function PackageInfoActions({
isAuthorized,
refreshDatabase,
onRefreshDatabaseChange,
isHeld,
onHoldToggle,
onUpdate,
onRemove,
autoRefreshIntervals,
autoRefreshInterval,
autoRefreshIntervals,
isAuthorized,
isHeld,
onAutoRefreshIntervalChange,
onHoldToggle,
onRefreshDatabaseChange,
onRemove,
onUpdate,
refreshDatabase,
}: PackageInfoActionsProps): React.JSX.Element {
return <DialogActions sx={{ flexWrap: "wrap", gap: 1 }}>
{isAuthorized &&
@@ -58,20 +58,20 @@ export default function PackageInfoActions({
control={<Checkbox checked={refreshDatabase} onChange={(_, checked) => onRefreshDatabaseChange(checked)} size="small" />}
label="update pacman databases"
/>
<Button onClick={onHoldToggle} variant="outlined" color="warning" startIcon={isHeld ? <PlayCircleIcon /> : <PauseCircleIcon />} size="small">
<Button color="warning" onClick={onHoldToggle} size="small" startIcon={isHeld ? <PlayCircleIcon /> : <PauseCircleIcon />} variant="outlined">
{isHeld ? "unhold" : "hold"}
</Button>
<Button onClick={onUpdate} variant="contained" color="success" startIcon={<PlayArrowIcon />} size="small">
<Button color="success" onClick={onUpdate} size="small" startIcon={<PlayArrowIcon />} variant="contained">
update
</Button>
<Button onClick={onRemove} variant="contained" color="error" startIcon={<DeleteIcon />} size="small">
<Button color="error" onClick={onRemove} size="small" startIcon={<DeleteIcon />} variant="contained">
remove
</Button>
</>
}
<AutoRefreshControl
intervals={autoRefreshIntervals}
currentInterval={autoRefreshInterval}
intervals={autoRefreshIntervals}
onIntervalChange={onAutoRefreshIntervalChange}
/>
</DialogActions>;
@@ -23,39 +23,39 @@ import type { Patch } from "models/Patch";
import type React from "react";
interface PackagePatchesListProps {
patches: Patch[];
editable: boolean;
onDelete: (key: string) => void;
patches: Patch[];
}
export default function PackagePatchesList({
patches,
editable,
onDelete,
patches,
}: PackagePatchesListProps): React.JSX.Element | null {
if (patches.length === 0) {
return null;
}
return <Box sx={{ mt: 2 }}>
<Typography variant="h6" gutterBottom>Environment variables</Typography>
<Typography gutterBottom variant="h6">Environment variables</Typography>
{patches.map(patch =>
<Box key={patch.key} sx={{ display: "flex", alignItems: "center", gap: 1, mb: 0.5 }}>
<Box key={patch.key} sx={{ alignItems: "center", display: "flex", gap: 1, mb: 0.5 }}>
<TextField
size="small"
value={patch.key}
disabled
size="small"
sx={{ flex: 1 }}
value={patch.key}
/>
<Box>=</Box>
<TextField
size="small"
value={JSON.stringify(patch.value)}
disabled
value={JSON.stringify(patch.value)}
size="small"
sx={{ flex: 1 }}
/>
{editable &&
<IconButton size="small" color="error" aria-label="Remove patch" onClick={() => onDelete(patch.key)}>
<IconButton aria-label="Remove patch" color="error" onClick={() => onDelete(patch.key)} size="small">
<DeleteIcon fontSize="small" />
</IconButton>
}
@@ -30,5 +30,5 @@ interface PkgbuildTabProps {
export default function PkgbuildTab({ packageBase, repository }: PkgbuildTabProps): React.JSX.Element {
const data = usePackageChanges(packageBase, repository);
return <CodeBlock language="bash" content={data?.pkgbuild ?? ""} height={400} />;
return <CodeBlock content={data?.pkgbuild ?? ""} height={400} language="bash" />;
}