diff --git a/.changeset/cool-cities-grab.md b/.changeset/cool-cities-grab.md new file mode 100644 index 0000000000..372ca9887f --- /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 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 diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index 4ec7e21d2f..8b11d7b9de 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,20 @@ 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) { + setRowHeight(line.lineNumber, lineRef.current.offsetHeight); + } + }, [line.lineNumber, setRowHeight]); + const elements = useMemo( () => chunks.map(({ text, modifiers, highlight }, index) => ( @@ -184,13 +193,14 @@ export function LogLine({ (highlight === highlightResultIndex ? classes.textSelectedHighlight : classes.textHighlight), + { [classes.textWrap]: !!setRowHeight }, )} > {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..e384a8e882 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,103 @@ export function RealLogViewer(props: RealLogViewerProps) { selection.setSelection(line, event.shiftKey); }; + function setRowHeight(index: number, size: number) { + if (shouldTextWrap && listInstance) { + (listInstance as VariableSizeList).resetAfterIndex(0); + // lineNumber is 1-based but index is 0-based + heights.current[index - 1] = 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..76e045ca32 100644 --- a/packages/core-components/src/components/LogViewer/styles.ts +++ b/packages/core-components/src/components/LogViewer/styles.ts @@ -66,10 +66,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, @@ -93,6 +96,7 @@ export const useStyles = makeStyles( width: 60, marginRight: theme.spacing(1), cursor: 'pointer', + flexShrink: 0, }, textHighlight: { background: alpha(theme.palette.info.main, 0.15), @@ -163,6 +167,10 @@ export const useStyles = makeStyles( modifierBackgroundGrey: { background: colors.grey[500], }, + textWrap: { + whiteSpace: 'pre-wrap', + wordBreak: 'break-all', + }, }), { name: 'BackstageLogViewer' }, );