UNPKG

@grafana/ui

Version:
695 lines (692 loc) • 23.5 kB
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