/* * 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 . */ import { Box, Link } from "@mui/material"; import { DataGrid, GRID_CHECKBOX_SELECTION_COL_DEF, type GridColDef, type GridFilterModel, type GridRenderCellParams, type GridRowId, useGridApiRef, } from "@mui/x-data-grid"; import StringList from "components/common/StringList"; import DashboardDialog from "components/dialogs/DashboardDialog"; import KeyImportDialog from "components/dialogs/KeyImportDialog"; import PackageAddDialog from "components/dialogs/PackageAddDialog"; import PackageInfoDialog from "components/dialogs/PackageInfoDialog"; import PackageRebuildDialog from "components/dialogs/PackageRebuildDialog"; import PackageTableToolbar from "components/table/PackageTableToolbar"; import StatusCell from "components/table/StatusCell"; import { useDebounce } from "hooks/useDebounce"; import { usePackageTable } from "hooks/usePackageTable"; import type { AutoRefreshInterval } from "models/AutoRefreshInterval"; import type { PackageRow } from "models/PackageRow"; import React, { useMemo } from "react"; interface PackageTableProps { autoRefreshIntervals: AutoRefreshInterval[]; } const PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; function createListColumn( field: keyof PackageRow, headerName: string, options: { flex?: number; minWidth?: number; width?: number }, ): GridColDef { return { field, headerName, ...options, valueGetter: (value: string[]) => (value ?? []).join(" "), renderCell: (params: GridRenderCellParams) => , sortComparator: (left: string, right: string) => left.localeCompare(right), }; } export default function PackageTable({ autoRefreshIntervals }: PackageTableProps): React.JSX.Element { const table = usePackageTable(autoRefreshIntervals); const apiRef = useGridApiRef(); const debouncedSearch = useDebounce(table.searchText, 300); const effectiveFilterModel: GridFilterModel = useMemo( () => ({ ...table.filterModel, quickFilterValues: debouncedSearch ? debouncedSearch.split(/\s+/) : undefined, }), [table.filterModel, debouncedSearch], ); const columns: GridColDef[] = useMemo( () => [ { field: "base", headerName: "package base", flex: 1, minWidth: 150, renderCell: (params: GridRenderCellParams) => params.row.webUrl ? {params.value as string} : params.value as string, }, { field: "version", headerName: "version", width: 180, align: "right", headerAlign: "right" }, createListColumn("packages", "packages", { flex: 1, minWidth: 120 }), createListColumn("groups", "groups", { width: 150 }), createListColumn("licenses", "licenses", { width: 150 }), { field: "packager", headerName: "packager", width: 150 }, { field: "timestamp", headerName: "last update", width: 180, align: "right", headerAlign: "right", }, { field: "status", headerName: "status", width: 120, align: "center", headerAlign: "center", renderCell: (params: GridRenderCellParams) => , }, ], [], ); return 0} isAuthorized={table.isAuthorized} status={table.status} searchText={table.searchText} onSearchChange={table.setSearchText} autoRefresh={{ autoRefreshIntervals, currentInterval: table.autoRefreshInterval, onIntervalChange: table.onAutoRefreshIntervalChange, }} actions={{ onDashboardClick: () => table.setDialogOpen("dashboard"), onAddClick: () => table.setDialogOpen("add"), onUpdateClick: () => void table.handleUpdate(), onRefreshDatabaseClick: () => void table.handleRefreshDatabase(), onRebuildClick: () => table.setDialogOpen("rebuild"), onRemoveClick: () => void table.handleRemove(), onKeyImportClick: () => table.setDialogOpen("keyImport"), onReloadClick: table.handleReload, onExportClick: () => apiRef.current?.exportDataAsCsv(), }} /> "auto"} checkboxSelection disableRowSelectionOnClick rowSelectionModel={{ type: "include", ids: new Set(table.selectionModel) }} onRowSelectionModelChange={model => { if (model.type === "exclude") { const excludeIds = new Set([...model.ids].map(String)); table.setSelectionModel(table.rows.map(row => row.id).filter(id => !excludeIds.has(id))); } else { table.setSelectionModel([...model.ids].map(String)); } }} paginationModel={table.paginationModel} onPaginationModelChange={table.setPaginationModel} pageSizeOptions={PAGE_SIZE_OPTIONS} columnVisibilityModel={table.columnVisibility} onColumnVisibilityModelChange={table.setColumnVisibility} filterModel={effectiveFilterModel} onFilterModelChange={table.setFilterModel} initialState={{ sorting: { sortModel: [{ field: "base", sort: "asc" }] }, }} onCellClick={(params, event) => { // Don't open info dialog when clicking checkbox or link if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field) { return; } if ((event.target as HTMLElement).closest("a")) { return; } table.setSelectedPackage(String(params.id)); }} autoHeight sx={{ "& .MuiDataGrid-row": { cursor: "pointer" }, }} density="compact" /> table.setDialogOpen(null)} /> table.setDialogOpen(null)} /> table.setDialogOpen(null)} /> table.setDialogOpen(null)} /> table.setSelectedPackage(null)} autoRefreshIntervals={autoRefreshIntervals} /> ; }