Skip to content

Commit d7ee3ea

Browse files
Table: Fix text wrapping applying to wrong field (#93707)
* Fix text wrap --------- Co-authored-by: Ihor Yeromin <[email protected]>
1 parent 37c54b4 commit d7ee3ea

File tree

4 files changed

+67
-41
lines changed

4 files changed

+67
-41
lines changed

packages/grafana-ui/src/components/Table/RowsList.tsx

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ interface RowsListProps {
5353
initialRowIndex?: number;
5454
headerGroups: HeaderGroup[];
5555
longestField?: Field;
56+
textWrapField?: Field;
5657
}
5758

5859
export const RowsList = (props: RowsListProps) => {
@@ -78,6 +79,7 @@ export const RowsList = (props: RowsListProps) => {
7879
initialRowIndex = undefined,
7980
headerGroups,
8081
longestField,
82+
textWrapField,
8183
} = props;
8284

8385
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex);
@@ -232,7 +234,7 @@ export const RowsList = (props: RowsListProps) => {
232234
);
233235

234236
let rowBg: Function | undefined = undefined;
235-
let textWrapField: Field | undefined = undefined;
237+
let textWrapFinal: Field | undefined;
236238
for (const field of data.fields) {
237239
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
238240
const fieldOptions = field.config.custom as TableFieldOptions;
@@ -250,16 +252,10 @@ export const RowsList = (props: RowsListProps) => {
250252
};
251253
}
252254

253-
if (
254-
cellOptionsExist &&
255-
(fieldOptions.cellOptions.type === TableCellDisplayMode.Auto ||
256-
fieldOptions.cellOptions.type === TableCellDisplayMode.ColorBackground ||
257-
fieldOptions.cellOptions.type === TableCellDisplayMode.ColorText) &&
258-
fieldOptions.cellOptions.wrapText
259-
) {
260-
textWrapField = field;
255+
if (textWrapField !== undefined) {
256+
textWrapFinal = textWrapField;
261257
} else if (longestField !== undefined) {
262-
textWrapField = longestField;
258+
textWrapFinal = longestField;
263259
}
264260
}
265261

@@ -288,16 +284,17 @@ export const RowsList = (props: RowsListProps) => {
288284
}
289285

290286
// If there's a text wrapping field we set the height of it here
291-
if (textWrapField) {
287+
if (textWrapFinal) {
292288
const visibleFields = data.fields.filter((field) => !Boolean(field.config.custom?.hidden));
293-
const seriesIndex = visibleFields.findIndex((field) => field.name === textWrapField.name);
289+
const seriesIndex = visibleFields.findIndex((field) => field.name === textWrapFinal.name);
294290
const pxLineHeight = theme.typography.body.lineHeight * theme.typography.fontSize;
295291
const bbox = guessTextBoundingBox(
296-
textWrapField.values[index],
292+
textWrapFinal.values[row.index],
297293
headerGroups[0].headers[seriesIndex],
298294
osContext,
299295
pxLineHeight,
300-
tableStyles.rowHeight
296+
tableStyles.rowHeight,
297+
tableStyles.cellPadding
301298
);
302299
style.height = bbox.height;
303300
}
@@ -335,7 +332,7 @@ export const RowsList = (props: RowsListProps) => {
335332
frame={data}
336333
rowStyled={rowBg !== undefined}
337334
rowExpanded={rowExpanded}
338-
textWrapped={textWrapField !== undefined}
335+
textWrapped={textWrapFinal !== undefined}
339336
height={Number(style.height)}
340337
/>
341338
))}
@@ -354,7 +351,7 @@ export const RowsList = (props: RowsListProps) => {
354351
rows,
355352
tableState.expanded,
356353
tableStyles,
357-
textWrapField,
354+
textWrapFinal,
358355
theme.components.table.rowSelected,
359356
theme.typography.fontSize,
360357
theme.typography.body.lineHeight,
@@ -374,16 +371,17 @@ export const RowsList = (props: RowsListProps) => {
374371
return getExpandedRowHeight(nestedDataField, row.index, tableStyles);
375372
}
376373

377-
if (textWrapField) {
374+
if (textWrapFinal) {
378375
const visibleFields = data.fields.filter((field) => !Boolean(field.config.custom?.hidden));
379-
const seriesIndex = visibleFields.findIndex((field) => field.name === textWrapField.name);
376+
const seriesIndex = visibleFields.findIndex((field) => field.name === textWrapFinal.name);
380377
const pxLineHeight = theme.typography.fontSize * theme.typography.body.lineHeight;
381378
return guessTextBoundingBox(
382-
textWrapField.values[index],
379+
textWrapFinal.values[row.index],
383380
headerGroups[0].headers[seriesIndex],
384381
osContext,
385382
pxLineHeight,
386-
tableStyles.rowHeight
383+
tableStyles.rowHeight,
384+
tableStyles.cellPadding
387385
).height;
388386
}
389387

@@ -401,22 +399,29 @@ export const RowsList = (props: RowsListProps) => {
401399
// Key the virtualizer for expanded rows
402400
const expandedKey = Object.keys(tableState.expanded).join('|');
403401

402+
// It's a hack for text wrapping.
403+
// VariableSizeList component didn't know that we manually set row height.
404+
// So we need to reset the list when the rows high changes.
405+
useEffect(() => {
406+
if (listRef.current) {
407+
listRef.current.resetAfterIndex(0);
408+
}
409+
}, [rows, listRef]);
410+
404411
return (
405-
<>
406-
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true} scrollTop={scrollTop}>
407-
<VariableSizeList
408-
// This component needs an unmount/remount when row height, page changes, or expanded rows change
409-
key={`${rowHeight}${pageIndex}${expandedKey}`}
410-
height={listHeight}
411-
itemCount={itemCount}
412-
itemSize={getItemSize}
413-
width={'100%'}
414-
ref={listRef}
415-
style={{ overflow: undefined }}
416-
>
417-
{({ index, style }) => RenderRow({ index, style, rowHighlightIndex })}
418-
</VariableSizeList>
419-
</CustomScrollbar>
420-
</>
412+
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true} scrollTop={scrollTop}>
413+
<VariableSizeList
414+
// This component needs an unmount/remount when row height, page changes, or expanded rows change
415+
key={`${rowHeight}${pageIndex}${expandedKey}`}
416+
height={listHeight}
417+
itemCount={itemCount}
418+
itemSize={getItemSize}
419+
width={'100%'}
420+
ref={listRef}
421+
style={{ overflow: undefined }}
422+
>
423+
{({ index, style }) => RenderRow({ index, style, rowHighlightIndex })}
424+
</VariableSizeList>
425+
</CustomScrollbar>
421426
);
422427
};

packages/grafana-ui/src/components/Table/Table.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from 'react-table';
1111
import { VariableSizeList } from 'react-window';
1212

13-
import { FieldType, ReducerID, getRowUniqueId } from '@grafana/data';
13+
import { FieldType, ReducerID, getRowUniqueId, getFieldMatcher } from '@grafana/data';
1414
import { selectors } from '@grafana/e2e-selectors';
1515
import { TableCellHeight } from '@grafana/schema';
1616

@@ -297,7 +297,24 @@ export const Table = memo((props: Props) => {
297297
}
298298

299299
// Try to determine the longet field
300+
// TODO: do we wrap only one field?
301+
// What if there are multiple fields with long text?
300302
const longestField = guessLongestField(fieldConfig, data);
303+
let textWrapField = undefined;
304+
if (fieldConfig !== undefined) {
305+
data.fields.forEach((field) => {
306+
fieldConfig.overrides.forEach((override) => {
307+
const matcher = getFieldMatcher(override.matcher);
308+
if (matcher(field, data, [data])) {
309+
for (const property of override.properties) {
310+
if (property.id === 'custom.cellOptions' && property.value.wrapText) {
311+
textWrapField = field;
312+
}
313+
}
314+
}
315+
});
316+
});
317+
}
301318

302319
return (
303320
<div
@@ -337,6 +354,7 @@ export const Table = memo((props: Props) => {
337354
enableSharedCrosshair={enableSharedCrosshair}
338355
initialRowIndex={initialRowIndex}
339356
longestField={longestField}
357+
textWrapField={textWrapField}
340358
/>
341359
</div>
342360
) : (

packages/grafana-ui/src/components/Table/utils.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ function getWrappableData(numRecords: number) {
7777
// this case so we simply leave it as zero
7878
for (let i = 0; i < numRecords; i++) {
7979
data.fields[0].values[i] = 0;
80-
data.fields[1].values[i] = faker.lorem.paragraphs(9);
80+
data.fields[1].values[i] = faker.lorem.paragraphs(6);
8181
data.fields[2].values[i] = faker.lorem.paragraphs(11);
8282
}
8383

@@ -546,7 +546,7 @@ describe('Table utils', () => {
546546

547547
describe('guessLongestField', () => {
548548
// FLAKY TEST - https://drone.grafana.net/grafana/grafana/201232/1/5
549-
it.skip('should guess the longest field correct if there are few records', () => {
549+
it.skip('should guess the longest field correctly if there are few records', () => {
550550
const data = getWrappableData(10);
551551
const config = {
552552
defaults: {

packages/grafana-ui/src/components/Table/utils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -644,11 +644,13 @@ export function guessTextBoundingBox(
644644
headerGroup: HeaderGroup,
645645
osContext: OffscreenCanvasRenderingContext2D | null,
646646
lineHeight: number,
647-
defaultRowHeight: number
647+
defaultRowHeight: number,
648+
padding = 0
648649
) {
649650
const width = Number(headerGroup?.width ?? 300);
650651
const LINE_SCALE_FACTOR = 1.17;
651652
const LOW_LINE_PAD = 42;
653+
const PADDING = padding * 2;
652654

653655
if (osContext !== null && typeof text === 'string') {
654656
const words = text.split(/\s/);
@@ -662,7 +664,7 @@ export function guessTextBoundingBox(
662664
const currentWord = words[i];
663665
let lineWidth = osContext.measureText(currentLine + ' ' + currentWord).width;
664666

665-
if (lineWidth < width) {
667+
if (lineWidth < width - PADDING) {
666668
currentLine += ' ' + currentWord;
667669
wordCount++;
668670
} else {
@@ -695,6 +697,7 @@ export function guessTextBoundingBox(
695697
} else {
696698
height = lineNumber * lineHeight + LOW_LINE_PAD;
697699
}
700+
height += PADDING;
698701

699702
return { width, height };
700703
}

0 commit comments

Comments
 (0)