TableNG: Specialize cell root and cell content renderers (#107427)

This commit is contained in:
Leon Sorokin
2025-07-01 02:38:26 +00:00
committed by GitHub
parent dec2df8379
commit 38e1f900e7
4 changed files with 176 additions and 176 deletions
@@ -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. <br>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. <br>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. <br>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. <br>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. |
| &nbsp;&nbsp;**User** | Sets the username used for basic authentication. |
| &nbsp;&nbsp;**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). |
| &nbsp;&nbsp;**ServerName** | The server name used to verify the hostname on the certificate returned by the server. |
| &nbsp;&nbsp;**Client Cert** | Client certificate generated by a Certificate Authority (CA) or self-signed. |
| &nbsp;&nbsp;**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. |
| &nbsp;&nbsp;**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. |
| &nbsp;&nbsp;**User** | Sets the username used for basic authentication. |
| &nbsp;&nbsp;**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). |
| &nbsp;&nbsp;**ServerName** | The server name used to verify the hostname on the certificate returned by the server. |
| &nbsp;&nbsp;**Client Cert** | Client certificate generated by a Certificate Authority (CA) or self-signed. |
| &nbsp;&nbsp;**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. |
| &nbsp;&nbsp;**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:
@@ -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
@@ -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<TableRow, TableSummaryRow>) => 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<string, Field> = 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[]>((): TableColumn[] => {
const columnsFromFields = (f: Field[], w: number[]): TableColumn[] =>
f.map((field, i): TableColumn => {
interface Schema {
columns: TableColumn[];
cellRootRenderers: Record<string, CellRootRenderer>;
}
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<TableRow, TableSummaryRow>): 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 (
<Cell
key={key}
{...props}
className={cx(props.className, cellStyle.cell)}
style={{ color: colors.textColor ?? 'inherit' }}
/>
);
};
result.cellRootRenderers[displayName] = renderCellRoot;
// this fires second
const renderCellContent = (props: RenderCellProps<TableRow, TableSummaryRow>): 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 && (
<TableCellActions
field={field}
value={value}
cellOptions={cellOptions}
displayName={displayName}
cellInspect={cellInspect}
showFilters={showFilters}
className={cellActionClassName}
setIsInspecting={setIsInspecting}
setContextMenuProps={setContextMenuProps}
onCellFilterAdded={onCellFilterAdded}
/>
)}
</>
);
};
const column: TableColumn = {
field,
key: displayName,
name: displayName,
width,
headerCellClass,
renderCell: (props: RenderCellProps<TableRow, TableSummaryRow>): 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 && (
<TableCellActions
field={field}
value={value}
cellOptions={cellOptions}
displayName={displayName}
cellInspect={cellInspect}
showFilters={showFilters}
className={cellActionClassName}
setIsInspecting={setIsInspecting}
setContextMenuProps={setContextMenuProps}
onCellFilterAdded={onCellFilterAdded}
/>
)}
</>
);
},
renderCell: renderCellContent,
renderHeaderCell: ({ column, sortDirection }): JSX.Element => (
<HeaderCell
column={column}
@@ -340,9 +388,14 @@ export function TableNG(props: TableNGProps) {
return <div className={footerStyles.footerCell}>{footerCalcs[i]}</div>;
},
};
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) => <Cell key={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 (
<RowExpander
height={defaultRowHeight}
@@ -405,9 +462,9 @@ export function TableNG(props: TableNGProps) {
<DataGrid<TableRow, TableSummaryRow>
{...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 (
<>
<DataGrid<TableRow, TableSummaryRow>
@@ -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<TableRow, TableSummaryRow>): 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 <Row key={key} {...props} {...handlers} />;
};
/**
* passed to the top-level `renderCell` prop on DataGrid. This applies all per-cell styles.
*/
const renderCellFactory =
(
columnTypes: Record<string, FieldType>,
applyToRowBgFn: ((rowIdx: number) => CellColors) | undefined,
rowHeight: number | ((row: TableRow) => number),
textWraps: Record<string, boolean>,
theme: GrafanaTheme2,
visibleFieldsByDisplayName: Record<string, Field>
) =>
(key: Key, props: CellRendererProps<TableRow, TableSummaryRow>) => {
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 <Cell key={key} {...props} />;
}
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 (
<Cell
key={key}
{...props}
className={cx(props.className, cellStyle.cell)}
style={{ color: colors.textColor ?? 'inherit' }}
/>
);
};
const getGridStyles = (
theme: GrafanaTheme2,
{ enablePagination, noHeader }: { enablePagination?: boolean; noHeader?: boolean }
@@ -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;
}
/**