@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
268 lines (267 loc) • 13.9 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/*
* Copyright 2025 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Icon, InlineIcon } from '@iconify/react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
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 React from 'react';
import { useTranslation } from 'react-i18next';
import { getTablesRowsPerPage, setTablesRowsPerPage } from '../../helpers/tablesRowsPerPage';
import { useURLState } from '../../lib/util';
import { useSettings } from '../App/Settings/hook';
import Empty from './EmptyContent';
import Loader from './Loader';
function ColumnSortButtons(props) {
const { t } = useTranslation();
const { isDefaultSorted, isIncreasingOrder, clickHandler } = props;
return isDefaultSorted ? (_jsx(IconButton, { "aria-label": isIncreasingOrder ? t('translation|sort up') : t('translation|sort down'), size: "small", onClick: () => clickHandler(!isIncreasingOrder), children: _jsx(Icon, { icon: isIncreasingOrder ? 'mdi:menu-up' : 'mdi:menu-down' }) })) : (_jsx(IconButton, { "aria-label": t('translation|sort swap'), size: "small", onClick: () => clickHandler(true), children: _jsx(Icon, { icon: "mdi:menu-swap" }) }));
}
// Use a zero-indexed "useURLState" hook, so pages are shown in the URL as 1-indexed
// but internally are 0-indexed.
function usePageURLState(key, prefix, initialPage) {
const [page, setPage] = useURLState(key, { defaultValue: initialPage + 1, prefix });
const [zeroIndexPage, setZeroIndexPage] = React.useState(page - 1);
React.useEffect(() => {
setZeroIndexPage((zeroIndexPage) => {
if (page - 1 !== zeroIndexPage) {
return page - 1;
}
return zeroIndexPage;
});
}, [page]);
React.useEffect(() => {
setPage(zeroIndexPage + 1);
}, [zeroIndexPage]);
return [zeroIndexPage, setZeroIndexPage];
}
export default function SimpleTable(props) {
const { columns, data, filterFunction = null, emptyMessage = null, page: initialPage = 0,
// @todo: This is a workaround due to how the pagination is built by default.
showPagination = !import.meta.env.UNDER_TEST, // Disable for snapshots: The pagination uses useId so snapshots will fail.
errorMessage = null, defaultSortingColumn, noTableHeader = false, reflectInURL, className, sx, } = props;
const shouldReflectInURL = reflectInURL !== undefined && reflectInURL !== false;
const prefix = reflectInURL === true ? '' : reflectInURL || '';
const [page, setPage] = usePageURLState(shouldReflectInURL ? 'p' : '', prefix, initialPage);
const [currentData, setCurrentData] = React.useState(data);
const [displayData, setDisplayData] = React.useState(data);
const storeRowsPerPageOptions = useSettings('tableRowsPerPageOptions');
const rowsPerPageOptions = props.rowsPerPage || storeRowsPerPageOptions;
const defaultRowsPerPage = React.useMemo(() => getTablesRowsPerPage(rowsPerPageOptions[0]), []);
const [rowsPerPage, setRowsPerPage] = useURLState(shouldReflectInURL ? 'perPage' : '', {
defaultValue: defaultRowsPerPage,
prefix,
});
const gridTemplateColumns = React.useMemo(() => {
const columnsTemplates = columns.map(column => column.gridTemplate || 1);
const templates = [];
columnsTemplates.forEach(template => {
if (typeof template === 'number') {
templates.push(`${template}fr`);
}
else if (typeof template === 'string') {
templates.push(template);
}
});
return templates.join(' ');
}, [columns]);
const [isIncreasingOrder, setIsIncreasingOrder] = React.useState(!defaultSortingColumn || defaultSortingColumn > 0);
// We use a -1 value here if no sorting should be done by default.
const [sortColIndex, setSortColIndex] = React.useState(defaultSortingColumn ? Math.abs(defaultSortingColumn) - 1 : -1);
const { t } = useTranslation();
function handleChangePage(_event, newPage) {
setPage(newPage);
}
// Protect against invalid page values
React.useEffect(() => {
if (page < 0) {
setPage(0);
return;
}
if (displayData && page * rowsPerPage > displayData.length) {
setPage(Math.floor(displayData.length / rowsPerPage));
}
}, [page, displayData, rowsPerPage]);
function handleChangeRowsPerPage(event) {
const numRows = +event.target.value;
setTablesRowsPerPage(numRows);
setRowsPerPage(numRows);
setPage(0);
}
React.useEffect(() => {
if (currentData === data) {
return;
}
// If the currentData is not up to date and we are in the first page, then update
// it directly. Otherwise it will require user's intervention.
if (!currentData || currentData.length === 0 || page === 0) {
setCurrentData(data);
setDisplayData(getSortData() || data);
}
},
// eslint-disable-next-line
[data, currentData]);
function defaultSortingFunction(column) {
const sort = column?.sort;
function defaultSortingReal(item1, item2) {
let getterFunc = column.getter;
if (!!sort && typeof sort === 'function') {
getterFunc = sort;
}
// If instead of a getter function, we have a datum, then we use it to fetch the values for
// comparison.
const datum = column.datum;
if (!getterFunc && !!datum) {
getterFunc = (item) => item[datum];
}
const value1 = getterFunc(item1);
const value2 = getterFunc(item2);
let compareValue = 0;
if (value1 < value2) {
compareValue = -1;
}
else if (value1 > value2) {
compareValue = 1;
}
return compareValue * (isIncreasingOrder ? 1 : -1);
}
return defaultSortingReal;
}
function getSortData() {
if (!data || sortColIndex < 0) {
return null;
}
let applySort = undefined;
const columnAskingForSort = columns[sortColIndex];
const sortFunction = columnAskingForSort?.sort;
if ((typeof sortFunction === 'boolean' && sortFunction) ||
(typeof sortFunction === 'function' && sortFunction.length === 1)) {
setDisplayData(data.slice().sort(defaultSortingFunction(columnAskingForSort)));
return;
}
if (typeof sortFunction === 'function') {
applySort = (arg1, arg2) => {
const orderChanger = isIncreasingOrder ? 1 : -1;
return sortFunction(arg1, arg2) * orderChanger;
};
const sortedData = data.slice().sort(applySort);
return sortedData;
}
}
React.useEffect(() => {
const sortedData = getSortData();
if (!sortedData) {
return;
}
setDisplayData(sortedData);
},
// eslint-disable-next-line
[sortColIndex, isIncreasingOrder, currentData]);
function getPagedRows() {
const startIndex = page * rowsPerPage;
return filteredData.slice(startIndex, startIndex + rowsPerPage);
}
if (displayData === null) {
if (!!errorMessage) {
return _jsx(Empty, { color: "error", children: errorMessage });
}
return _jsx(Loader, { title: t('Loading table data') });
}
let filteredData = displayData;
if (filterFunction) {
filteredData = displayData?.filter(filterFunction);
}
if ((filteredData?.length === 0 || (filteredData?.length ?? 0) < page * rowsPerPage) &&
page !== 0) {
setPage(0);
}
function sortClickHandler(isIncreasingOrder, index) {
setIsIncreasingOrder(isIncreasingOrder);
setSortColIndex(index);
}
return !currentData || currentData.length === 0 ? (_jsx(Paper, { variant: "outlined", children: _jsx(Empty, { children: emptyMessage || t('No data to be shown.') }) })) : (_jsxs(TableContainer, { className: className, sx: {
overflowY: 'hidden',
...sx,
}, component: Paper, variant: "outlined", children: [
// Show a refresh button if the data is not up to date, so we allow the user to keep
// reading the current data without "losing" it or being sent to the first page
currentData !== data && page !== 0 && (_jsx(Box, { textAlign: "center", p: 2, children: _jsx(Button, { variant: "contained", startIcon: _jsx(Icon, { icon: "mdi:refresh" }), onClick: () => {
setCurrentData(data);
setPage(0);
}, children: t('translation|Refresh') }) })), _jsxs(Table, { sx: theme => ({
minWidth: '100%',
width: 'auto',
display: 'grid',
gridTemplateColumns: gridTemplateColumns || '1fr',
background: theme.palette.background.default,
[theme.breakpoints.down('sm')]: {
overflowX: 'auto', // make it responsive
},
'& .MuiTableCell-root': {
borderColor: theme.palette.divider,
padding: '8px 16px 7px 16px',
[theme.breakpoints.down('sm')]: {
padding: '15px 24px 15px 16px',
},
overflow: 'hidden',
width: '100%',
wordWrap: 'break-word',
},
'& .MuiTableBody-root': {
'& .MuiTableRow-root:last-child': {
'& .MuiTableCell-root': {
borderBottom: 'none',
},
},
},
'& .MuiTableCell-head': {
overflow: 'hidden',
textOverflow: 'unset',
whiteSpace: 'nowrap',
color: theme.palette.tables.head.text,
background: theme.palette.background.muted,
width: '100%',
minWidth: 'max-content',
},
'& .MuiTableHead-root, & .MuiTableRow-root, & .MuiTableBody-root': {
display: 'contents',
},
}), size: "small", children: [!noTableHeader && (_jsx(TableHead, { children: _jsx(TableRow, { children: columns.map(({ label, header, cellProps = {}, sort }, i) => {
const { className = '', ...otherProps } = cellProps;
return (_jsxs(TableCell, { className: className, sx: theme => ({
fontWeight: 'bold',
paddingBottom: theme.spacing(0.5),
...(sort ? { whiteSpace: 'nowrap' } : {}),
}), ...otherProps, children: [header || label, sort && (_jsx(ColumnSortButtons, { isIncreasingOrder: Boolean(isIncreasingOrder), isDefaultSorted: sortColIndex === i, clickHandler: (isIncreasingOrder) => sortClickHandler(isIncreasingOrder, i) }))] }, `tabletitle_${i}`));
}) }) })), _jsx(TableBody, { children: filteredData.length > 0 ? (getPagedRows().map((row, i) => (_jsx(TableRow, { children: columns.map((col, i) => {
const { cellProps = {} } = col;
return (_jsxs(TableCell, { ...cellProps, children: [i === 0 && row.color && (_jsxs(React.Fragment, { children: [_jsx(InlineIcon, { icon: "mdi:square", color: row.color, height: "15", width: "15" }), "\u00A0"] })), 'datum' in col ? row[col.datum] : col.getter(row)] }, `cell_${i}`));
}) }, i)))) : (_jsx(TableRow, { children: _jsx(TableCell, { style: { gridColumn: `span ${columns.length}` }, children: _jsx(Empty, { children: t('No data matching the filter criteria.') }) }) })) })] }), filteredData.length > rowsPerPageOptions[0] && showPagination && (_jsx(TablePagination, { rowsPerPageOptions: rowsPerPageOptions, component: "div", count: filteredData.length, rowsPerPage: rowsPerPage, showFirstButton: true, showLastButton: true, page: page, backIconButtonProps: {
'aria-label': 'previous page',
}, nextIconButtonProps: {
'aria-label': 'next page',
}, onPageChange: handleChangePage, onRowsPerPageChange: handleChangeRowsPerPage }))] }));
}
// For legacy reasons.
export * from './NameValueTable';