@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
301 lines (300 loc) • 13.4 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import { useTheme } from '@mui/material/styles';
import MuiTable from '@mui/material/Table';
import TableHead from '@mui/material/TableHead';
import { alpha, styled } from '@mui/system';
import { MRT_BottomToolbar, MRT_TableBodyCell, MRT_TableHeadCell, MRT_TopToolbar, useMaterialReactTable, useMRT_Rows, } from 'material-react-table';
import { MRT_Localization_DE } from 'material-react-table/locales/de';
import { MRT_Localization_EN } from 'material-react-table/locales/en';
import { MRT_Localization_ES } from 'material-react-table/locales/es';
import { MRT_Localization_FR } from 'material-react-table/locales/fr';
import { MRT_Localization_IT } from 'material-react-table/locales/it';
import { MRT_Localization_JA } from 'material-react-table/locales/ja';
import { MRT_Localization_KO } from 'material-react-table/locales/ko';
import { MRT_Localization_PT } from 'material-react-table/locales/pt';
import { MRT_Localization_ZH_HANS } from 'material-react-table/locales/zh-Hans';
import { MRT_Localization_ZH_HANT } from 'material-react-table/locales/zh-Hant';
import { memo, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getTablesRowsPerPage } from '../../../helpers/tablesRowsPerPage';
import { useURLState } from '../../../lib/util';
import { useSettings } from '../../App/Settings/hook';
import Empty from '../EmptyContent';
import Loader from '../Loader';
// 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] = useState(page - 1);
useEffect(() => {
setZeroIndexPage((zeroIndexPage) => {
if (page - 1 !== zeroIndexPage) {
return page - 1;
}
return zeroIndexPage;
});
}, [page]);
useEffect(() => {
setPage(zeroIndexPage + 1);
}, [zeroIndexPage]);
return [zeroIndexPage, setZeroIndexPage];
}
const tableLocalizationMap = {
de: MRT_Localization_DE,
en: MRT_Localization_EN,
es: MRT_Localization_ES,
fr: MRT_Localization_FR,
it: MRT_Localization_IT,
ja: MRT_Localization_JA,
pt: MRT_Localization_PT,
ko: MRT_Localization_KO,
zh: MRT_Localization_ZH_HANS,
'zh-TW': MRT_Localization_ZH_HANT,
};
const StyledHeadRow = styled('tr')(({ theme }) => ({
display: 'contents',
background: theme.palette.background.muted,
}));
const StyledRow = styled('tr')(({ theme }) => ({
display: 'contents',
'&[data-selected=true]': {
background: alpha(theme.palette.primary.main, 0.2),
},
}));
const StyledBody = styled('tbody')({ display: 'contents' });
/**
* Table component based on the Material React Table
*
* @see https://www.material-react-table.com/docs
*/
export default function Table({ emptyMessage, reflectInURL, initialPage = 1, rowsPerPage, filterFunction, errorMessage, loading, ...tableProps }) {
const shouldReflectInURL = reflectInURL !== undefined && reflectInURL !== false;
const prefix = reflectInURL === true ? '' : reflectInURL || '';
const [page, setPage] = usePageURLState(shouldReflectInURL ? 'p' : '', prefix, initialPage);
const storeRowsPerPageOptions = useSettings('tableRowsPerPageOptions');
const rowsPerPageOptions = rowsPerPage || storeRowsPerPageOptions;
const defaultRowsPerPage = useMemo(() => getTablesRowsPerPage(rowsPerPageOptions[0]), []);
const [pageSize, setPageSize] = useURLState(shouldReflectInURL ? 'perPage' : '', {
defaultValue: defaultRowsPerPage,
prefix,
});
const { t, i18n } = useTranslation();
const theme = useTheme();
// Provide defaults for the columns
const tableColumns = useMemo(() => tableProps.columns.map((column, i) => ({
...column,
id: column.id ?? String(i),
header: column.header || '',
})), [tableProps.columns]);
const tableData = useMemo(() => {
if (!filterFunction)
return tableProps.data ?? [];
return (tableProps.data ?? []).filter(it => filterFunction(it));
}, [tableProps.data, filterFunction]);
const paginationSelectProps = import.meta.env.UNDER_TEST
? {
inputProps: {
SelectDisplayProps: {
'aria-controls': 'test-id',
},
},
}
: undefined;
const columnOrder = useMemo(() => {
const ids = tableProps.columns.map((it, i) => it.id ?? String(i));
if (tableProps.enableRowActions) {
ids.push('mrt-row-actions');
}
if (tableProps.enableRowSelection) {
ids.unshift('mrt-row-select');
}
return ids;
}, [tableProps.columns, tableProps.enableRowActions, tableProps.enableRowSelection]);
const table = useMaterialReactTable({
...tableProps,
columns: tableColumns ?? [],
data: tableData,
enablePagination: tableData.length > rowsPerPageOptions[0],
enableDensityToggle: tableProps.enableDensityToggle ?? false,
enableFullScreenToggle: tableProps.enableFullScreenToggle ?? false,
enableColumnActions: false,
localization: tableLocalizationMap[i18n.language],
autoResetAll: false,
onPaginationChange: (updater) => {
if (!tableProps.data?.length)
return;
const pagination = updater({ pageIndex: Number(page) - 1, pageSize: Number(pageSize) });
setPage(pagination.pageIndex + 1);
setPageSize(pagination.pageSize);
},
renderToolbarInternalActions: props => {
const isSomeRowsSelected = tableProps.enableRowSelection && props.table.getSelectedRowModel().rows.length !== 0;
if (isSomeRowsSelected) {
const renderRowSelectionToolbar = tableProps.renderRowSelectionToolbar;
if (renderRowSelectionToolbar !== undefined) {
return renderRowSelectionToolbar(props);
}
}
return null;
},
initialState: {
density: 'compact',
...(tableProps.initialState ?? {}),
},
state: {
...(tableProps.state ?? {}),
columnOrder: columnOrder,
pagination: {
pageIndex: page - 1,
pageSize: pageSize,
},
},
positionActionsColumn: 'last',
layoutMode: 'grid',
// Need to provide our own empty message
// because default one breaks with our custom layout
renderEmptyRowsFallback: () => (_jsx(Box, { height: 60, children: _jsx(Box, { position: "absolute", left: 0, right: 0, textAlign: "center", children: _jsx(Empty, { children: t('No results found') }) }) })),
muiSearchTextFieldProps: {
id: 'table-search-field',
},
muiPaginationProps: {
rowsPerPageOptions: rowsPerPageOptions,
showFirstButton: false,
showLastButton: false,
SelectProps: paginationSelectProps,
},
muiTableBodyCellProps: {
sx: {
// By default in compact mode text doesn't wrap
// so we need to override that
whiteSpace: 'normal',
width: 'unset',
minWidth: 'unset',
},
},
muiTopToolbarProps: {
sx: {
height: '3.5rem',
backgroundColor: undefined,
},
},
muiBottomToolbarProps: {
sx: {
backgroundColor: undefined,
boxShadow: undefined,
},
},
muiTableHeadCellProps: {
sx: {
width: 'unset',
minWidth: 'unset',
'.MuiTableSortLabel-icon': {
margin: 0,
width: '14px',
height: '14px',
marginTop: '-2px',
},
',MuiTableSortLabel-root': {
width: 'auto',
},
},
},
muiSelectCheckboxProps: {
size: 'small',
sx: { padding: 0 },
},
muiSelectAllCheckboxProps: {
size: 'small',
sx: { padding: 0 },
},
});
const gridTemplateColumns = useMemo(() => {
let preGridTemplateColumns = tableProps.columns
.filter((it, i) => {
const id = it.id ?? String(i);
const isHidden = table.getState().columnVisibility?.[id] === false ||
tableProps.state?.columnVisibility?.[id] === false;
return !isHidden;
})
.map(it => {
if (typeof it.gridTemplate === 'number') {
return `${it.gridTemplate}fr`;
}
return it.gridTemplate ?? '1fr';
})
.join(' ');
if (tableProps.enableRowActions) {
preGridTemplateColumns = `${preGridTemplateColumns} 0.05fr`;
}
if (tableProps.enableRowSelection) {
preGridTemplateColumns = `44px ${preGridTemplateColumns}`;
}
return preGridTemplateColumns;
}, [
tableProps.columns,
table.getState()?.columnVisibility,
tableProps.state?.columnVisibility,
tableProps.enableRowActions,
tableProps.enableRowSelection,
]);
const rows = useMRT_Rows(table);
if (!!errorMessage) {
return _jsx(Empty, { color: "error", children: errorMessage });
}
if (loading) {
return _jsx(Loader, { title: t('Loading table data') });
}
if (!tableProps.data?.length && !loading) {
return (_jsx(Paper, { variant: "outlined", children: _jsx(Empty, { children: emptyMessage || t('No data to be shown.') }) }));
}
const headerGroups = table.getHeaderGroups();
return (_jsxs(_Fragment, { children: [_jsx(MRT_TopToolbar, { table: table }), _jsxs(MuiTable, { sx: {
display: 'grid',
border: '1px solid',
borderColor: theme.palette.tables.head.borderColor,
borderRadius: 1,
borderBottom: 'none',
overflowX: 'auto',
width: '100%',
gridTemplateColumns,
}, children: [_jsx(TableHead, { sx: { display: 'contents' }, children: _jsx(StyledHeadRow, { children: headerGroups[0].headers.map(header => (_jsx(MemoHeadCell, { header: header, table: table, isFiltered: header.column.getIsFiltered(), sorting: header.column.getIsSorted(), showColumnFilters: table.getState().showColumnFilters, selected: table.getSelectedRowModel().flatRows.length }, header.id))) }) }), _jsx(StyledBody, { children: rows.map(row => (_jsx(Row, { cells: row.getVisibleCells(), table: table, isSelected: row.getIsSelected() }, row.id))) })] }), _jsx(MRT_BottomToolbar, { table: table })] }));
}
const MemoHeadCell = memo(({ header, table, }) => {
return (_jsx(MRT_TableHeadCell, { header: header, staticColumnIndex: -1, table: table, sx: theme => ({ borderColor: theme.palette.divider }) }, header.id));
}, (a, b) => a.header.column.id === b.header.column.id &&
a.sorting === b.sorting &&
a.isFiltered === b.isFiltered &&
a.showColumnFilters === b.showColumnFilters &&
(a.header.column.id === 'mrt-row-select' ? a.selected === b.selected : true));
const Row = memo(({ cells, table, isSelected, }) => (_jsx(StyledRow, { "data-selected": isSelected, children: cells.map(cell => (_jsx(MemoCell, { cell: cell, table: table, isRowSelected: cell.row.getIsSelected(), canSelect: cell.row.getCanSelect() }, cell.id))) })));
const MemoCell = memo(({ cell, table, }) => {
const column = cell.column.columnDef;
return (_jsx(MRT_TableBodyCell, { staticRowIndex: -1, cell: cell, table: table, rowRef: { current: null }, sx: theme => ({
whiteSpace: 'normal',
width: 'unset',
minWidth: 'unset',
wordBreak: column.gridTemplate === 'min-content' ? 'normal' : 'break-word',
borderColor: theme.palette.divider,
...column.muiTableBodyCellProps?.sx,
}) }));
}, (a, b) => a.cell.getValue() === b.cell.getValue() &&
(a.cell.column.id === 'mrt-row-select' && b.cell.column.id === 'mrt-row-select'
? a.canSelect === b.canSelect && a.isRowSelected === b.isRowSelected
: true));