Skip to content

Commit

Permalink
feat: replace mui-x-data-grid with mui-core table
Browse files Browse the repository at this point in the history
  • Loading branch information
aldbr committed Mar 19, 2024
1 parent e640b9d commit 20bfa45
Show file tree
Hide file tree
Showing 6 changed files with 496 additions and 79 deletions.
4 changes: 2 additions & 2 deletions src/components/applications/JobMonitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CssBaseline from "@mui/material/CssBaseline";
import { Box } from "@mui/material";
import { useMUITheme } from "@/hooks/theme";
import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles";
import { JobDataGrid } from "../ui/JobDataGrid";
import { JobDataTable } from "../ui/JobDataTable";

/**
* Build the Job Monitor application
Expand All @@ -24,7 +24,7 @@ export default function JobMonitor() {
}}
>
<h2>Job Monitor</h2>
<JobDataGrid />
<JobDataTable />
</Box>
</MUIThemeProvider>
</React.Fragment>
Expand Down
328 changes: 328 additions & 0 deletions src/components/ui/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
import * as React from "react";
import Box from "@mui/material/Box";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TablePagination from "@mui/material/TablePagination";
import TableRow from "@mui/material/TableRow";
import TableSortLabel from "@mui/material/TableSortLabel";
import Paper from "@mui/material/Paper";
import Checkbox from "@mui/material/Checkbox";
import { visuallyHidden } from "@mui/utils";

/**
* Descending comparator function
* @param a - the first value to compare
* @param b - the second value to compare
* @param orderBy - the key to compare
* @returns -1 if b is less than a, 1 if b is greater than a, 0 if they are equal
* @template T - the type of the values to compare
*/
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
}

type Order = "asc" | "desc";

/**
* Get the comparator function for a given key and order
* @param order - the order to sort by
* @param orderBy - the key to sort by
* @returns a comparator function
* @template Key - the type of the key to sort by
*/
function getComparator<Key extends keyof any>(
order: Order,
orderBy: Key,
): (
a: { [key in Key]: number | string },
b: { [key in Key]: number | string },
) => number {
return order === "desc"
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
}

/**
* Stable sort function
* @param array - the array to sort
* @param comparator - the comparator function
* @returns the sorted array
* @template T - the type of the array to sort
*/
function stableSort<T>(
array: readonly T[],
comparator: (a: T, b: T) => number,
) {
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) return order;
return a[1] - b[1];
});
return stabilizedThis.map((el) => el[0]);
}

/**
* The head cells for the table.
* Components using this table should provide a list of head cells.
* @property {number | string} id - the id of the cell
* @property {string} label - the label of the cell
*/
export interface HeadCell {
id: number | string;
label: string;
render?: ((value: any) => JSX.Element) | null;
}

/**
* Enhanced table props
* @property {HeadCell[]} headCells - the head cells for the table
* @property {number} numSelected - the number of selected rows
* @property {function} onRequestSort - the function to call when sorting is requested
* @property {function} onSelectAllClick - the function to call when all rows are selected
* @property {Order} order - the order to sort by
* @property {string} orderBy - the key to sort by
* @property {number} rowCount - the number of rows
*/
interface EnhancedTableProps {
headCells: HeadCell[];
numSelected: number;
onRequestSort: (
event: React.MouseEvent<unknown>,
property: string | number,
) => void;
onSelectAllClick: (
event: React.ChangeEvent<HTMLInputElement>,
checked: boolean,
) => void;
order: Order;
orderBy: string;
rowCount: number;
}

/**
* Data table head component
* @param {EnhancedTableProps} props - the props for the component
*/
function DataTableHead(props: EnhancedTableProps) {
const {
headCells,
onSelectAllClick,
order,
orderBy,
numSelected,
rowCount,
onRequestSort,
} = props;
const createSortHandler =
(property: string | number) => (event: React.MouseEvent<unknown>) => {
onRequestSort(event, property);
};

return (
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
color="primary"
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={onSelectAllClick}
inputProps={{ "aria-label": "select all items" }}
/>
</TableCell>
{headCells.map((headCell) => (
<TableCell
key={headCell.id}
sortDirection={orderBy === headCell.id ? order : false}
>
<TableSortLabel
active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : "asc"}
onClick={createSortHandler(headCell.id)}
>
{headCell.label}
{orderBy === headCell.id ? (
<Box component="span" sx={visuallyHidden}>
{order === "desc" ? "sorted descending" : "sorted ascending"}
</Box>
) : null}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
);
}

/**
* Data table props
* @property {HeadCell[]} columns - the columns for the table
* @property {any[]} rows - the rows for the table
*/
interface DataTableProps {
columns: HeadCell[];
rows: any[];
rowIdentifier: string;
isMobile: boolean;
}

/**
* Data table component
* @param {DataTableProps} props - the props for the component
* @returns a DataTable component
*/
export function DataTable(props: DataTableProps) {
const { columns, rows, rowIdentifier, isMobile } = props;
const [order, setOrder] = React.useState<Order>("asc");
const [orderBy, setOrderBy] = React.useState<string | number>(rowIdentifier);
const [selected, setSelected] = React.useState<readonly number[]>([]);
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(25);

const handleRequestSort = (
event: React.MouseEvent<unknown>,
property: string | number,
) => {
const isAsc = orderBy === property && order === "asc";
setOrder(isAsc ? "desc" : "asc");
setOrderBy(property);
};

const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
const newSelected = rows.map((n: any) => n[rowIdentifier]);
setSelected(newSelected);
return;
}
setSelected([]);
};

const handleClick = (event: React.MouseEvent<unknown>, id: number) => {
const selectedIndex = selected.indexOf(id);
let newSelected: readonly number[] = [];

if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, id);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1),
);
}

setSelected(newSelected);
};

const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};

const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};

const isSelected = (name: number) => selected.indexOf(name) !== -1;

// Calculate the number of empty rows needed to fill the space
const emptyRows =
rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage);

return (
<Box sx={{ width: "100%" }}>
<Paper sx={{ width: "100%", mb: 2 }}>
<TableContainer sx={{ maxHeight: "75vh" }}>
<Table
stickyHeader
sx={{ minWidth: isMobile ? "undefined" : "90vw" }}
aria-labelledby="tableTitle"
size={"medium"}
>
<DataTableHead
headCells={columns}
numSelected={selected.length}
order={order}
orderBy={orderBy.toString()}
onSelectAllClick={handleSelectAllClick}
onRequestSort={handleRequestSort}
rowCount={rows.length}
/>
<TableBody>
{stableSort(rows, getComparator(order, orderBy))
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((row, index) => {
const isItemSelected = isSelected(
row[rowIdentifier] as number,
);
const labelId = `enhanced-table-checkbox-${index}`;

return (
<TableRow
hover
onClick={(event) =>
handleClick(event, row[rowIdentifier] as number)
}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
key={row[rowIdentifier]}
selected={isItemSelected}
>
<TableCell padding="checkbox">
<Checkbox
color="primary"
checked={isItemSelected}
inputProps={{ "aria-labelledby": labelId }}
/>
</TableCell>
{columns.map((column) => {
const cellValue = row[column.id];
return (
<TableCell key={column.id}>
{column.render
? column.render(cellValue)
: cellValue}
</TableCell>
);
})}
</TableRow>
);
})}
{emptyRows > 0 && (
<TableRow style={{ height: 53 * emptyRows }}>
<TableCell colSpan={columns.length + 1} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[25, 50, 100, 500, 1000]}
component="div"
count={rows.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
labelRowsPerPage={isMobile ? "" : "Rows per page"}
/>
</Paper>
</Box>
);
}
48 changes: 0 additions & 48 deletions src/components/ui/JobDataGrid.tsx

This file was deleted.

Loading

0 comments on commit 20bfa45

Please sign in to comment.