/*
* 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}
/>
;
}