From 38e1f900e7df8013c0bdddfa02a493170666bc93 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Mon, 30 Jun 2025 21:38:26 -0500 Subject: [PATCH] TableNG: Specialize cell root and cell content renderers (#107427) --- .../datasources/graphite/configure/index.md | 68 ++--- .../graphite/template-variables/index.md | 8 +- .../src/components/Table/TableNG/TableNG.tsx | 261 +++++++++--------- .../src/components/Table/TableNG/utils.ts | 15 +- 4 files changed, 176 insertions(+), 176 deletions(-) diff --git a/docs/sources/datasources/graphite/configure/index.md b/docs/sources/datasources/graphite/configure/index.md index d7e03817c45..ab384c99560 100644 --- a/docs/sources/datasources/graphite/configure/index.md +++ b/docs/sources/datasources/graphite/configure/index.md @@ -88,52 +88,52 @@ Grafana takes you to the **Settings** tab, where you will set up your Graphite c Following is a list of configuration options for Graphite. -| Setting | Description | -|-------------|-----------------------------------------------------------------------------------------------------------------------------------------| -| **Name** | The display name for the data source. This is how you'll reference it in panels and queries.
Examples: `graphite-1`, `graphite-metrics`. | -| **Default** | When enabled, sets this data source as the default for dashboard panels. It will be automatically selected when creating new panels. | +| Setting | Description | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | The display name for the data source. This is how you'll reference it in panels and queries.
Examples: `graphite-1`, `graphite-metrics`. | +| **Default** | When enabled, sets this data source as the default for dashboard panels. It will be automatically selected when creating new panels. | **HTTP:** -| Setting | Description | -|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **URL** | Sets the HTTP protocol, IP, and port of your `graphite-web` or `graphite-api` installation.
Since the access method is set to *Server*, the URL must be accessible from the Grafana backend. | -| **Allowed cookies**| By default, Grafana removes forwarded cookies. Specify cookie names here to allow them to be forwarded to the data source. | -| **Timeout** | Sets the HTTP request timeout in seconds. | +| Setting | Description | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **URL** | Sets the HTTP protocol, IP, and port of your `graphite-web` or `graphite-api` installation.
Since the access method is set to _Server_, the URL must be accessible from the Grafana backend. | +| **Allowed cookies** | By default, Grafana removes forwarded cookies. Specify cookie names here to allow them to be forwarded to the data source. | +| **Timeout** | Sets the HTTP request timeout in seconds. | **Auth:** -| **Setting** | **Description** | -|------------------------------|----------------------------------------------------------------------------------------------------------------------------------| -| **Basic Auth** | Toggle on to enable basic authentication to the data source. | -|   **User** | Sets the username used for basic authentication. | -|   **Password** | Enter the password used for basic authentication. | -| **With Credentials** | Toggle on to include cookies and authentication headers in cross-origin requests. | -| **TLS Client Auth** | Toggle on to enable TLS client authentication (both server and client are verified). | -|   **ServerName** | The server name used to verify the hostname on the certificate returned by the server. | -|   **Client Cert** | Client certificate generated by a Certificate Authority (CA) or self-signed. | -|   **Client Key** | Private key used to encrypt communication between the client and server. Also generated by a CA or self-signed. | -| **With CA Cert** | Toggle on to authenticate with a CA certificate. | -|   **CA Cert** | CA certificate used to validate the server certificate. | -| **Skip TLS Verify** | Toggle on to bypass TLS certificate validation. Not recommended unless necessary or for testing purposes. | -| **Forward OAuth Identity** | Toggle on to forward the user's upstream OAuth identity to the data source. Grafana includes the access token in the request. | +| **Setting** | **Description** | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| **Basic Auth** | Toggle on to enable basic authentication to the data source. | +|   **User** | Sets the username used for basic authentication. | +|   **Password** | Enter the password used for basic authentication. | +| **With Credentials** | Toggle on to include cookies and authentication headers in cross-origin requests. | +| **TLS Client Auth** | Toggle on to enable TLS client authentication (both server and client are verified). | +|   **ServerName** | The server name used to verify the hostname on the certificate returned by the server. | +|   **Client Cert** | Client certificate generated by a Certificate Authority (CA) or self-signed. | +|   **Client Key** | Private key used to encrypt communication between the client and server. Also generated by a CA or self-signed. | +| **With CA Cert** | Toggle on to authenticate with a CA certificate. | +|   **CA Cert** | CA certificate used to validate the server certificate. | +| **Skip TLS Verify** | Toggle on to bypass TLS certificate validation. Not recommended unless necessary or for testing purposes. | +| **Forward OAuth Identity** | Toggle on to forward the user's upstream OAuth identity to the data source. Grafana includes the access token in the request. | **Custom HTTP Headers:** Pass along additional information and metadata about the request or response. -| **Setting** | **Description** | -|-------------|--------------------------------------------------------------------------------------------------| +| **Setting** | **Description** | +| ----------- | ---------------------------------------------------------------------------------------------------------- | | **Header** | Add a custom header. This allows custom headers to be passed based on the needs of your Graphite instance. | -| **Value** | The value of the header. | +| **Value** | The value of the header. | **Graphite details:** -| **Setting** | **Description** | -|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Version** | Select your Graphite version from the drop-down. This controls which functions are available in the Graphite query editor. Use `1.1.x` for Grafana Cloud Graphite. | -| **Graphite backend type**| Select the Graphite backend type. Choosing `Metrictank` enables additional features like query processing metadata. (`Metrictank` is a multi-tenant time series engine compatible with Graphite.) Use `Default` for Grafana Cloud Graphite. | -| **Rollup indicator** | Toggle on to display an info icon in panel headers when data aggregation (rollup) occurs. Only available when `Metrictank` is selected. | +| **Setting** | **Description** | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Version** | Select your Graphite version from the drop-down. This controls which functions are available in the Graphite query editor. Use `1.1.x` for Grafana Cloud Graphite. | +| **Graphite backend type** | Select the Graphite backend type. Choosing `Metrictank` enables additional features like query processing metadata. (`Metrictank` is a multi-tenant time series engine compatible with Graphite.) Use `Default` for Grafana Cloud Graphite. | +| **Rollup indicator** | Toggle on to display an info icon in panel headers when data aggregation (rollup) occurs. Only available when `Metrictank` is selected. | **Label mappings:** @@ -149,10 +149,10 @@ Grafana automatically maps all Graphite tags to labels, even if you haven’t de | `alias(servers.*.{001,002}.*,1,2)` | `{server=~"(001,002)"}` | | `interpolate(seriesByTag('foo=bar', 'server=002'), inf)` | `{foo="bar", server="002"}` | +| **Setting** | **Description** | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Private data source connect** | _Only for Grafana Cloud users._ Establishes a private, secured connection between a Grafana Cloud stack and data sources within a private network. Use the drop-down to locate the PDC URL. For setup instructions, refer to [Private data source connect (PDC)](ref:private-data-source-connect) and [Configure PDC](ref:configure-pdc). Click **Manage private data source connect** to open your PDC connection page and view your configuration details. | -| **Setting** | **Description** | -|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Private data source connect** | _Only for Grafana Cloud users._ Establishes a private, secured connection between a Grafana Cloud stack and data sources within a private network. Use the drop-down to locate the PDC URL. For setup instructions, refer to [Private data source connect (PDC)](ref:private-data-source-connect) and [Configure PDC](ref:configure-pdc). Click **Manage private data source connect** to open your PDC connection page and view your configuration details. | After configuring your Graphite data source options, click **Save & test** at the bottom to test the connection. You should see a confirmation dialog box that says: diff --git a/docs/sources/datasources/graphite/template-variables/index.md b/docs/sources/datasources/graphite/template-variables/index.md index cb1633225cb..b79feb5e3d0 100644 --- a/docs/sources/datasources/graphite/template-variables/index.md +++ b/docs/sources/datasources/graphite/template-variables/index.md @@ -48,11 +48,11 @@ To view an example templated dashboard, refer to [Graphite Templated Nested dash With Graphite data sources, you can only create query variables. Grafana supports three specific query types for Graphite-based variables: -| Query type | Description | Example usage | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| Query type | Description | Example usage | +| --------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------- | | **Default query** | Allows you to dynamically list metrics, nodes, or tag values using Graphite functions. | `tag_values(apps.*.requests.count, app)` | -| **Value query** | Returns all the values for a query that includes a metric and function. | `tag_values(apps.*.status.*, status)` | -| **Metric name query** | Returns all the names for a query that includes a metric and function. | `apps.*.requests.count` | +| **Value query** | Returns all the values for a query that includes a metric and function. | `tag_values(apps.*.status.*, status)` | +| **Metric name query** | Returns all the names for a query that includes a metric and function. | `apps.*.requests.count` | ### Choose a variable syntax diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index d2e38be4f58..625a4b4b106 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -1,7 +1,7 @@ import 'react-data-grid/lib/styles.css'; import { css, cx } from '@emotion/css'; import { Property } from 'csstype'; -import { Key, useLayoutEffect, useMemo, useState } from 'react'; +import { Key, ReactNode, useLayoutEffect, useMemo, useState } from 'react'; import { Cell, CellRendererProps, @@ -22,7 +22,7 @@ import { MenuItem } from '../../Menu/MenuItem'; import { Pagination } from '../../Pagination/Pagination'; import { PanelContext, usePanelContext } from '../../PanelChrome'; import { TableCellInspector, TableCellInspectorMode } from '../TableCellInspector'; -import { CellColors } from '../types'; +import { CellColors, TableCellDisplayMode } from '../types'; import { HeaderCell } from './Cells/HeaderCell'; import { RowExpander } from './Cells/RowExpander'; @@ -55,6 +55,8 @@ import { getCellOptions, } from './utils'; +type CellRootRenderer = (key: React.Key, props: CellRendererProps) => React.ReactNode; + export function TableNG(props: TableNGProps) { const { cellHeight, @@ -137,10 +139,6 @@ export function TableNG(props: TableNGProps) { // vt scrollbar accounting for column auto-sizing const visibleFields = useMemo(() => getVisibleFields(data.fields), [data.fields]); - const visibleFieldsByDisplayName: Record = useMemo( - () => visibleFields.reduce((acc, f) => ({ ...acc, [getDisplayName(f)]: f }), {}), - [visibleFields] - ); const availableWidth = useMemo( () => (hasNestedFrames ? width - COLUMN.EXPANDER_WIDTH : width), [width, hasNestedFrames] @@ -175,11 +173,6 @@ export function TableNG(props: TableNGProps) { [data, enableSharedCrosshair, expandedRows, panelContext] ); - const renderCell = useMemo( - () => renderCellFactory(columnTypes, applyToRowBgFn, rowHeight, textWraps, theme, visibleFieldsByDisplayName), - [columnTypes, applyToRowBgFn, rowHeight, textWraps, theme, visibleFieldsByDisplayName] - ); - const commonDataGridProps = useMemo( () => ({ @@ -240,9 +233,19 @@ export function TableNG(props: TableNGProps) { ] ); - const columns = useMemo((): TableColumn[] => { - const columnsFromFields = (f: Field[], w: number[]): TableColumn[] => - f.map((field, i): TableColumn => { + interface Schema { + columns: TableColumn[]; + cellRootRenderers: Record; + } + + const { columns, cellRootRenderers } = useMemo(() => { + const fromFields = (f: Field[], widths: number[]) => { + const result: Schema = { + columns: [], + cellRootRenderers: {}, + }; + + f.forEach((field, i) => { const justifyContent = getTextAlign(field); const footerStyles = getFooterStyles(justifyContent); const displayName = getDisplayName(field); @@ -253,7 +256,7 @@ export function TableNG(props: TableNGProps) { const cellInspect = Boolean(field.config.custom?.inspect); const showFilters = Boolean(field.config.filterable && onCellFilterAdded != null); const showActions = cellInspect || showFilters; - const width = w[i]; + const width = widths[i]; const frame = data; // helps us avoid string cx and emotion per-cell @@ -265,54 +268,99 @@ export function TableNG(props: TableNGProps) { ) : undefined; - return { + const cellType = cellOptions.type; + const fieldType = columnTypes[displayName]; + const shouldWrap = textWraps[displayName]; + const shouldOverflow = shouldTextOverflow(fieldType, cellType, shouldWrap, cellInspect); + + let lastRowIdx = -1; + let _rowHeight = 0; + + // this fires first + const renderCellRoot = (key: Key, props: CellRendererProps): ReactNode => { + const rowIdx = props.row.__index; + const value = props.row[props.column.key]; + + // meh, this should be cached by the renderRow() call? + if (rowIdx !== lastRowIdx) { + _rowHeight = typeof rowHeight === 'function' ? rowHeight(props.row) : rowHeight; + lastRowIdx = rowIdx; + } + + let colors: CellColors; + + if (applyToRowBgFn != null) { + colors = applyToRowBgFn(props.rowIdx); + } else if (cellType !== TableCellDisplayMode.Auto) { + const displayValue = field.display!(value); // this fires here to get colors, then again to get rendered value? + colors = getCellColors(theme, cellOptions, displayValue); + } else { + colors = {}; + } + + const cellStyle = getCellStyles(theme, field, _rowHeight, shouldWrap, shouldOverflow, colors); + + return ( + + ); + }; + + result.cellRootRenderers[displayName] = renderCellRoot; + + // this fires second + const renderCellContent = (props: RenderCellProps): JSX.Element => { + const rowIdx = props.row.__index; + const value = props.row[props.column.key]; + + // TODO: defer until click? + const actions = getActions?.(frame, field, props.row.__index, replaceVariables); + + return ( + <> + {renderFieldCell({ + actions, + cellOptions, + frame, + field, + height, + justifyContent, + rowIdx, + theme, + value, + width, + cellInspect, + showFilters, + })} + {showActions && ( + + )} + + ); + }; + + const column: TableColumn = { field, key: displayName, name: displayName, width, headerCellClass, - renderCell: (props: RenderCellProps): JSX.Element => { - // TODO: once per row - const height = typeof rowHeight === 'function' ? rowHeight(props.row) : rowHeight; - // TODO: defer until click? - const actions = getActions?.(frame, field, props.row.__index, replaceVariables); - - const rowIdx = props.row.__index; - const value = props.row[displayName]; - - return ( - <> - {renderFieldCell({ - actions, - cellOptions, - frame, - field, - height, - justifyContent, - rowIdx, - theme, - value, - width, - cellInspect, - showFilters, - })} - {showActions && ( - - )} - - ); - }, + renderCell: renderCellContent, renderHeaderCell: ({ column, sortDirection }): JSX.Element => ( {footerCalcs[i]}; }, }; + + result.columns.push(column); }); - const result: TableColumn[] = columnsFromFields(visibleFields, widths); + return result; + }; + + const result = fromFields(visibleFields, widths); // handle nested frames rendering from here. if (!hasNestedFrames) { @@ -356,13 +409,17 @@ export function TableNG(props: TableNGProps) { } const renderRow = renderRowFactory(firstNestedData.fields, panelContext, expandedRows, enableSharedCrosshair); - const expandedColumns = columnsFromFields( + const { columns: nestedColumns, cellRootRenderers: nestedCellRootRenderers } = fromFields( firstNestedData.fields, computeColWidths(firstNestedData.fields, availableWidth) ); + const renderCellRoot: CellRootRenderer = (key, props) => nestedCellRootRenderers[props.column.key](key, props); + + result.cellRootRenderers.expanded = (key, props) => ; + // If we have nested frames, we need to add a column for the row expansion - result.unshift({ + result.columns.unshift({ key: 'expanded', name: '', field: { @@ -372,16 +429,16 @@ export function TableNG(props: TableNGProps) { values: [], }, cellClass(row) { - if (Number(row.__depth) !== 0) { + if (row.__depth !== 0) { return styles.cellNested; } return; }, colSpan(args) { - return args.type === 'ROW' && Number(args.row.__depth) === 1 ? data.fields.length : 1; + return args.type === 'ROW' && args.row.__depth === 1 ? data.fields.length : 1; }, renderCell: ({ row }) => { - if (Number(row.__depth) === 0) { + if (row.__depth === 0) { return ( {...commonDataGridProps} className={cx(styles.grid, styles.gridNested)} - columns={expandedColumns} + columns={nestedColumns} rows={expandedRecords} - renderers={{ renderRow, renderCell }} + renderers={{ renderRow, renderCell: renderCellRoot }} /> ); }, @@ -433,7 +490,6 @@ export function TableNG(props: TableNGProps) { onCellFilterAdded, panelContext, replaceVariables, - renderCell, rows, rowHeight, setFilter, @@ -443,6 +499,10 @@ export function TableNG(props: TableNGProps) { theme, visibleFields, widths, + applyToRowBgFn, + columnTypes, + height, + textWraps, ]); // invalidate columns on every structureRev change. this supports width editing in the fieldConfig. @@ -454,6 +514,10 @@ export function TableNG(props: TableNGProps) { const displayedEnd = pageRangeEnd; const numRows = sortedRows.length; + const renderCellRoot: CellRootRenderer = (key, props) => { + return cellRootRenderers[props.column.key](key, props); + }; + return ( <> @@ -471,7 +535,7 @@ export function TableNG(props: TableNGProps) { } : null } - renderers={{ renderRow, renderCell }} + renderers={{ renderRow, renderCell: renderCellRoot }} /> {enablePagination && ( @@ -538,11 +602,11 @@ const renderRowFactory = ) => (key: React.Key, props: RenderRowProps): React.ReactNode => { const { row } = props; - const rowIdx = Number(row.__index); + const rowIdx = row.__index; const isExpanded = !!expandedRows[rowIdx]; // Don't render non expanded child rows - if (Number(row.__depth) === 1 && !isExpanded) { + if (row.__depth === 1 && !isExpanded) { return null; } @@ -573,63 +637,6 @@ const renderRowFactory = return ; }; -/** - * passed to the top-level `renderCell` prop on DataGrid. This applies all per-cell styles. - */ -const renderCellFactory = - ( - columnTypes: Record, - applyToRowBgFn: ((rowIdx: number) => CellColors) | undefined, - rowHeight: number | ((row: TableRow) => number), - textWraps: Record, - theme: GrafanaTheme2, - visibleFieldsByDisplayName: Record - ) => - (key: Key, props: CellRendererProps) => { - const displayName = props.column.key; - const field = visibleFieldsByDisplayName[displayName]; - - // exit early if we fail to look up the field from the column key. - if (!field) { - return ; - } - - const cellOptions = getCellOptions(field); - const cellType = cellOptions.type; - const value = props.row[props.column.key]; - - const colors: CellColors = (() => { - if (applyToRowBgFn) { - return applyToRowBgFn(props.rowIdx); - } - const displayValue = field.display?.(value); - if (displayValue && cellOptions) { - return getCellColors(theme, cellOptions, displayValue); - } - return {}; - })(); - - const rh = typeof rowHeight === 'function' ? rowHeight(props.row) : rowHeight; - const shouldOverflow = shouldTextOverflow( - displayName, - columnTypes, - textWraps[getDisplayName(field)], - field, - cellType - ); - const shouldWrap = textWraps[displayName] ?? false; - const cellStyle = getCellStyles(theme, field, rh, shouldWrap, shouldOverflow, colors); - - return ( - - ); - }; - const getGridStyles = ( theme: GrafanaTheme2, { enablePagination, noHeader }: { enablePagination?: boolean; noHeader?: boolean } diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index d65b1cd9ff5..79afab8b076 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -74,21 +74,14 @@ export function getDefaultRowHeight(theme: GrafanaTheme2, cellHeight?: TableCell * Returns true if text overflow handling should be applied to the cell. */ export function shouldTextOverflow( - key: string, - columnTypes: ColumnTypes, + fieldType: FieldType, + cellType: TableCellDisplayMode, textWrap: boolean, - field: Field, - cellType: TableCellDisplayMode + cellInspect: boolean ): boolean { - const cellInspect = field.config?.custom?.inspect ?? false; - // Tech debt: Technically image cells are of type string, which is misleading (kinda?) // so we need to ensure we don't apply overflow hover states fo type image - if (textWrap || cellInspect || cellType === TableCellDisplayMode.Image || columnTypes[key] !== FieldType.string) { - return false; - } - - return true; + return fieldType === FieldType.string && cellType !== TableCellDisplayMode.Image && !textWrap && !cellInspect; } /**