feat: dynamic package hold (#160)

* add dynamic hold implementation to backend

* update frontend to support new status

* force reporter loader

* handle missing packages explicitly

* handle missing packages explicitly
This commit is contained in:
2026-03-15 18:47:02 +02:00
committed by GitHub
parent 058f784b05
commit dc394f7df9
36 changed files with 636 additions and 15 deletions

View File

@@ -78,6 +78,14 @@ export class ServiceClient {
return this.client.request("/api/v1/service/pgp", { method: "POST", json: data });
}
async servicePackageHoldUpdate(packageBase: string, repository: RepositoryId, isHeld: boolean): Promise<void> {
return this.client.request(`/api/v1/packages/${encodeURIComponent(packageBase)}/hold`, {
method: "POST",
query: repository.toQuery(),
json: { is_held: isHeld },
});
}
async serviceRebuild(repository: RepositoryId, packages: string[]): Promise<void> {
return this.client.request("/api/v1/service/rebuild", {
method: "POST",

View File

@@ -130,6 +130,19 @@ export default function PackageInfoDialog({
}
};
const handleHoldToggle: () => Promise<void> = async () => {
if (!localPackageBase || !currentRepository) {
return;
}
try {
const newHeldStatus = !(status?.is_held ?? false);
await client.service.servicePackageHoldUpdate(localPackageBase, currentRepository, newHeldStatus);
void queryClient.invalidateQueries({ queryKey: QueryKeys.package(localPackageBase, currentRepository) });
} catch (exception) {
showError("Action failed", `Could not update hold status: ${ApiError.errorDetail(exception)}`);
}
};
const handleDeletePatch: (key: string) => Promise<void> = async key => {
if (!localPackageBase) {
return;
@@ -189,6 +202,8 @@ export default function PackageInfoDialog({
isAuthorized={isAuthorized}
refreshDatabase={refreshDatabase}
onRefreshDatabaseChange={setRefreshDatabase}
isHeld={status?.is_held ?? false}
onHoldToggle={() => void handleHoldToggle()}
onUpdate={() => void handleUpdate()}
onRemove={() => void handleRemove()}
autoRefreshIntervals={autoRefreshIntervals}

View File

@@ -18,7 +18,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import DeleteIcon from "@mui/icons-material/Delete";
import PauseCircleIcon from "@mui/icons-material/PauseCircle";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PlayCircleIcon from "@mui/icons-material/PlayCircle";
import { Button, Checkbox, DialogActions, FormControlLabel } from "@mui/material";
import AutoRefreshControl from "components/common/AutoRefreshControl";
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
@@ -26,6 +28,8 @@ import type React from "react";
interface PackageInfoActionsProps {
isAuthorized: boolean;
isHeld: boolean;
onHoldToggle: () => void;
refreshDatabase: boolean;
onRefreshDatabaseChange: (checked: boolean) => void;
onUpdate: () => void;
@@ -39,6 +43,8 @@ export default function PackageInfoActions({
isAuthorized,
refreshDatabase,
onRefreshDatabaseChange,
isHeld,
onHoldToggle,
onUpdate,
onRemove,
autoRefreshIntervals,
@@ -52,6 +58,9 @@ 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">
{isHeld ? "unhold" : "hold"}
</Button>
<Button onClick={onUpdate} variant="contained" color="success" startIcon={<PlayArrowIcon />} size="small">
update
</Button>

View File

@@ -107,7 +107,8 @@ export default function PackageTable({ autoRefreshIntervals }: PackageTableProps
width: 120,
align: "center",
headerAlign: "center",
renderCell: (params: GridRenderCellParams<PackageRow>) => <StatusCell status={params.row.status} />,
renderCell: (params: GridRenderCellParams<PackageRow>) =>
<StatusCell status={params.row.status} isHeld={params.row.isHeld} />,
},
],
[],

View File

@@ -17,6 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import PauseCircleIcon from "@mui/icons-material/PauseCircle";
import { Chip } from "@mui/material";
import type { BuildStatus } from "models/BuildStatus";
import type React from "react";
@@ -24,10 +25,12 @@ import { StatusColors } from "theme/StatusColors";
interface StatusCellProps {
status: BuildStatus;
isHeld?: boolean;
}
export default function StatusCell({ status }: StatusCellProps): React.JSX.Element {
export default function StatusCell({ status, isHeld }: StatusCellProps): React.JSX.Element {
return <Chip
icon={isHeld ? <PauseCircleIcon /> : undefined}
label={status}
size="small"
sx={{

View File

@@ -32,6 +32,7 @@ export class PackageRow {
timestamp: string;
timestampValue: number;
status: BuildStatus;
isHeld: boolean;
constructor(descriptor: PackageStatus) {
this.id = descriptor.package.base;
@@ -45,6 +46,7 @@ export class PackageRow {
this.timestamp = new Date(descriptor.status.timestamp * 1000).toISOStringShort();
this.timestampValue = descriptor.status.timestamp;
this.status = descriptor.status.status;
this.isHeld = descriptor.status.is_held ?? false;
}
private static extractListProperties(pkg: PackageStatus["package"], property: "groups" | "licenses"): string[] {

View File

@@ -22,4 +22,5 @@ import type { BuildStatus } from "models/BuildStatus";
export interface Status {
status: BuildStatus;
timestamp: number;
is_held?: boolean;
}