/*
* 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 GridRowId,
useGridApiRef,
} from "@mui/x-data-grid";
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[];
}
function createListColumn(
field: keyof PackageRow,
headerName: string,
options: { flex?: number; minWidth?: number; width?: number },
): GridColDef {
return {
field,
headerName,
...options,
renderCell: params =>
{((params.row[field] as string[]) ?? []).join("\n")},
sortComparator: (left: string, right: string) => left.localeCompare(right),
valueGetter: (value: string[]) => (value ?? []).join(" "),
};
}
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",
flex: 1,
headerName: "package base",
minWidth: 150,
renderCell: params =>
params.row.webUrl ?
{params.value as string}
: params.value as string,
},
{ align: "right", field: "version", headerAlign: "right", headerName: "version", width: 180 },
createListColumn("packages", "packages", { flex: 1, minWidth: 120 }),
createListColumn("groups", "groups", { width: 150 }),
createListColumn("licenses", "licenses", { width: 150 }),
{ field: "packager", headerName: "packager", width: 150 },
{ align: "right", field: "timestamp", headerName: "last update", headerAlign: "right", width: 180 },
{
align: "center",
field: "status",
headerAlign: "center",
headerName: "status",
renderCell: params =>
,
width: 120,
},
],
[],
);
return
table.setDialogOpen("add"),
onDashboardClick: () => table.setDialogOpen("dashboard"),
onExportClick: () => apiRef.current?.exportDataAsCsv(),
onKeyImportClick: () => table.setDialogOpen("keyImport"),
onRebuildClick: () => table.setDialogOpen("rebuild"),
onRefreshDatabaseClick: () => void table.handleRefreshDatabase(),
onReloadClick: table.handleReload,
onRemoveClick: () => void table.handleRemove(),
onUpdateClick: () => void table.handleUpdate(),
}}
autoRefresh={{
autoRefreshIntervals,
currentInterval: table.autoRefreshInterval,
onIntervalChange: table.onAutoRefreshIntervalChange,
}}
isAuthorized={table.isAuthorized}
hasSelection={table.selectionModel.length > 0}
onSearchChange={table.setSearchText}
searchText={table.searchText}
status={table.status}
/>
"auto"}
initialState={{
sorting: { sortModel: [{ field: "base", sort: "asc" }] },
}}
loading={table.isLoading}
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));
}}
onColumnVisibilityModelChange={table.setColumnVisibility}
onFilterModelChange={table.setFilterModel}
onPaginationModelChange={table.setPaginationModel}
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}
rowSelectionModel={{ type: "include", ids: new Set(table.selectionModel) }}
rows={table.rows}
sx={{ flex: 1 }}
/>
table.setDialogOpen(null)} open={table.dialogOpen === "dashboard"} />
table.setDialogOpen(null)} open={table.dialogOpen === "add"} />
table.setDialogOpen(null)} open={table.dialogOpen === "rebuild"} />
table.setDialogOpen(null)} open={table.dialogOpen === "keyImport"} />
table.setSelectedPackage(null)}
open={table.selectedPackage !== null}
packageBase={table.selectedPackage}
/>
;
}