From 12eff729b90ba77981740d531e878668089c6393 Mon Sep 17 00:00:00 2001 From: Ben Vu Date: Wed, 23 Apr 2025 23:43:58 -0400 Subject: [PATCH 1/8] feat(logviewer): add textwrapping support for long lines Signed-off-by: Ben Vu --- .../src/components/LogViewer/LogLine.tsx | 17 +- .../LogViewer/LogViewer.stories.tsx | 6 + .../src/components/LogViewer/LogViewer.tsx | 4 + .../components/LogViewer/RealLogViewer.tsx | 181 +++++++++++------- .../src/components/LogViewer/styles.ts | 7 +- 5 files changed, 138 insertions(+), 77 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index 4ec7e21d2f..637fe0f922 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useMemo } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { AnsiChunk, AnsiLine, ChunkModifiers } from './AnsiProcessor'; import startCase from 'lodash/startCase'; import classnames from 'classnames'; @@ -159,6 +159,7 @@ export interface LogLineProps { classes: ReturnType; searchText: string; highlightResultIndex?: number; + setRowHeight?: (index: number, size: number) => void; } export function LogLine({ @@ -166,12 +167,21 @@ export function LogLine({ classes, searchText, highlightResultIndex, + setRowHeight, }: LogLineProps) { + const lineRef = useRef(null); const chunks = useMemo( () => calculateHighlightedChunks(line, searchText), [line, searchText], ); + useEffect(() => { + if (lineRef.current && setRowHeight) { + // lineNumber is 1-based but height index is 0-based + setRowHeight(line.lineNumber - 1, lineRef.current.offsetHeight + 5); + } + }, [line.lineNumber, setRowHeight]); + const elements = useMemo( () => chunks.map(({ text, modifiers, highlight }, index) => ( @@ -184,13 +194,14 @@ export function LogLine({ (highlight === highlightResultIndex ? classes.textSelectedHighlight : classes.textHighlight), + !!setRowHeight && classes.modifierTextWrap, )} > {text} )), - [chunks, highlightResultIndex, classes], + [chunks, highlightResultIndex, classes, setRowHeight], ); - return <>{elements}; + return {elements}; } diff --git a/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx index 86a2b2ebcd..984477a6f9 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx @@ -83,3 +83,9 @@ export const ExampleLogViewer = () => ( ); + +export const WithTextWrap = () => ( +
+ +
+); diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index afe20b9dc9..26ddecfcfc 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -33,6 +33,10 @@ export interface LogViewerProps { * The LogViewer component is optimized for appending content at the end of the text. */ text: string; + /** + * Determines if the overflow text should be wrapped or shown via a single line in a horizontal scrollbar. + */ + textWrap?: boolean; /** * Styling overrides for classes within the LogViewer component. */ diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx index 8430ac4bd0..1647e9a925 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -18,10 +18,10 @@ import Box from '@material-ui/core/Box'; import IconButton from '@material-ui/core/IconButton'; import CopyIcon from '@material-ui/icons/FileCopy'; import classnames from 'classnames'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useLocation } from 'react-router-dom'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { FixedSizeList } from 'react-window'; +import { VariableSizeList, FixedSizeList } from 'react-window'; import { AnsiLine, AnsiProcessor } from './AnsiProcessor'; import { LogLine } from './LogLine'; @@ -32,14 +32,17 @@ import { useLogViewerSelection } from './useLogViewerSelection'; export interface RealLogViewerProps { text: string; + textWrap?: boolean; classes?: { root?: string }; } export function RealLogViewer(props: RealLogViewerProps) { const classes = useStyles({ classes: props.classes }); - const [fixedListInstance, setFixedListInstance] = useState | null>(null); + const [listInstance, setListInstance] = useState< + VariableSizeList | FixedSizeList | null + >(null); + const shouldTextWrap = props.textWrap ?? false; + const heights = useRef<{ [key: number]: number }>({}); // The processor keeps state that optimizes appending to the text const processor = useMemo(() => new AnsiProcessor(), []); @@ -50,21 +53,21 @@ export function RealLogViewer(props: RealLogViewerProps) { const location = useLocation(); useEffect(() => { - if (fixedListInstance) { - fixedListInstance.scrollToItem(lines.length - 1, 'end'); + if (listInstance) { + listInstance.scrollToItem(lines.length - 1, 'end'); } - }, [fixedListInstance, lines]); + }, [listInstance, lines]); useEffect(() => { - if (!fixedListInstance) { + if (!listInstance) { return; } if (search.resultLine) { - fixedListInstance.scrollToItem(search.resultLine - 1, 'center'); + listInstance.scrollToItem(search.resultLine - 1, 'center'); } else { - fixedListInstance.scrollToItem(lines.length - 1, 'end'); + listInstance.scrollToItem(lines.length - 1, 'end'); } - }, [fixedListInstance, search.resultLine, lines]); + }, [listInstance, search.resultLine, lines]); useEffect(() => { if (location.hash) { @@ -81,70 +84,102 @@ export function RealLogViewer(props: RealLogViewerProps) { selection.setSelection(line, event.shiftKey); }; + function setRowHeight(index: number, size: number) { + if (shouldTextWrap && listInstance) { + (listInstance as VariableSizeList).resetAfterIndex(0); + heights.current = { ...heights.current, [index]: size }; + } + } + + function getRowHeight(index: number) { + return heights.current[index] || 20; + } + return ( - {({ height, width }: { height?: number; width?: number }) => ( - - - - - ) => { - setFixedListInstance(instance); - }} - className={classes.log} - height={(height || 480) - HEADER_SIZE} - width={width || 640} - itemData={search.lines} - itemSize={20} - itemCount={search.lines.length} - > - {({ index, style, data }) => { - const line = data[index]; - const { lineNumber } = line; - return ( - { + const commonProps = { + ref: setListInstance, + className: classes.log, + height: (height || 480) - HEADER_SIZE, + width: width || 640, + itemData: search.lines, + itemCount: search.lines.length, + }; + + const renderItem = ({ + index, + style, + data, + }: { + index: number; + style: React.CSSProperties; + data: AnsiLine[]; + }) => { + const line = data[index]; + const { lineNumber } = line; + return ( + + {selection.shouldShowButton(lineNumber) && ( + selection.copySelection()} > - {selection.shouldShowButton(lineNumber) && ( - selection.copySelection()} - > - - - )} - handleSelectLine(lineNumber, event)} - onKeyPress={event => handleSelectLine(lineNumber, event)} - > - {lineNumber} - - - - ); - }} - - - )} + + + )} + handleSelectLine(lineNumber, event)} + onKeyPress={event => handleSelectLine(lineNumber, event)} + > + {lineNumber} + + + + ); + }; + + return ( + + + + + {shouldTextWrap ? ( + + {...commonProps} + itemSize={getRowHeight} + > + {renderItem} + + ) : ( + {...commonProps} itemSize={20}> + {renderItem} + + )} + + ); + }} ); } diff --git a/packages/core-components/src/components/LogViewer/styles.ts b/packages/core-components/src/components/LogViewer/styles.ts index 2028809543..da83a8a444 100644 --- a/packages/core-components/src/components/LogViewer/styles.ts +++ b/packages/core-components/src/components/LogViewer/styles.ts @@ -50,7 +50,8 @@ export type LogViewerClassKey = | 'modifierBackgroundMagenta' | 'modifierBackgroundCyan' | 'modifierBackgroundWhite' - | 'modifierBackgroundGrey'; + | 'modifierBackgroundGrey' + | 'modifierTextWrap'; export const useStyles = makeStyles( theme => ({ @@ -163,6 +164,10 @@ export const useStyles = makeStyles( modifierBackgroundGrey: { background: colors.grey[500], }, + modifierTextWrap: { + whiteSpace: 'pre-wrap', + lineHeight: '1.7', + }, }), { name: 'BackstageLogViewer' }, ); From d28ebc90c1db990ad6d46d479d5e83fe7dce8957 Mon Sep 17 00:00:00 2001 From: Ben Vu Date: Thu, 24 Apr 2025 12:01:05 -0400 Subject: [PATCH 2/8] make line breaks and overflow cleaner Signed-off-by: Ben Vu --- .../core-components/src/components/LogViewer/LogLine.tsx | 2 +- packages/core-components/src/components/LogViewer/styles.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index 637fe0f922..4f888fc68e 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -178,7 +178,7 @@ export function LogLine({ useEffect(() => { if (lineRef.current && setRowHeight) { // lineNumber is 1-based but height index is 0-based - setRowHeight(line.lineNumber - 1, lineRef.current.offsetHeight + 5); + setRowHeight(line.lineNumber - 1, lineRef.current.offsetHeight); } }, [line.lineNumber, setRowHeight]); diff --git a/packages/core-components/src/components/LogViewer/styles.ts b/packages/core-components/src/components/LogViewer/styles.ts index da83a8a444..6511854d68 100644 --- a/packages/core-components/src/components/LogViewer/styles.ts +++ b/packages/core-components/src/components/LogViewer/styles.ts @@ -67,10 +67,13 @@ export const useStyles = makeStyles( log: { fontFamily: '"Monaco", monospace', fontSize: theme.typography.pxToRem(12), + lineHeight: '20px', }, line: { position: 'relative', whiteSpace: 'pre', + display: 'flex', + alignItems: 'flex-start', '&:hover': { background: theme.palette.action.hover, @@ -94,6 +97,7 @@ export const useStyles = makeStyles( width: 60, marginRight: theme.spacing(1), cursor: 'pointer', + flexShrink: 0, }, textHighlight: { background: alpha(theme.palette.info.main, 0.15), @@ -166,7 +170,7 @@ export const useStyles = makeStyles( }, modifierTextWrap: { whiteSpace: 'pre-wrap', - lineHeight: '1.7', + wordBreak: 'break-all', }, }), { name: 'BackstageLogViewer' }, From 5cf89b373d7993a3488464eaa480ff649611d9e3 Mon Sep 17 00:00:00 2001 From: Ben Vu Date: Thu, 24 Apr 2025 12:09:35 -0400 Subject: [PATCH 3/8] move line index logic Signed-off-by: Ben Vu --- packages/core-components/src/components/LogViewer/LogLine.tsx | 3 +-- .../core-components/src/components/LogViewer/RealLogViewer.tsx | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index 4f888fc68e..c258c27655 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -177,8 +177,7 @@ export function LogLine({ useEffect(() => { if (lineRef.current && setRowHeight) { - // lineNumber is 1-based but height index is 0-based - setRowHeight(line.lineNumber - 1, lineRef.current.offsetHeight); + setRowHeight(line.lineNumber, lineRef.current.offsetHeight); } }, [line.lineNumber, setRowHeight]); diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx index 1647e9a925..c3b9d67773 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -87,7 +87,8 @@ export function RealLogViewer(props: RealLogViewerProps) { function setRowHeight(index: number, size: number) { if (shouldTextWrap && listInstance) { (listInstance as VariableSizeList).resetAfterIndex(0); - heights.current = { ...heights.current, [index]: size }; + // lineNumber is 1-based but index is 0-based + heights.current = { ...heights.current, [index - 1]: size }; } } From b7918eca29fb6a29091635cba907ce84e15c9180 Mon Sep 17 00:00:00 2001 From: Ben Vu Date: Thu, 24 Apr 2025 15:43:25 -0400 Subject: [PATCH 4/8] fix typescript issues Signed-off-by: Ben Vu --- .../core-components/src/components/LogViewer/LogLine.tsx | 2 +- packages/core-components/src/components/LogViewer/styles.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index c258c27655..8b11d7b9de 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -193,7 +193,7 @@ export function LogLine({ (highlight === highlightResultIndex ? classes.textSelectedHighlight : classes.textHighlight), - !!setRowHeight && classes.modifierTextWrap, + { [classes.textWrap]: !!setRowHeight }, )} > {text} diff --git a/packages/core-components/src/components/LogViewer/styles.ts b/packages/core-components/src/components/LogViewer/styles.ts index 6511854d68..76e045ca32 100644 --- a/packages/core-components/src/components/LogViewer/styles.ts +++ b/packages/core-components/src/components/LogViewer/styles.ts @@ -50,8 +50,7 @@ export type LogViewerClassKey = | 'modifierBackgroundMagenta' | 'modifierBackgroundCyan' | 'modifierBackgroundWhite' - | 'modifierBackgroundGrey' - | 'modifierTextWrap'; + | 'modifierBackgroundGrey'; export const useStyles = makeStyles( theme => ({ @@ -168,7 +167,7 @@ export const useStyles = makeStyles( modifierBackgroundGrey: { background: colors.grey[500], }, - modifierTextWrap: { + textWrap: { whiteSpace: 'pre-wrap', wordBreak: 'break-all', }, From e0d102599cb1827e4270bc5b9acee83119310b8f Mon Sep 17 00:00:00 2001 From: Ben Vu Date: Thu, 24 Apr 2025 15:51:59 -0400 Subject: [PATCH 5/8] add a changeset Signed-off-by: Ben Vu --- .changeset/cool-cities-grab.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cool-cities-grab.md diff --git a/.changeset/cool-cities-grab.md b/.changeset/cool-cities-grab.md new file mode 100644 index 0000000000..e8e2e83695 --- /dev/null +++ b/.changeset/cool-cities-grab.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +LogViewer now supports a textWrap prop that wraps log lines to the next line for overflowing content instead of using horizontal scroll From 09fd59f4eb0a04574fa897948b0d9af07cb9ca84 Mon Sep 17 00:00:00 2001 From: Ben Vu Date: Thu, 24 Apr 2025 15:55:35 -0400 Subject: [PATCH 6/8] add api reports Signed-off-by: Ben Vu --- packages/core-components/report.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index 6e1057c5a3..90fa9ced22 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -750,6 +750,7 @@ export interface LogViewerProps { root?: string; }; text: string; + textWrap?: boolean; } // Warning: (ae-forgotten-export) The symbol "Props_8" needs to be exported by the entry point index.d.ts From c7e7bd7dca1d55421d5215473bf2544b1644b6fa Mon Sep 17 00:00:00 2001 From: Ben Vu <167256519+beanview@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:07:13 -0400 Subject: [PATCH 7/8] Update .changeset/cool-cities-grab.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Ben Vu <167256519+beanview@users.noreply.github.com> --- .changeset/cool-cities-grab.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/cool-cities-grab.md b/.changeset/cool-cities-grab.md index e8e2e83695..372ca9887f 100644 --- a/.changeset/cool-cities-grab.md +++ b/.changeset/cool-cities-grab.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -LogViewer now supports a textWrap prop that wraps log lines to the next line for overflowing content instead of using horizontal scroll +`LogViewer` now supports a `textWrap` prop that wraps log lines to the next line for overflowing content instead of using horizontal scroll From 0411d6d3e9806981c1b068b7235787adf26fa3de Mon Sep 17 00:00:00 2001 From: Ben Vu <167256519+beanview@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:07:30 -0400 Subject: [PATCH 8/8] Update packages/core-components/src/components/LogViewer/RealLogViewer.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Ben Vu <167256519+beanview@users.noreply.github.com> --- .../core-components/src/components/LogViewer/RealLogViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx index c3b9d67773..e384a8e882 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -88,7 +88,7 @@ export function RealLogViewer(props: RealLogViewerProps) { if (shouldTextWrap && listInstance) { (listInstance as VariableSizeList).resetAfterIndex(0); // lineNumber is 1-based but index is 0-based - heights.current = { ...heights.current, [index - 1]: size }; + heights.current[index - 1] = size; } }