From 16afb50c195d14bbb94432aca0e9bf23f6d381a7 Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Wed, 23 Sep 2026 16:04:24 +0300 Subject: [PATCH] feat: add update and remove buttons to the status table --- .../src/components/table/PackageTable.tsx | 40 ++++++++++++++-- frontend/src/hooks/usePackageActions.ts | 47 +++++++++---------- frontend/src/hooks/usePackageTable.ts | 6 +-- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/table/PackageTable.tsx b/frontend/src/components/table/PackageTable.tsx index a573bd54..9d742dfa 100644 --- a/frontend/src/components/table/PackageTable.tsx +++ b/frontend/src/components/table/PackageTable.tsx @@ -17,10 +17,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +import DeleteIcon from "@mui/icons-material/Delete"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import { Box, Link } from "@mui/material"; import { DataGrid, GRID_CHECKBOX_SELECTION_COL_DEF, + GridActionsCellItem, type GridColDef, type GridFilterModel, type GridRowId, @@ -56,6 +59,7 @@ function createListColumn( export default function PackageTable(): React.JSX.Element { const table = usePackageTable(); + const { handleRemove, handleUpdate, isAuthorized } = table; const apiRef = useGridApiRef(); const debouncedSearch = useDebounce(table.searchText, 300); @@ -96,8 +100,31 @@ export default function PackageTable(): React.JSX.Element { , width: 120, }, + ...isAuthorized ? [{ + field: "actions", + type: "actions", + headerName: "actions", + width: 100, + disableExport: true, + getActions: ({ row }) => [ + } + label={`Update ${row.base}`} + title={`Update ${row.base}`} + onClick={() => void handleUpdate([row.base])} + />, + } + label={`Remove ${row.base}`} + title={`Remove ${row.base}`} + onClick={() => void handleRemove([row.base])} + />, + ], + } satisfies GridColDef] : [], ], - [], + [handleRemove, handleUpdate, isAuthorized], ); return @@ -110,8 +137,10 @@ export default function PackageTable(): React.JSX.Element { onRebuildClick: () => table.setDialogOpen("rebuild"), onRefreshDatabaseClick: () => void table.handleRefreshDatabase(), onReloadClick: table.handleReload, - onRemoveClick: () => void table.handleRemove(), - onUpdateClick: () => void table.handleUpdate(), + onRemoveClick: () => void table.handleRemove(table.selectionModel) + .then(() => table.setSelectionModel([])), + onUpdateClick: () => void table.handleUpdate(table.selectionModel) + .then(() => table.setSelectionModel([])), }} isAuthorized={table.isAuthorized} hasSelection={table.selectionModel.length > 0} @@ -123,6 +152,7 @@ export default function PackageTable(): React.JSX.Element { { - // Don't open info dialog when clicking checkbox or link - if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field) { + // Don't open info dialog when clicking checkbox, actions, or link + if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field || params.field === "actions") { return; } if ((event.target as HTMLElement).closest("a")) { diff --git a/frontend/src/hooks/usePackageActions.ts b/frontend/src/hooks/usePackageActions.ts index 369c2db7..d36ea4b4 100644 --- a/frontend/src/hooks/usePackageActions.ts +++ b/frontend/src/hooks/usePackageActions.ts @@ -24,29 +24,27 @@ import { useClient } from "hooks/useClient"; import { useNotification } from "hooks/useNotification"; import { useRepository } from "hooks/useRepository"; import type { RepositoryId } from "models/RepositoryId"; +import { useCallback } from "react"; export interface UsePackageActionsResult { handleRefreshDatabase: () => Promise; handleReload: () => void; - handleRemove: () => Promise; - handleUpdate: () => Promise; + handleRemove: (packages: string[]) => Promise; + handleUpdate: (packages: string[]) => Promise; } -export function usePackageActions( - selectionModel: string[], - setSelectionModel: (model: string[]) => void, -): UsePackageActionsResult { +export function usePackageActions(): UsePackageActionsResult { const client = useClient(); const { currentRepository } = useRepository(); const { showSuccess, showError } = useNotification(); 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.status(repository) }); - }; + }, [queryClient]); - const performAction = async ( + const performAction = useCallback(async ( action: (repository: RepositoryId) => Promise, errorMessage: string, ): Promise => { @@ -57,11 +55,10 @@ export function usePackageActions( const successMessage = await action(currentRepository); showSuccess("Success", successMessage); invalidate(currentRepository); - setSelectionModel([]); } catch (exception) { showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`); } - }; + }, [currentRepository, invalidate, showError, showSuccess]); const handleReload = (): void => { if (currentRepository !== null) { @@ -69,14 +66,16 @@ export function usePackageActions( } }; - const handleUpdate = (): Promise => performAction(async (repository): Promise => { - if (selectionModel.length === 0) { - 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(", ")}`; - }, "Packages update failed"); + const handleUpdate = useCallback((packages: string[]): Promise => { + return performAction(async (repository): Promise => { + if (packages.length === 0) { + await client.service.servicePackageUpdate(repository, { packages: [] }); + return "Repository update has been run"; + } + await client.service.servicePackageAdd(repository, { packages }); + return `Run update for packages ${packages.join(", ")}`; + }, "Packages update failed"); + }, [client, performAction]); const handleRefreshDatabase = (): Promise => performAction(async (repository): Promise => { await client.service.servicePackageUpdate(repository, { @@ -89,15 +88,15 @@ export function usePackageActions( return "Pacman database update has been requested"; }, "Could not update pacman databases"); - const handleRemove = (): Promise => { - if (selectionModel.length === 0) { + const handleRemove = useCallback((packages: string[]): Promise => { + if (packages.length === 0) { return Promise.resolve(); } return performAction(async (repository): Promise => { - await client.service.servicePackageRemove(repository, selectionModel); - return `Packages ${selectionModel.join(", ")} have been removed`; + await client.service.servicePackageRemove(repository, packages); + return `Packages ${packages.join(", ")} have been removed`; }, "Could not remove packages"); - }; + }, [client, performAction]); return { handleRefreshDatabase, diff --git a/frontend/src/hooks/usePackageTable.ts b/frontend/src/hooks/usePackageTable.ts index 0fd94b29..f0104847 100644 --- a/frontend/src/hooks/usePackageTable.ts +++ b/frontend/src/hooks/usePackageTable.ts @@ -30,8 +30,8 @@ export interface UsePackageTableResult { filterModel: GridFilterModel; handleRefreshDatabase: () => Promise; handleReload: () => void; - handleRemove: () => Promise; - handleUpdate: () => Promise; + handleRemove: (packages: string[]) => Promise; + handleUpdate: (packages: string[]) => Promise; isAuthorized: boolean; isLoading: boolean; paginationModel: { page: number; pageSize: number }; @@ -52,7 +52,7 @@ export interface UsePackageTableResult { export function usePackageTable(): UsePackageTableResult { const { rows, isLoading, isAuthorized, status } = usePackageData(); const tableState = useTableState(); - const actions = usePackageActions(tableState.selectionModel, tableState.setSelectionModel); + const actions = usePackageActions(); return { isLoading,