import { css, cx } from '@emotion/css'; import { ReactNode, useState } from 'react'; import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { IconButton, Pagination, useStyles2 } from '@grafana/ui'; import { usePagination } from '../hooks/usePagination'; import { getPaginationStyles } from '../styles/pagination'; interface DynamicTablePagination { itemsPerPage: number; } export interface DynamicTableColumnProps { id: string | number; /** Column header to display */ label: string; alignColumn?: 'end' | string; renderCell: (item: DynamicTableItemProps, index: number) => ReactNode; size?: number | string; className?: string; } export interface DynamicTableItemProps { id: string | number; data: T; renderExpandedContent?: () => ReactNode; } export interface DynamicTableProps { cols: Array>; items: Array>; dataTestId?: string; isExpandable?: boolean; pagination?: DynamicTablePagination; paginationStyles?: string; // provide these to manually control expanded status onCollapse?: (item: DynamicTableItemProps) => void; onExpand?: (item: DynamicTableItemProps) => void; isExpanded?: (item: DynamicTableItemProps) => boolean; renderExpandedContent?: ( item: DynamicTableItemProps, index: number, items: Array> ) => ReactNode; testIdGenerator?: (item: DynamicTableItemProps, index: number) => string; renderPrefixHeader?: () => ReactNode; renderPrefixCell?: ( item: DynamicTableItemProps, index: number, items: Array> ) => ReactNode; footerRow?: React.ReactNode; } export const DynamicTable = ({ cols, items, isExpandable = false, onCollapse, onExpand, isExpanded, renderExpandedContent, testIdGenerator, pagination, paginationStyles, // render a cell BEFORE expand icon for header/ each row. // currently use by RuleList to render guidelines renderPrefixCell, renderPrefixHeader, footerRow, dataTestId, }: DynamicTableProps) => { const defaultPaginationStyles = useStyles2(getPaginationStyles); if ((onCollapse || onExpand || isExpanded) && !(onCollapse && onExpand && isExpanded)) { throw new Error('either all of onCollapse, onExpand, isExpanded must be provided, or none'); } if ((isExpandable || renderExpandedContent) && !(isExpandable && renderExpandedContent)) { throw new Error('either both isExpanded and renderExpandedContent must be provided, or neither'); } const styles = useStyles2(getStyles(cols, isExpandable, !!renderPrefixHeader)); const [expandedIds, setExpandedIds] = useState>([]); const toggleExpanded = (item: DynamicTableItemProps) => { if (isExpanded && onCollapse && onExpand) { isExpanded(item) ? onCollapse(item) : onExpand(item); } else { setExpandedIds( expandedIds.includes(item.id) ? expandedIds.filter((itemId) => itemId !== item.id) : [...expandedIds, item.id] ); } }; const itemsPerPage = pagination?.itemsPerPage ?? items.length; const { page, numberOfPages, onPageChange, pageItems } = usePagination(items, 1, itemsPerPage); return ( <>
{renderPrefixHeader && renderPrefixHeader()} {isExpandable &&
} {cols.map((col) => (
{col.label}
))}
{pageItems.map((item, index) => { const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id); return (
{renderPrefixCell && renderPrefixCell(item, index, items)} {isExpandable && (
toggleExpanded(item)} />
)} {cols.map((col) => (
{col.renderCell(item, index)}
))} {isItemExpanded && renderExpandedContent && (
{renderExpandedContent(item, index, items)}
)}
); })} {footerRow &&
{footerRow}
}
{pagination && ( )} ); }; const getStyles = ( cols: Array>, isExpandable: boolean, hasPrefixCell: boolean ) => { const sizes = cols.map((col) => { if (!col.size) { return 'auto'; } if (typeof col.size === 'number') { return `${col.size}fr`; } return col.size; }); if (isExpandable) { sizes.unshift('calc(1em + 16px)'); } if (hasPrefixCell) { sizes.unshift('0'); } return (theme: GrafanaTheme2) => ({ container: css({ border: `1px solid ${theme.colors.border.weak}`, borderRadius: theme.shape.radius.default, color: theme.colors.text.secondary, }), row: css({ display: 'grid', gridTemplateColumns: sizes.join(' '), gridTemplateRows: '1fr auto', '&:nth-child(2n + 1)': { backgroundColor: theme.colors.background.secondary, }, '&:nth-child(2n)': { backgroundColor: theme.colors.background.primary, }, [theme.breakpoints.down('sm')]: { gridTemplateColumns: 'auto 1fr', gridTemplateAreas: 'left right', padding: `0 ${theme.spacing(0.5)}`, '&:first-child': { display: 'none', }, '& > *:first-child': { display: hasPrefixCell ? 'none' : undefined, }, }, }), footerRow: css({ display: 'flex', padding: theme.spacing(1), }), cell: (alignColumn?: string) => css({ display: 'flex', alignItems: 'center', padding: theme.spacing(1), justifyContent: alignColumn || 'initial', [theme.breakpoints.down('sm')]: { padding: `${theme.spacing(1)} 0`, gridTemplateColumns: '1fr', }, }), bodyCell: css({ overflow: 'hidden', [theme.breakpoints.down('sm')]: { gridColumnEnd: 'right', gridColumnStart: 'right', '&::before': { content: 'attr(data-column)', display: 'block', color: theme.colors.text.primary, }, }, }), expandCell: css({ justifyContent: 'center', [theme.breakpoints.down('sm')]: { alignItems: 'start', gridArea: 'left', }, }), expandedContentRow: css({ gridColumnEnd: sizes.length + 1, gridColumnStart: hasPrefixCell ? 3 : 2, gridRow: 2, padding: `0 ${theme.spacing(3)} 0 ${theme.spacing(1)}`, position: 'relative', [theme.breakpoints.down('sm')]: { gridColumnStart: 2, borderTop: `1px solid ${theme.colors.border.strong}`, gridRow: 'auto', padding: `${theme.spacing(1)} 0 0 0`, }, }), }); };