mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-03-22 01:23:38 +00:00
feat: package rollback support (#161)
* implement support of rollback handler * react interface for the rollback
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
"@mui/icons-material": "^7.3.9",
|
||||
"@mui/material": "^7.3.9",
|
||||
"@mui/x-data-grid": "^8.27.4",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-query": "^5.91.3",
|
||||
"chart.js": "^4.5.1",
|
||||
"react": "^19.2.4",
|
||||
"react-chartjs-2": "^5.3.1",
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { Event } from "models/Event";
|
||||
import type { InfoResponse } from "models/InfoResponse";
|
||||
import type { InternalStatus } from "models/InternalStatus";
|
||||
import type { LogRecord } from "models/LogRecord";
|
||||
import type { Package } from "models/Package";
|
||||
import type { PackageStatus } from "models/PackageStatus";
|
||||
import type { Patch } from "models/Patch";
|
||||
import { RepositoryId } from "models/RepositoryId";
|
||||
@@ -36,6 +37,12 @@ export class FetchClient {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async fetchPackageArtifacts(packageBase: string, repository: RepositoryId): Promise<Package[]> {
|
||||
return this.client.request<Package[]>(`/api/v1/packages/${encodeURIComponent(packageBase)}/archives`, {
|
||||
query: repository.toQuery(),
|
||||
});
|
||||
}
|
||||
|
||||
async fetchPackage(packageBase: string, repository: RepositoryId): Promise<PackageStatus[]> {
|
||||
return this.client.request<PackageStatus[]>(`/api/v1/packages/${encodeURIComponent(packageBase)}`, {
|
||||
query: repository.toQuery(),
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { PackageActionRequest } from "models/PackageActionRequest";
|
||||
import type { PGPKey } from "models/PGPKey";
|
||||
import type { PGPKeyRequest } from "models/PGPKeyRequest";
|
||||
import type { RepositoryId } from "models/RepositoryId";
|
||||
import type { RollbackRequest } from "models/RollbackRequest";
|
||||
|
||||
export class ServiceClient {
|
||||
|
||||
@@ -42,6 +43,14 @@ export class ServiceClient {
|
||||
});
|
||||
}
|
||||
|
||||
async servicePackageRollback(repository: RepositoryId, data: RollbackRequest): Promise<void> {
|
||||
return this.client.request("/api/v1/service/rollback", {
|
||||
method: "POST",
|
||||
query: repository.toQuery(),
|
||||
json: data,
|
||||
});
|
||||
}
|
||||
|
||||
async servicePackageRemove(repository: RepositoryId, packages: string[]): Promise<void> {
|
||||
return this.client.request("/api/v1/service/remove", {
|
||||
method: "POST",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Box, Dialog, DialogContent, Tab, Tabs } from "@mui/material";
|
||||
import { skipToken, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ApiError } from "api/client/ApiError";
|
||||
import DialogHeader from "components/common/DialogHeader";
|
||||
import ArtifactsTab from "components/package/ArtifactsTab";
|
||||
import BuildLogsTab from "components/package/BuildLogsTab";
|
||||
import ChangesTab from "components/package/ChangesTab";
|
||||
import EventsTab from "components/package/EventsTab";
|
||||
@@ -194,6 +195,12 @@ export default function PackageInfoDialog({
|
||||
{activeTab === "events" && localPackageBase && currentRepository &&
|
||||
<EventsTab packageBase={localPackageBase} repository={currentRepository} />
|
||||
}
|
||||
{activeTab === "artifacts" && localPackageBase && currentRepository &&
|
||||
<ArtifactsTab
|
||||
packageBase={localPackageBase}
|
||||
repository={currentRepository}
|
||||
currentVersion={pkg.version} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
</DialogContent>
|
||||
|
||||
127
frontend/src/components/package/ArtifactsTab.tsx
Normal file
127
frontend/src/components/package/ArtifactsTab.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2026 ahriman team.
|
||||
*
|
||||
* This file is part of ahriman
|
||||
* (see https://github.com/arcan1s/ahriman).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* 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 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 { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ApiError } from "api/client/ApiError";
|
||||
import { QueryKeys } from "hooks/QueryKeys";
|
||||
import { useAuth } from "hooks/useAuth";
|
||||
import { useClient } from "hooks/useClient";
|
||||
import { useNotification } from "hooks/useNotification";
|
||||
import type { RepositoryId } from "models/RepositoryId";
|
||||
import type React from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
interface ArtifactsTabProps {
|
||||
packageBase: string;
|
||||
repository: RepositoryId;
|
||||
currentVersion: string;
|
||||
}
|
||||
|
||||
interface ArtifactRow {
|
||||
id: string;
|
||||
version: string;
|
||||
packager: string;
|
||||
packages: string[];
|
||||
}
|
||||
|
||||
const staticColumns: GridColDef<ArtifactRow>[] = [
|
||||
{ field: "version", headerName: "version", flex: 1, align: "right", headerAlign: "right" },
|
||||
{
|
||||
field: "packages",
|
||||
headerName: "packages",
|
||||
flex: 2,
|
||||
renderCell: (params: GridRenderCellParams<ArtifactRow>) =>
|
||||
<Box sx={{ whiteSpace: "pre-line" }}>{params.row.packages.join("\n")}</Box>,
|
||||
},
|
||||
{ field: "packager", headerName: "packager", flex: 1 },
|
||||
];
|
||||
|
||||
export default function ArtifactsTab({
|
||||
packageBase,
|
||||
repository,
|
||||
currentVersion,
|
||||
}: ArtifactsTabProps): React.JSX.Element {
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
const { isAuthorized } = useAuth();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
|
||||
const { data: rows = [] } = useQuery<ArtifactRow[]>({
|
||||
queryKey: QueryKeys.artifacts(packageBase, repository),
|
||||
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(),
|
||||
})).reverse();
|
||||
},
|
||||
enabled: !!packageBase,
|
||||
});
|
||||
|
||||
const handleRollback = useCallback(async (version: string): Promise<void> => {
|
||||
try {
|
||||
await client.service.servicePackageRollback(repository, { package: packageBase, version });
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.artifacts(packageBase, repository) });
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.package(packageBase, repository) });
|
||||
showSuccess("Success", `Rollback ${packageBase} to ${version} has been started`);
|
||||
} catch (exception) {
|
||||
showError("Action failed", `Rollback failed: ${ApiError.errorDetail(exception)}`);
|
||||
}
|
||||
}, [client, repository, packageBase, queryClient, showSuccess, showError]);
|
||||
|
||||
const columns = useMemo<GridColDef<ArtifactRow>[]>(() => [
|
||||
...staticColumns,
|
||||
...isAuthorized ? [{
|
||||
field: "actions",
|
||||
filterable: false,
|
||||
headerName: "",
|
||||
width: 60,
|
||||
renderCell: (params: GridRenderCellParams<ArtifactRow>) =>
|
||||
<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)}
|
||||
>
|
||||
<RestoreIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>,
|
||||
} 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 }}
|
||||
/>
|
||||
</Box>;
|
||||
}
|
||||
@@ -68,12 +68,10 @@ export default function EventsTab({ packageBase, repository }: EventsTabProps):
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
density="compact"
|
||||
initialState={{
|
||||
sorting: { sortModel: [{ field: "timestamp", sort: "desc" }] },
|
||||
}}
|
||||
disableColumnSorting
|
||||
disableRowSelectionOnClick
|
||||
pageSizeOptions={[10, 25]}
|
||||
sx={{ height: 400, mt: 1 }}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export type TabKey = "logs" | "changes" | "pkgbuild" | "events";
|
||||
export type TabKey = "logs" | "changes" | "pkgbuild" | "events" | "artifacts";
|
||||
|
||||
export const tabs: { key: TabKey; label: string }[] = [
|
||||
{ key: "logs", label: "Build logs" },
|
||||
{ key: "changes", label: "Changes" },
|
||||
{ key: "pkgbuild", label: "PKGBUILD" },
|
||||
{ key: "events", label: "Events" },
|
||||
{ key: "artifacts", label: "Artifacts" },
|
||||
];
|
||||
|
||||
@@ -21,6 +21,8 @@ import type { RepositoryId } from "models/RepositoryId";
|
||||
|
||||
export const QueryKeys = {
|
||||
|
||||
artifacts: (packageBase: string, repository: RepositoryId) => ["artifacts", repository.key, packageBase] as const,
|
||||
|
||||
changes: (packageBase: string, repository: RepositoryId) => ["changes", repository.key, packageBase] as const,
|
||||
|
||||
dependencies: (packageBase: string, repository: RepositoryId) => ["dependencies", repository.key, packageBase] as const,
|
||||
|
||||
23
frontend/src/models/RollbackRequest.ts
Normal file
23
frontend/src/models/RollbackRequest.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2026 ahriman team.
|
||||
*
|
||||
* This file is part of ahriman
|
||||
* (see https://github.com/arcan1s/ahriman).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export interface RollbackRequest {
|
||||
package: string;
|
||||
version: string;
|
||||
}
|
||||
Reference in New Issue
Block a user