From f91efcfe2c8f6e6078d64118e3fd1332f9c12873 Mon Sep 17 00:00:00 2001 From: Jesse David Peterson Date: Fri, 19 Dec 2025 13:12:01 -0500 Subject: [PATCH] TimeSeries: Fix truncated label text in legend table mode (#115647) * fix(legend-table): remove arbitrary 600px max width for full width cells * test(legend-table): backfill test coverage for viz legend table * test(legend-table): backfill test coverage for viz legend table item * refactor(legend-table): use derived theme spacing, not hard-coded values --- .../VizLegend/VizLegendTable.test.tsx | 78 ++++++++++++ .../components/VizLegend/VizLegendTable.tsx | 1 - .../VizLegend/VizLegendTableItem.test.tsx | 112 ++++++++++++++++++ .../VizLegend/VizLegendTableItem.tsx | 64 ++++++---- 4 files changed, 232 insertions(+), 23 deletions(-) create mode 100644 packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx create mode 100644 packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx new file mode 100644 index 00000000000..131133bcdfb --- /dev/null +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx @@ -0,0 +1,78 @@ +import { render, screen } from '@testing-library/react'; + +import { VizLegendTable } from './VizLegendTable'; +import { VizLegendItem } from './types'; + +describe('VizLegendTable', () => { + const mockItems: VizLegendItem[] = [ + { label: 'Series 1', color: 'red', yAxis: 1 }, + { label: 'Series 2', color: 'blue', yAxis: 1 }, + { label: 'Series 3', color: 'green', yAxis: 1 }, + ]; + + it('renders without crashing', () => { + const { container } = render(); + expect(container.querySelector('table')).toBeInTheDocument(); + }); + + it('renders all items', () => { + render(); + expect(screen.getByText('Series 1')).toBeInTheDocument(); + expect(screen.getByText('Series 2')).toBeInTheDocument(); + expect(screen.getByText('Series 3')).toBeInTheDocument(); + }); + + it('renders table headers when items have display values', () => { + const itemsWithStats: VizLegendItem[] = [ + { + label: 'Series 1', + color: 'red', + yAxis: 1, + getDisplayValues: () => [ + { numeric: 100, text: '100', title: 'Max' }, + { numeric: 50, text: '50', title: 'Min' }, + ], + }, + ]; + render(); + expect(screen.getByText('Max')).toBeInTheDocument(); + expect(screen.getByText('Min')).toBeInTheDocument(); + }); + + it('renders sort icon when sorted', () => { + const { container } = render( + + ); + expect(container.querySelector('svg')).toBeInTheDocument(); + }); + + it('calls onToggleSort when header is clicked', () => { + const onToggleSort = jest.fn(); + render(); + const header = screen.getByText('Name'); + header.click(); + expect(onToggleSort).toHaveBeenCalledWith('Name'); + }); + + it('does not call onToggleSort when not sortable', () => { + const onToggleSort = jest.fn(); + render(); + const header = screen.getByText('Name'); + header.click(); + expect(onToggleSort).not.toHaveBeenCalled(); + }); + + it('renders with long labels', () => { + const itemsWithLongLabels: VizLegendItem[] = [ + { + label: 'This is a very long series name that should be scrollable within its table cell', + color: 'red', + yAxis: 1, + }, + ]; + render(); + expect( + screen.getByText('This is a very long series name that should be scrollable within its table cell') + ).toBeInTheDocument(); + }); +}); diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx index b654a2d3ac6..0c2859453eb 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx @@ -119,7 +119,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ table: css({ width: '100%', 'th:first-child': { - width: '100%', borderBottom: `1px solid ${theme.colors.border.weak}`, }, }), diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx new file mode 100644 index 00000000000..4ca95aa395c --- /dev/null +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx @@ -0,0 +1,112 @@ +import { render, screen } from '@testing-library/react'; + +import { LegendTableItem } from './VizLegendTableItem'; +import { VizLegendItem } from './types'; + +describe('LegendTableItem', () => { + const mockItem: VizLegendItem = { + label: 'Series 1', + color: 'red', + yAxis: 1, + }; + + it('renders without crashing', () => { + const { container } = render( + + + + +
+ ); + expect(container.querySelector('tr')).toBeInTheDocument(); + }); + + it('renders label text', () => { + render( + + + + +
+ ); + expect(screen.getByText('Series 1')).toBeInTheDocument(); + }); + + it('renders with long label text', () => { + const longLabelItem: VizLegendItem = { + ...mockItem, + label: 'This is a very long series name that should be scrollable in the table cell', + }; + render( + + + + +
+ ); + expect( + screen.getByText('This is a very long series name that should be scrollable in the table cell') + ).toBeInTheDocument(); + }); + + it('renders stat values when provided', () => { + const itemWithStats: VizLegendItem = { + ...mockItem, + getDisplayValues: () => [ + { numeric: 100, text: '100', title: 'Max' }, + { numeric: 50, text: '50', title: 'Min' }, + ], + }; + render( + + + + +
+ ); + expect(screen.getByText('100')).toBeInTheDocument(); + expect(screen.getByText('50')).toBeInTheDocument(); + }); + + it('renders right y-axis indicator when yAxis is 2', () => { + const rightAxisItem: VizLegendItem = { + ...mockItem, + yAxis: 2, + }; + render( + + + + +
+ ); + expect(screen.getByText('(right y-axis)')).toBeInTheDocument(); + }); + + it('calls onLabelClick when label is clicked', () => { + const onLabelClick = jest.fn(); + render( + + + + +
+ ); + const button = screen.getByRole('button'); + button.click(); + expect(onLabelClick).toHaveBeenCalledWith(mockItem, expect.any(Object)); + }); + + it('does not call onClick when readonly', () => { + const onLabelClick = jest.fn(); + render( + + + + +
+ ); + const button = screen.getByRole('button'); + expect(button).toBeDisabled(); + }); +}); diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx index 335cf4309e9..56ec6cb733e 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx @@ -69,7 +69,7 @@ export const LegendTableItem = ({ return ( - + - +
+ +
{item.getDisplayValues && @@ -128,6 +130,27 @@ const getStyles = (theme: GrafanaTheme2) => { background: rowHoverBg, }, }), + labelCell: css({ + label: 'LegendLabelCell', + maxWidth: 0, + width: '100%', + }), + labelCellInner: css({ + label: 'LegendLabelCellInner', + display: 'block', + flex: 1, + minWidth: 0, + overflowX: 'auto', + overflowY: 'hidden', + paddingRight: theme.spacing(3), + scrollbarWidth: 'none', + msOverflowStyle: 'none', + maskImage: `linear-gradient(to right, black calc(100% - ${theme.spacing(3)}), transparent 100%)`, + WebkitMaskImage: `linear-gradient(to right, black calc(100% - ${theme.spacing(3)}), transparent 100%)`, + '&::-webkit-scrollbar': { + display: 'none', + }, + }), label: css({ label: 'LegendLabel', whiteSpace: 'nowrap', @@ -135,9 +158,6 @@ const getStyles = (theme: GrafanaTheme2) => { border: 'none', fontSize: 'inherit', padding: 0, - maxWidth: '600px', - textOverflow: 'ellipsis', - overflow: 'hidden', userSelect: 'text', }), labelDisabled: css({