From 992d189c0fb3f8adfa93ea47d6aeb361b41c53e7 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Fri, 8 Aug 2025 10:55:53 -0400 Subject: [PATCH] Table: Frozen columns (#109276) * Table: Frozen columns * i18n extract * clamp the frozen columns by the total number of cols visible * future-proof a bit: frozenColumns.left, add to kitchenSink --- .../panel-table/table_kitchen_sink.json | 5 ++++- .../src/components/Table/TableNG/TableNG.tsx | 19 ++++++++++++++++++- .../src/components/Table/TableNG/styles.ts | 14 +++++++++++--- .../src/components/Table/TableNG/types.ts | 1 + .../panel/table/table-new/TablePanel.tsx | 1 + .../plugins/panel/table/table-new/module.tsx | 9 +++++++++ .../panel/table/table-new/panelcfg.cue | 4 ++++ .../panel/table/table-new/panelcfg.gen.ts | 6 ++++++ public/locales/en-US/grafana.json | 2 ++ 9 files changed, 56 insertions(+), 5 deletions(-) diff --git a/devenv/dev-dashboards/panel-table/table_kitchen_sink.json b/devenv/dev-dashboards/panel-table/table_kitchen_sink.json index 77ba090a0df..257893982e0 100644 --- a/devenv/dev-dashboards/panel-table/table_kitchen_sink.json +++ b/devenv/dev-dashboards/panel-table/table_kitchen_sink.json @@ -410,6 +410,9 @@ "id": 1, "options": { "cellHeight": "sm", + "frozenColumns": { + "left": 1 + }, "footer": { "countRows": false, "enablePagination": false, @@ -1670,5 +1673,5 @@ "timezone": "", "title": "Panel Tests - Table - Kitchen Sink", "uid": "dcb9f5e9-8066-4397-889e-864b99555dbb", - "version": 7 + "version": 8 } diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index 4adbf7541c9..a3e532f7cad 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -87,6 +87,7 @@ export function TableNG(props: TableNGProps) { enableSharedCrosshair = false, enableVirtualization, footerOptions, + frozenColumns = 0, getActions = () => [], height, initialSortBy, @@ -184,6 +185,19 @@ export function TableNG(props: TableNGProps) { [theme] ); const widths = useMemo(() => computeColWidths(visibleFields, availableWidth), [visibleFields, availableWidth]); + const numColsFullyInView = useMemo( + () => + widths.reduce( + ([count, remainingWidth], nextWidth) => { + if (remainingWidth - nextWidth >= 0) { + return [count + 1, remainingWidth - nextWidth]; + } + return [count, 0]; + }, + [0, availableWidth] + )[0], + [widths, availableWidth] + ); const headerHeight = useHeaderHeight({ columnWidths: widths, fields: visibleFields, @@ -476,6 +490,7 @@ export function TableNG(props: TableNGProps) { name: displayName, width, headerCellClass, + frozen: Math.min(frozenColumns, numColsFullyInView) > i, renderCell: renderCellContent, renderHeaderCell: ({ column, sortDirection }) => ( :not(.rdg-summary-row, .rdg-header-row) > .rdg-cell': { '&:hover, &[aria-selected=true]': { boxShadow: theme.shadows.z2 }, // selected cells should appear below hovered cells. - '&:hover': { zIndex: theme.zIndex.tooltip - 2 }, - '&[aria-selected=true]': { zIndex: theme.zIndex.tooltip - 3 }, + '&:hover': { zIndex: theme.zIndex.tooltip - 4 }, + '&[aria-selected=true]': { zIndex: theme.zIndex.tooltip - 5 }, }, + '.rdg-cell.rdg-cell-frozen': { zIndex: theme.zIndex.tooltip - 2 }, + '.rdg-header-row, .rdg-summary-row': { - '.rdg-cell': { zIndex: theme.zIndex.tooltip - 1 }, + '.rdg-cell': { + zIndex: theme.zIndex.tooltip - 3, + + '&.rdg-cell-frozen': { + zIndex: theme.zIndex.tooltip - 1, + }, + }, }, }), gridNested: css({ diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts index 105e900189c..8a2b25bc986 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/types.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts @@ -129,6 +129,7 @@ export interface BaseTableProps { onCellFilterAdded?: TableFilterActionCallback; footerOptions?: TableFooterCalc; footerValues?: FooterItem[]; + frozenColumns?: number; enablePagination?: boolean; cellHeight?: TableCellHeight; structureRev?: number; diff --git a/public/app/plugins/panel/table/table-new/TablePanel.tsx b/public/app/plugins/panel/table/table-new/TablePanel.tsx index 6859e6368fb..18bce856a4a 100644 --- a/public/app/plugins/panel/table/table-new/TablePanel.tsx +++ b/public/app/plugins/panel/table/table-new/TablePanel.tsx @@ -77,6 +77,7 @@ export function TablePanel(props: Props) { onColumnResize={(displayName, resizedWidth) => onColumnResize(displayName, resizedWidth, props)} onCellFilterAdded={panelContext.onAddAdHocFilter} footerOptions={options.footer} + frozenColumns={options.frozenColumns?.left} enablePagination={options.footer?.enablePagination} cellHeight={options.cellHeight} timeRange={timeRange} diff --git a/public/app/plugins/panel/table/table-new/module.tsx b/public/app/plugins/panel/table/table-new/module.tsx index a40d4c0b047..bde9b5c7c40 100644 --- a/public/app/plugins/panel/table/table-new/module.tsx +++ b/public/app/plugins/panel/table/table-new/module.tsx @@ -128,6 +128,15 @@ export const plugin = new PanelPlugin(TablePanel) category, defaultValue: defaultOptions.showHeader, }) + .addNumberInput({ + path: 'frozenColumns.left', + name: t('table-new.name-frozen-columns', 'Frozen columns'), + description: t('table-new.description-frozen-columns', 'Columns are frozen from the left side of the table'), + settings: { + placeholder: 'none', + }, + category, + }) .addRadio({ path: 'cellHeight', name: t('table-new.name-cell-height', 'Cell height'), diff --git a/public/app/plugins/panel/table/table-new/panelcfg.cue b/public/app/plugins/panel/table/table-new/panelcfg.cue index 214e854f4a4..1bfbd053cf2 100644 --- a/public/app/plugins/panel/table/table-new/panelcfg.cue +++ b/public/app/plugins/panel/table/table-new/panelcfg.cue @@ -44,6 +44,10 @@ composableKinds: PanelCfg: { } // Controls the height of the rows cellHeight?: ui.TableCellHeight & (*"sm" | _) + // Defines the number of columns to freeze on the left side of the table + frozenColumns?: { + left?: number | *0 + } } @cuetsy(kind="interface") FieldConfig: { ui.TableFieldOptions diff --git a/public/app/plugins/panel/table/table-new/panelcfg.gen.ts b/public/app/plugins/panel/table/table-new/panelcfg.gen.ts index 5b771ff2640..bed8165671a 100644 --- a/public/app/plugins/panel/table/table-new/panelcfg.gen.ts +++ b/public/app/plugins/panel/table/table-new/panelcfg.gen.ts @@ -23,6 +23,12 @@ export interface Options { * Represents the index of the selected frame */ frameIndex: number; + /** + * number of columns on the left side of the table that should be frozen + */ + frozenColumns?: { + left?: number; + }; /** * Controls whether the panel should show the header */ diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 34429ae992c..5f45a3bab72 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -12717,6 +12717,7 @@ "description-column-filter": "Enables/disables field filters in table", "description-count-rows": "Display a single count for all data rows", "description-fields": "Select the fields that should be calculated", + "description-frozen-columns": "Columns are frozen from the left side of the table", "description-min-column-width": "The minimum width for column auto resizing", "name-calculation": "Calculation", "name-cell-height": "Cell height", @@ -12728,6 +12729,7 @@ "name-count-rows": "Count rows", "name-enable-pagination": "Enable pagination", "name-fields": "Fields", + "name-frozen-columns": "Frozen columns", "name-hide-in-table": "Hide in table", "name-min-column-width": "Minimum column width", "name-show-table-footer": "Show table footer",