@grafana/ui
Version:
Grafana Components Library
695 lines (692 loc) • 23.5 kB
JavaScript
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import 'react-data-grid/lib/styles.css';
import { cx, css } from '@emotion/css';
import { useCallback, useState, useLayoutEffect, useMemo } from 'react';
import { Cell, DataGrid, Row } from 'react-data-grid';
import { ReducerID, FieldType, DataHoverEvent, DataHoverClearEvent } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { TableCellHeight, TableCellDisplayMode } from '@grafana/schema';
import { useTheme2, useStyles2 } from '../../../themes/ThemeContext.mjs';
import { ContextMenu } from '../../ContextMenu/ContextMenu.mjs';
import { MenuItem } from '../../Menu/MenuItem.mjs';
import { Pagination } from '../../Pagination/Pagination.mjs';
import '../../PanelChrome/LoadingIndicator.mjs';
import 'react-use';
import '@grafana/e2e-selectors';
import 'tinycolor2';
import '../../ElementSelectionContext/ElementSelectionContext.mjs';
import '../../Icon/Icon.mjs';
import '../../Text/Text.mjs';
import '../../Tooltip/Tooltip.mjs';
import '../../Dropdown/Dropdown.mjs';
import '../../ToolbarButton/ToolbarButton.mjs';
import '../../PanelChrome/TitleItem.mjs';
import { usePanelContext } from '../../PanelChrome/PanelContext.mjs';
import { DataLinksActionsTooltip } from '../DataLinksActionsTooltip.mjs';
import { TableCellInspector, TableCellInspectorMode } from '../TableCellInspector.mjs';
import { HeaderCell } from './Cells/HeaderCell.mjs';
import { RowExpander } from './Cells/RowExpander.mjs';
import { TableCellActions } from './Cells/TableCellActions.mjs';
import { getCellRenderer } from './Cells/renderers.mjs';
import { COLUMN, TABLE } from './constants.mjs';
import { useColumnResize, useFilteredRows, useSortedRows, useTypographyCtx, useHeaderHeight, useRowHeight, usePaginatedRows, useFooterCalcs } from './hooks.mjs';
import { frameToRecords, getIsNestedTable, getDefaultRowHeight, getVisibleFields, computeColWidths, getApplyToRowBgFn, applySort, getDisplayName, getCellLinks, getTextAlign, getCellOptions, isCellInspectEnabled, shouldTextOverflow, shouldTextWrap, withDataLinksActionsTooltip, getCellColors } from './utils.mjs';
function TableNG(props) {
var _a, _b, _c;
const {
cellHeight,
data,
enablePagination = false,
enableSharedCrosshair = false,
enableVirtualization,
footerOptions,
getActions = () => [],
height,
initialSortBy,
noHeader,
onCellFilterAdded,
onColumnResize,
onSortByChange,
showTypeIcons,
structureRev,
width
} = props;
const theme = useTheme2();
const styles = useStyles2(getGridStyles, {
enablePagination,
noHeader
});
const panelContext = usePanelContext();
const getCellActions = useCallback(
(field, rowIdx) => getActions(data, field, rowIdx),
[getActions, data]
);
const hasHeader = !noHeader;
const hasFooter = Boolean((footerOptions == null ? void 0 : footerOptions.show) && ((_a = footerOptions.reducer) == null ? void 0 : _a.length));
const isCountRowsSet = Boolean(
(footerOptions == null ? void 0 : footerOptions.countRows) && footerOptions.reducer && footerOptions.reducer.length && footerOptions.reducer[0] === ReducerID.count
);
const [contextMenuProps, setContextMenuProps] = useState(null);
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
const resizeHandler = useColumnResize(onColumnResize);
useLayoutEffect(() => {
if (!isContextMenuOpen) {
return;
}
function onClick(_event) {
setIsContextMenuOpen(false);
}
window.addEventListener("click", onClick);
return () => {
window.removeEventListener("click", onClick);
};
}, [isContextMenuOpen]);
const rows = useMemo(() => frameToRecords(data), [data]);
const hasNestedFrames = useMemo(() => getIsNestedTable(data.fields), [data]);
const {
rows: filteredRows,
filter,
setFilter,
crossFilterOrder,
crossFilterRows
} = useFilteredRows(rows, data.fields, { hasNestedFrames });
const {
rows: sortedRows,
sortColumns,
setSortColumns
} = useSortedRows(filteredRows, data.fields, { hasNestedFrames, initialSortBy });
const defaultRowHeight = getDefaultRowHeight(theme, cellHeight);
const defaultHeaderHeight = getDefaultRowHeight(theme, TableCellHeight.Sm);
const [isInspecting, setIsInspecting] = useState(false);
const [expandedRows, setExpandedRows] = useState({});
const visibleFields = useMemo(() => getVisibleFields(data.fields), [data.fields]);
const availableWidth = useMemo(
() => hasNestedFrames ? width - COLUMN.EXPANDER_WIDTH : width,
[width, hasNestedFrames]
);
const typographyCtx = useTypographyCtx();
const widths = useMemo(() => computeColWidths(visibleFields, availableWidth), [visibleFields, availableWidth]);
const headerHeight = useHeaderHeight({
columnWidths: widths,
fields: visibleFields,
enabled: hasHeader,
defaultHeight: defaultHeaderHeight,
sortColumns,
showTypeIcons: showTypeIcons != null ? showTypeIcons : false,
typographyCtx
});
const rowHeight = useRowHeight({
columnWidths: widths,
fields: visibleFields,
hasNestedFrames,
defaultHeight: defaultRowHeight,
headerHeight,
expandedRows,
typographyCtx
});
const {
rows: paginatedRows,
page,
setPage,
numPages,
pageRangeStart,
pageRangeEnd,
smallPagination
} = usePaginatedRows(sortedRows, {
enabled: enablePagination,
width: availableWidth,
height,
headerHeight,
footerHeight: hasFooter ? defaultRowHeight : 0,
rowHeight
});
const footerCalcs = useFooterCalcs(sortedRows, data.fields, { enabled: hasFooter, footerOptions, isCountRowsSet });
const applyToRowBgFn = useMemo(() => {
var _a2;
return (_a2 = getApplyToRowBgFn(data.fields, theme)) != null ? _a2 : void 0;
}, [data.fields, theme]);
const renderRow = useMemo(
() => renderRowFactory(data.fields, panelContext, expandedRows, enableSharedCrosshair),
[data, enableSharedCrosshair, expandedRows, panelContext]
);
const commonDataGridProps = useMemo(
() => ({
enableVirtualization,
defaultColumnOptions: {
minWidth: 50,
resizable: true,
sortable: true
// draggable: true,
},
onCellContextMenu: ({ row, column }, event) => {
if (column.key === "expanded") {
return;
}
event.preventGridDefault();
event.preventDefault();
const cellValue = row[column.key];
setContextMenuProps({
// rowIdx: rows.indexOf(row),
value: String(cellValue != null ? cellValue : ""),
top: event.clientY,
left: event.clientX
});
setIsContextMenuOpen(true);
},
onColumnResize: resizeHandler,
onSortColumnsChange: (newSortColumns) => {
setSortColumns(newSortColumns);
onSortByChange == null ? void 0 : onSortByChange(
newSortColumns.map(({ columnKey, direction }) => ({
displayName: columnKey,
desc: direction === "DESC"
}))
);
},
sortColumns,
rowHeight,
headerRowClass: styles.headerRow,
headerRowHeight: headerHeight,
bottomSummaryRows: hasFooter ? [{}] : void 0
}),
[
enableVirtualization,
resizeHandler,
sortColumns,
headerHeight,
styles.headerRow,
rowHeight,
hasFooter,
setSortColumns,
onSortByChange
]
);
const { columns, cellRootRenderers, colsWithTooltip } = useMemo(() => {
var _a2;
const fromFields = (f, widths2) => {
const result2 = {
columns: [],
cellRootRenderers: {},
colsWithTooltip: {}
};
let lastRowIdx = -1;
let _rowHeight = 0;
f.forEach((field, i) => {
const justifyContent = getTextAlign(field);
const footerStyles = getFooterStyles(justifyContent);
const displayName = getDisplayName(field);
const headerCellClass = getHeaderCellStyles(theme, justifyContent).headerCell;
const cellOptions = getCellOptions(field);
const renderFieldCell = getCellRenderer(field, cellOptions);
const cellInspect = isCellInspectEnabled(field);
const showFilters = Boolean(field.config.filterable && onCellFilterAdded != null);
const showActions = cellInspect || showFilters;
const width2 = widths2[i];
const cellActionClassName = showActions ? cx(
"table-cell-actions",
styles.cellActions,
justifyContent === "flex-end" ? styles.cellActionsEnd : styles.cellActionsStart
) : void 0;
const cellType = cellOptions.type;
const shouldOverflow = shouldTextOverflow(field);
const shouldWrap = shouldTextWrap(field);
const withTooltip = withDataLinksActionsTooltip(field, cellType);
result2.colsWithTooltip[displayName] = withTooltip;
const renderCellRoot3 = (key, props2) => {
var _a3;
const rowIdx = props2.row.__index;
const value = props2.row[props2.column.key];
if (rowIdx !== lastRowIdx) {
_rowHeight = typeof rowHeight === "function" ? rowHeight(props2.row) : rowHeight;
lastRowIdx = rowIdx;
}
let colors;
if (applyToRowBgFn != null) {
colors = applyToRowBgFn(props2.rowIdx);
} else if (cellType !== TableCellDisplayMode.Auto) {
const displayValue = field.display(value);
colors = getCellColors(theme, cellOptions, displayValue);
} else {
colors = {};
}
const cellStyle = getCellStyles(theme, field, _rowHeight, shouldWrap, shouldOverflow, withTooltip, colors);
return /* @__PURE__ */ jsx(
Cell,
{
...props2,
className: cx(props2.className, cellStyle.cell),
style: { color: (_a3 = colors.textColor) != null ? _a3 : "inherit" }
},
key
);
};
result2.cellRootRenderers[displayName] = renderCellRoot3;
const renderCellContent = (props2) => {
const rowIdx = props2.row.__index;
const value = props2.row[props2.column.key];
const frame = data;
return /* @__PURE__ */ jsxs(Fragment, { children: [
renderFieldCell({
cellOptions,
frame,
field,
height: _rowHeight,
justifyContent,
rowIdx,
theme,
value,
width: width2,
cellInspect,
showFilters,
getActions: getCellActions
}),
showActions && /* @__PURE__ */ jsx(
TableCellActions,
{
field,
value,
cellOptions,
displayName,
cellInspect,
showFilters,
className: cellActionClassName,
setIsInspecting,
setContextMenuProps,
onCellFilterAdded
}
)
] });
};
const column = {
field,
key: displayName,
name: displayName,
width: width2,
headerCellClass,
renderCell: renderCellContent,
renderHeaderCell: ({ column: column2, sortDirection }) => /* @__PURE__ */ jsx(
HeaderCell,
{
column: column2,
rows,
field,
filter,
setFilter,
crossFilterOrder,
crossFilterRows,
direction: sortDirection,
showTypeIcons
}
),
renderSummaryCell: () => {
if (isCountRowsSet && i === 0) {
return /* @__PURE__ */ jsxs("div", { className: footerStyles.footerCellCountRows, children: [
/* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(Trans, { i18nKey: "grafana-ui.table.count", children: "Count" }) }),
/* @__PURE__ */ jsx("span", { children: footerCalcs[i] })
] });
}
return /* @__PURE__ */ jsx("div", { className: footerStyles.footerCell, children: footerCalcs[i] });
}
};
result2.columns.push(column);
});
return result2;
};
const result = fromFields(visibleFields, widths);
if (!hasNestedFrames) {
return result;
}
const firstNestedData = (_a2 = rows.find((r) => r.data)) == null ? void 0 : _a2.data;
if (!firstNestedData) {
return result;
}
const renderRow2 = renderRowFactory(firstNestedData.fields, panelContext, expandedRows, enableSharedCrosshair);
const { columns: nestedColumns, cellRootRenderers: nestedCellRootRenderers } = fromFields(
firstNestedData.fields,
computeColWidths(firstNestedData.fields, availableWidth)
);
const renderCellRoot2 = (key, props2) => nestedCellRootRenderers[props2.column.key](key, props2);
result.cellRootRenderers.expanded = (key, props2) => /* @__PURE__ */ jsx(Cell, { ...props2 }, key);
result.columns.unshift({
key: "expanded",
name: "",
field: {
name: "",
type: FieldType.other,
config: {},
values: []
},
cellClass(row) {
if (row.__depth !== 0) {
return styles.cellNested;
}
return;
},
colSpan(args) {
return args.type === "ROW" && args.row.__depth === 1 ? data.fields.length : 1;
},
renderCell: ({ row }) => {
var _a3;
if (row.__depth === 0) {
return /* @__PURE__ */ jsx(
RowExpander,
{
height: defaultRowHeight,
isExpanded: (_a3 = expandedRows[row.__index]) != null ? _a3 : false,
onCellExpand: () => {
setExpandedRows({ ...expandedRows, [row.__index]: !expandedRows[row.__index] });
}
}
);
}
const nestedData = row.data;
if (!nestedData) {
return null;
}
const expandedRecords = applySort(frameToRecords(nestedData), nestedData.fields, sortColumns);
return /* @__PURE__ */ jsx(
DataGrid,
{
...commonDataGridProps,
className: cx(styles.grid, styles.gridNested),
columns: nestedColumns,
rows: expandedRecords,
renderers: { renderRow: renderRow2, renderCell: renderCellRoot2 }
}
);
},
width: COLUMN.EXPANDER_WIDTH,
minWidth: COLUMN.EXPANDER_WIDTH
});
return result;
}, [
applyToRowBgFn,
availableWidth,
commonDataGridProps,
crossFilterOrder,
crossFilterRows,
data,
defaultRowHeight,
enableSharedCrosshair,
expandedRows,
filter,
footerCalcs,
hasNestedFrames,
isCountRowsSet,
onCellFilterAdded,
panelContext,
rowHeight,
rows,
setFilter,
showTypeIcons,
sortColumns,
styles,
theme,
visibleFields,
widths,
getCellActions
]);
const structureRevColumns = useMemo(() => columns, [columns, structureRev]);
const itemsRangeStart = pageRangeStart;
const displayedEnd = pageRangeEnd;
const numRows = sortedRows.length;
const renderCellRoot = (key, props2) => {
return cellRootRenderers[props2.column.key](key, props2);
};
const [tooltipState, setTooltipState] = useState();
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(
DataGrid,
{
...commonDataGridProps,
className: styles.grid,
columns: structureRevColumns,
rows: paginatedRows,
onCellClick: ({ column, row }, { clientX, clientY, preventGridDefault }) => {
const field = columns[column.idx].field;
if (colsWithTooltip[getDisplayName(field)]) {
const rowIdx = row.__index;
setTooltipState({
coords: {
clientX,
clientY
},
links: getCellLinks(field, rowIdx),
actions: getCellActions(field, rowIdx)
});
preventGridDefault();
}
},
onCellKeyDown: hasNestedFrames ? (_, event) => {
if (event.isDefaultPrevented()) {
event.preventGridDefault();
}
} : null,
renderers: { renderRow, renderCell: renderCellRoot }
}
),
enablePagination && /* @__PURE__ */ jsxs("div", { className: styles.paginationContainer, children: [
/* @__PURE__ */ jsx(
Pagination,
{
className: "table-ng-pagination",
currentPage: page + 1,
numberOfPages: numPages,
showSmallVersion: smallPagination,
onNavigate: (toPage) => {
setPage(toPage - 1);
}
}
),
!smallPagination && /* @__PURE__ */ jsx("div", { className: styles.paginationSummary, children: /* @__PURE__ */ jsxs(Trans, { i18nKey: "grafana-ui.table.pagination-summary", children: [
{ itemsRangeStart },
" - ",
{ displayedEnd },
" of ",
{ numRows },
" rows"
] }) })
] }),
tooltipState && /* @__PURE__ */ jsx(
DataLinksActionsTooltip,
{
links: (_b = tooltipState.links) != null ? _b : [],
actions: tooltipState.actions,
coords: tooltipState.coords,
onTooltipClose: () => setTooltipState(void 0)
}
),
isContextMenuOpen && /* @__PURE__ */ jsx(
ContextMenu,
{
x: (contextMenuProps == null ? void 0 : contextMenuProps.left) || 0,
y: (contextMenuProps == null ? void 0 : contextMenuProps.top) || 0,
renderMenuItems: () => /* @__PURE__ */ jsx(
MenuItem,
{
label: t("grafana-ui.table.inspect-menu-label", "Inspect value"),
onClick: () => setIsInspecting(true),
className: styles.menuItem
}
),
focusOnOpen: false
}
),
isInspecting && /* @__PURE__ */ jsx(
TableCellInspector,
{
mode: (_c = contextMenuProps == null ? void 0 : contextMenuProps.mode) != null ? _c : TableCellInspectorMode.text,
value: contextMenuProps == null ? void 0 : contextMenuProps.value,
onDismiss: () => {
setIsInspecting(false);
setContextMenuProps(null);
}
}
)
] });
}
const renderRowFactory = (fields, panelContext, expandedRows, enableSharedCrosshair) => (key, props) => {
const { row } = props;
const rowIdx = row.__index;
const isExpanded = !!expandedRows[rowIdx];
if (row.__depth === 1 && !isExpanded) {
return null;
}
if (row.data) {
return /* @__PURE__ */ jsx(Row, { ...props, "aria-expanded": isExpanded }, key);
}
const handlers = {};
if (enableSharedCrosshair) {
const timeField = fields.find((f) => f.type === FieldType.time);
if (timeField) {
handlers.onMouseEnter = () => {
panelContext.eventBus.publish(
new DataHoverEvent({
point: {
time: timeField == null ? void 0 : timeField.values[rowIdx]
}
})
);
};
handlers.onMouseLeave = () => {
panelContext.eventBus.publish(new DataHoverClearEvent());
};
}
}
return /* @__PURE__ */ jsx(Row, { ...props, ...handlers }, key);
};
const getGridStyles = (theme, { enablePagination, noHeader }) => ({
grid: css({
"--rdg-background-color": theme.colors.background.primary,
"--rdg-header-background-color": theme.colors.background.primary,
"--rdg-border-color": theme.isDark ? "#282b30" : "#ebebec",
"--rdg-color": theme.colors.text.primary,
// note: this cannot have any transparency since default cells that
// overlay/overflow on hover inherit this background and need to occlude cells below
"--rdg-row-hover-background-color": theme.isDark ? "#212428" : "#f4f5f5",
// TODO: magic 32px number is unfortunate. it would be better to have the content
// flow using flexbox rather than hard-coding this size via a calc
blockSize: enablePagination ? "calc(100% - 32px)" : "100%",
scrollbarWidth: "thin",
scrollbarColor: theme.isDark ? "#fff5 #fff1" : "#0005 #0001",
border: "none",
".rdg-summary-row": {
".rdg-cell": {
zIndex: theme.zIndex.tooltip - 1,
paddingInline: TABLE.CELL_PADDING,
paddingBlock: TABLE.CELL_PADDING
}
}
}),
gridNested: css({
height: "100%",
width: `calc(100% - ${COLUMN.EXPANDER_WIDTH - 1}px)`,
overflow: "visible",
marginLeft: COLUMN.EXPANDER_WIDTH - 1
}),
cellNested: css({
"&[aria-selected=true]": {
outline: "none"
}
}),
cellActions: css({
display: "none",
position: "absolute",
top: 0,
margin: "auto",
height: "100%",
color: theme.colors.text.primary,
background: theme.isDark ? "rgba(0, 0, 0, 0.3)" : "rgba(255, 255, 255, 0.7)",
padding: theme.spacing.x0_5,
paddingInlineStart: theme.spacing.x1
}),
cellActionsEnd: css({
left: 0
}),
cellActionsStart: css({
right: 0
}),
headerRow: css({
paddingBlockStart: 0,
fontWeight: "normal",
...noHeader ? { display: "none" } : {},
"& .rdg-cell": {
height: "100%",
alignItems: "flex-end"
}
}),
paginationContainer: css({
alignItems: "center",
display: "flex",
justifyContent: "center",
marginTop: "8px",
width: "100%"
}),
paginationSummary: css({
color: theme.colors.text.secondary,
fontSize: theme.typography.bodySmall.fontSize,
display: "flex",
justifyContent: "flex-end",
padding: theme.spacing(0, 1, 0, 2)
}),
menuItem: css({
maxWidth: "200px"
})
});
const getFooterStyles = (justifyContent) => ({
footerCellCountRows: css({
display: "flex",
justifyContent: "space-between"
}),
footerCell: css({
display: "flex",
justifyContent: justifyContent || "space-between"
})
});
const getHeaderCellStyles = (theme, justifyContent) => ({
headerCell: css({
display: "flex",
gap: theme.spacing(0.5),
zIndex: theme.zIndex.tooltip - 1,
paddingInline: TABLE.CELL_PADDING,
paddingBlockEnd: TABLE.CELL_PADDING,
justifyContent,
"&:last-child": {
borderInlineEnd: "none"
}
})
});
const getCellStyles = (theme, field, rowHeight, shouldWrap, shouldOverflow, hasTooltip, colors) => {
var _a;
return {
cell: css({
textOverflow: "initial",
background: (_a = colors.bgColor) != null ? _a : "inherit",
alignContent: "center",
justifyContent: getTextAlign(field),
paddingInline: TABLE.CELL_PADDING,
height: "100%",
minHeight: rowHeight,
// min height interacts with the fit-content property on the overflow container
...shouldWrap && { whiteSpace: "pre-line" },
...hasTooltip && { cursor: "pointer" },
"&:last-child": {
borderInlineEnd: "none"
},
"&:hover": {
background: colors.bgHoverColor,
".table-cell-actions": {
display: "flex"
},
...shouldOverflow && {
zIndex: theme.zIndex.tooltip - 2,
whiteSpace: "pre-line",
height: "fit-content",
minWidth: "fit-content"
}
}
})
};
};
export { TableNG };
//# sourceMappingURL=TableNG.mjs.map