feat: add update and remove buttons to the status table

This commit is contained in:
2026-09-23 16:04:42 +03:00
parent 23c834e186
commit 16afb50c19
3 changed files with 61 additions and 32 deletions
+35 -5
View File
@@ -17,10 +17,13 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import DeleteIcon from "@mui/icons-material/Delete";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { Box, Link } from "@mui/material"; import { Box, Link } from "@mui/material";
import { import {
DataGrid, DataGrid,
GRID_CHECKBOX_SELECTION_COL_DEF, GRID_CHECKBOX_SELECTION_COL_DEF,
GridActionsCellItem,
type GridColDef, type GridColDef,
type GridFilterModel, type GridFilterModel,
type GridRowId, type GridRowId,
@@ -56,6 +59,7 @@ function createListColumn(
export default function PackageTable(): React.JSX.Element { export default function PackageTable(): React.JSX.Element {
const table = usePackageTable(); const table = usePackageTable();
const { handleRemove, handleUpdate, isAuthorized } = table;
const apiRef = useGridApiRef(); const apiRef = useGridApiRef();
const debouncedSearch = useDebounce(table.searchText, 300); const debouncedSearch = useDebounce(table.searchText, 300);
@@ -96,8 +100,31 @@ export default function PackageTable(): React.JSX.Element {
<StatusCell isHeld={params.row.isHeld} status={params.row.status} />, <StatusCell isHeld={params.row.isHeld} status={params.row.status} />,
width: 120, width: 120,
}, },
...isAuthorized ? [{
field: "actions",
type: "actions",
headerName: "actions",
width: 100,
disableExport: true,
getActions: ({ row }) => [
<GridActionsCellItem
key="update"
icon={<PlayArrowIcon />}
label={`Update ${row.base}`}
title={`Update ${row.base}`}
onClick={() => void handleUpdate([row.base])}
/>,
<GridActionsCellItem
key="remove"
icon={<DeleteIcon />}
label={`Remove ${row.base}`}
title={`Remove ${row.base}`}
onClick={() => void handleRemove([row.base])}
/>,
],
} satisfies GridColDef<PackageRow>] : [],
], ],
[], [handleRemove, handleUpdate, isAuthorized],
); );
return <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}> return <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
@@ -110,8 +137,10 @@ export default function PackageTable(): React.JSX.Element {
onRebuildClick: () => table.setDialogOpen("rebuild"), onRebuildClick: () => table.setDialogOpen("rebuild"),
onRefreshDatabaseClick: () => void table.handleRefreshDatabase(), onRefreshDatabaseClick: () => void table.handleRefreshDatabase(),
onReloadClick: table.handleReload, onReloadClick: table.handleReload,
onRemoveClick: () => void table.handleRemove(), onRemoveClick: () => void table.handleRemove(table.selectionModel)
onUpdateClick: () => void table.handleUpdate(), .then(() => table.setSelectionModel([])),
onUpdateClick: () => void table.handleUpdate(table.selectionModel)
.then(() => table.setSelectionModel([])),
}} }}
isAuthorized={table.isAuthorized} isAuthorized={table.isAuthorized}
hasSelection={table.selectionModel.length > 0} hasSelection={table.selectionModel.length > 0}
@@ -123,6 +152,7 @@ export default function PackageTable(): React.JSX.Element {
<DataGrid <DataGrid
apiRef={apiRef} apiRef={apiRef}
checkboxSelection checkboxSelection
localeText={{ checkboxSelectionHeaderName: "selection" }}
columnVisibilityModel={table.columnVisibility} columnVisibilityModel={table.columnVisibility}
columns={columns} columns={columns}
density="compact" density="compact"
@@ -134,8 +164,8 @@ export default function PackageTable(): React.JSX.Element {
}} }}
loading={table.isLoading} loading={table.isLoading}
onCellClick={(params, event) => { onCellClick={(params, event) => {
// Don't open info dialog when clicking checkbox or link // Don't open info dialog when clicking checkbox, actions, or link
if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field) { if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field || params.field === "actions") {
return; return;
} }
if ((event.target as HTMLElement).closest("a")) { if ((event.target as HTMLElement).closest("a")) {
+23 -24
View File
@@ -24,29 +24,27 @@ import { useClient } from "hooks/useClient";
import { useNotification } from "hooks/useNotification"; import { useNotification } from "hooks/useNotification";
import { useRepository } from "hooks/useRepository"; import { useRepository } from "hooks/useRepository";
import type { RepositoryId } from "models/RepositoryId"; import type { RepositoryId } from "models/RepositoryId";
import { useCallback } from "react";
export interface UsePackageActionsResult { export interface UsePackageActionsResult {
handleRefreshDatabase: () => Promise<void>; handleRefreshDatabase: () => Promise<void>;
handleReload: () => void; handleReload: () => void;
handleRemove: () => Promise<void>; handleRemove: (packages: string[]) => Promise<void>;
handleUpdate: () => Promise<void>; handleUpdate: (packages: string[]) => Promise<void>;
} }
export function usePackageActions( export function usePackageActions(): UsePackageActionsResult {
selectionModel: string[],
setSelectionModel: (model: string[]) => void,
): UsePackageActionsResult {
const client = useClient(); const client = useClient();
const { currentRepository } = useRepository(); const { currentRepository } = useRepository();
const { showSuccess, showError } = useNotification(); const { showSuccess, showError } = useNotification();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const invalidate = (repository: RepositoryId): void => { const invalidate = useCallback((repository: RepositoryId): void => {
void queryClient.invalidateQueries({ queryKey: QueryKeys.packages(repository) }); void queryClient.invalidateQueries({ queryKey: QueryKeys.packages(repository) });
void queryClient.invalidateQueries({ queryKey: QueryKeys.status(repository) }); void queryClient.invalidateQueries({ queryKey: QueryKeys.status(repository) });
}; }, [queryClient]);
const performAction = async ( const performAction = useCallback(async (
action: (repository: RepositoryId) => Promise<string>, action: (repository: RepositoryId) => Promise<string>,
errorMessage: string, errorMessage: string,
): Promise<void> => { ): Promise<void> => {
@@ -57,11 +55,10 @@ export function usePackageActions(
const successMessage = await action(currentRepository); const successMessage = await action(currentRepository);
showSuccess("Success", successMessage); showSuccess("Success", successMessage);
invalidate(currentRepository); invalidate(currentRepository);
setSelectionModel([]);
} catch (exception) { } catch (exception) {
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`); showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
} }
}; }, [currentRepository, invalidate, showError, showSuccess]);
const handleReload = (): void => { const handleReload = (): void => {
if (currentRepository !== null) { if (currentRepository !== null) {
@@ -69,14 +66,16 @@ export function usePackageActions(
} }
}; };
const handleUpdate = (): Promise<void> => performAction(async (repository): Promise<string> => { const handleUpdate = useCallback((packages: string[]): Promise<void> => {
if (selectionModel.length === 0) { return performAction(async (repository): Promise<string> => {
await client.service.servicePackageUpdate(repository, { packages: [] }); if (packages.length === 0) {
return "Repository update has been run"; await client.service.servicePackageUpdate(repository, { packages: [] });
} return "Repository update has been run";
await client.service.servicePackageAdd(repository, { packages: selectionModel }); }
return `Run update for packages ${selectionModel.join(", ")}`; await client.service.servicePackageAdd(repository, { packages });
}, "Packages update failed"); return `Run update for packages ${packages.join(", ")}`;
}, "Packages update failed");
}, [client, performAction]);
const handleRefreshDatabase = (): Promise<void> => performAction(async (repository): Promise<string> => { const handleRefreshDatabase = (): Promise<void> => performAction(async (repository): Promise<string> => {
await client.service.servicePackageUpdate(repository, { await client.service.servicePackageUpdate(repository, {
@@ -89,15 +88,15 @@ export function usePackageActions(
return "Pacman database update has been requested"; return "Pacman database update has been requested";
}, "Could not update pacman databases"); }, "Could not update pacman databases");
const handleRemove = (): Promise<void> => { const handleRemove = useCallback((packages: string[]): Promise<void> => {
if (selectionModel.length === 0) { if (packages.length === 0) {
return Promise.resolve(); return Promise.resolve();
} }
return performAction(async (repository): Promise<string> => { return performAction(async (repository): Promise<string> => {
await client.service.servicePackageRemove(repository, selectionModel); await client.service.servicePackageRemove(repository, packages);
return `Packages ${selectionModel.join(", ")} have been removed`; return `Packages ${packages.join(", ")} have been removed`;
}, "Could not remove packages"); }, "Could not remove packages");
}; }, [client, performAction]);
return { return {
handleRefreshDatabase, handleRefreshDatabase,
+3 -3
View File
@@ -30,8 +30,8 @@ export interface UsePackageTableResult {
filterModel: GridFilterModel; filterModel: GridFilterModel;
handleRefreshDatabase: () => Promise<void>; handleRefreshDatabase: () => Promise<void>;
handleReload: () => void; handleReload: () => void;
handleRemove: () => Promise<void>; handleRemove: (packages: string[]) => Promise<void>;
handleUpdate: () => Promise<void>; handleUpdate: (packages: string[]) => Promise<void>;
isAuthorized: boolean; isAuthorized: boolean;
isLoading: boolean; isLoading: boolean;
paginationModel: { page: number; pageSize: number }; paginationModel: { page: number; pageSize: number };
@@ -52,7 +52,7 @@ export interface UsePackageTableResult {
export function usePackageTable(): UsePackageTableResult { export function usePackageTable(): UsePackageTableResult {
const { rows, isLoading, isAuthorized, status } = usePackageData(); const { rows, isLoading, isAuthorized, status } = usePackageData();
const tableState = useTableState(); const tableState = useTableState();
const actions = usePackageActions(tableState.selectionModel, tableState.setSelectionModel); const actions = usePackageActions();
return { return {
isLoading, isLoading,