core-components: document LogViewer + restructure
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
import { LogViewerProps } from './LogViewer';
|
||||
|
||||
const LogViewer = lazy(() =>
|
||||
import('./LogViewer').then(m => ({ default: m.LogViewer })),
|
||||
);
|
||||
|
||||
export function LazyLogViewer(props: LogViewerProps) {
|
||||
const { Progress } = useApp().getComponents();
|
||||
return (
|
||||
<Suspense fallback={<Progress />}>
|
||||
<LogViewer {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -14,12 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { LogViewer } from './LogViewer';
|
||||
|
||||
export default {
|
||||
title: 'Data Display/LogViewer',
|
||||
component: LogViewer,
|
||||
decorators: [(Story: ComponentType<{}>) => wrapInTestApp(<Story />)],
|
||||
};
|
||||
|
||||
const exampleLog = `Starting up task with 3 steps
|
||||
|
||||
@@ -14,113 +14,45 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import CopyIcon from '@material-ui/icons/FileCopy';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { AnsiProcessor } from './AnsiProcessor';
|
||||
import { HEADER_SIZE, useStyles } from './styles';
|
||||
import clsx from 'clsx';
|
||||
import { LogLine } from './LogLine';
|
||||
import { LogViewerControls } from './LogViewerControls';
|
||||
import { useLogViewerSearch } from './useLogViewerSearch';
|
||||
import { useLogViewerSelection } from './useLogViewerSelection';
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
|
||||
const RealLogViewer = lazy(() =>
|
||||
import('./RealLogViewer').then(m => ({ default: m.RealLogViewer })),
|
||||
);
|
||||
|
||||
/**
|
||||
* The properties for the LogViewer component.
|
||||
*/
|
||||
export interface LogViewerProps {
|
||||
/**
|
||||
* The text of the logs to display.
|
||||
*
|
||||
* The LogViewer component is optimized for appending content at the end of the text.
|
||||
*/
|
||||
text: string;
|
||||
/**
|
||||
* The className to apply to the root LogViewer element inside the auto sizer.
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that displays logs in a scrollable text area.
|
||||
*
|
||||
* The LogViewer has support for search and filtering, as well as displaying
|
||||
* text content with ANSI color escape codes.
|
||||
*
|
||||
* Since the LogViewer uses windowing to avoid rendering all contents at once, the
|
||||
* log is sized automatically to fill the available vertical space. This means
|
||||
* it may often be needed to wrap the LogViewer in a container that provides it
|
||||
* with a fixed amount of space.
|
||||
*/
|
||||
export function LogViewer(props: LogViewerProps) {
|
||||
const classes = useStyles();
|
||||
const listRef = useRef<FixedSizeList | null>(null);
|
||||
|
||||
// The processor keeps state that optimizes appending to the text
|
||||
const processor = useMemo(() => new AnsiProcessor(), []);
|
||||
const lines = processor.process(props.text);
|
||||
|
||||
const search = useLogViewerSearch(lines);
|
||||
const selection = useLogViewerSelection(lines);
|
||||
|
||||
useEffect(() => {
|
||||
if (search.resultLine !== undefined && listRef.current) {
|
||||
listRef.current.scrollToItem(search.resultLine - 1, 'center');
|
||||
}
|
||||
}, [search.resultLine]);
|
||||
|
||||
const handleSelectLine = (
|
||||
line: number,
|
||||
event: { shiftKey: boolean; preventDefault: () => void },
|
||||
) => {
|
||||
event.preventDefault();
|
||||
selection.setSelection(line, event.shiftKey);
|
||||
};
|
||||
|
||||
const { Progress } = useApp().getComponents();
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<div
|
||||
style={{ width, height }}
|
||||
className={clsx(classes.root, props.className)}
|
||||
>
|
||||
<div className={classes.header}>
|
||||
<LogViewerControls {...search} />
|
||||
</div>
|
||||
<FixedSizeList
|
||||
ref={listRef}
|
||||
className={classes.log}
|
||||
height={height - HEADER_SIZE}
|
||||
width={width}
|
||||
itemData={search.lines}
|
||||
itemSize={20}
|
||||
itemCount={search.lines.length}
|
||||
>
|
||||
{({ index, style, data }) => {
|
||||
const line = data[index];
|
||||
const { lineNumber } = line;
|
||||
return (
|
||||
<div
|
||||
style={{ ...style }}
|
||||
className={clsx(classes.line, {
|
||||
[classes.lineSelected]: selection.isSelected(lineNumber),
|
||||
})}
|
||||
>
|
||||
{selection.shouldShowButton(lineNumber) && (
|
||||
<IconButton
|
||||
size="small"
|
||||
className={classes.lineCopyButton}
|
||||
onClick={() => selection.copySelection()}
|
||||
>
|
||||
<CopyIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
)}
|
||||
<a
|
||||
role="row"
|
||||
target="_self"
|
||||
href={`#line-${lineNumber}`}
|
||||
className={classes.lineNumber}
|
||||
onClick={event => handleSelectLine(lineNumber, event)}
|
||||
onKeyPress={event => handleSelectLine(lineNumber, event)}
|
||||
>
|
||||
{lineNumber}
|
||||
</a>
|
||||
<LogLine
|
||||
line={line}
|
||||
classes={classes}
|
||||
searchText={search.searchText}
|
||||
highlightResultIndex={
|
||||
search.resultLine === lineNumber
|
||||
? search.resultLineIndex
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</FixedSizeList>
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<Suspense fallback={<Progress />}>
|
||||
<RealLogViewer {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import CopyIcon from '@material-ui/icons/FileCopy';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { AnsiProcessor } from './AnsiProcessor';
|
||||
import { HEADER_SIZE, useStyles } from './styles';
|
||||
import clsx from 'clsx';
|
||||
import { LogLine } from './LogLine';
|
||||
import { LogViewerControls } from './LogViewerControls';
|
||||
import { useLogViewerSearch } from './useLogViewerSearch';
|
||||
import { useLogViewerSelection } from './useLogViewerSelection';
|
||||
|
||||
export interface RealLogViewerProps {
|
||||
text: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function RealLogViewer(props: RealLogViewerProps) {
|
||||
const classes = useStyles();
|
||||
const listRef = useRef<FixedSizeList | null>(null);
|
||||
|
||||
// The processor keeps state that optimizes appending to the text
|
||||
const processor = useMemo(() => new AnsiProcessor(), []);
|
||||
const lines = processor.process(props.text);
|
||||
|
||||
const search = useLogViewerSearch(lines);
|
||||
const selection = useLogViewerSelection(lines);
|
||||
|
||||
useEffect(() => {
|
||||
if (search.resultLine !== undefined && listRef.current) {
|
||||
listRef.current.scrollToItem(search.resultLine - 1, 'center');
|
||||
}
|
||||
}, [search.resultLine]);
|
||||
|
||||
const handleSelectLine = (
|
||||
line: number,
|
||||
event: { shiftKey: boolean; preventDefault: () => void },
|
||||
) => {
|
||||
event.preventDefault();
|
||||
selection.setSelection(line, event.shiftKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<div
|
||||
style={{ width, height }}
|
||||
className={clsx(classes.root, props.className)}
|
||||
>
|
||||
<div className={classes.header}>
|
||||
<LogViewerControls {...search} />
|
||||
</div>
|
||||
<FixedSizeList
|
||||
ref={listRef}
|
||||
className={classes.log}
|
||||
height={height - HEADER_SIZE}
|
||||
width={width}
|
||||
itemData={search.lines}
|
||||
itemSize={20}
|
||||
itemCount={search.lines.length}
|
||||
>
|
||||
{({ index, style, data }) => {
|
||||
const line = data[index];
|
||||
const { lineNumber } = line;
|
||||
return (
|
||||
<div
|
||||
style={{ ...style }}
|
||||
className={clsx(classes.line, {
|
||||
[classes.lineSelected]: selection.isSelected(lineNumber),
|
||||
})}
|
||||
>
|
||||
{selection.shouldShowButton(lineNumber) && (
|
||||
<IconButton
|
||||
size="small"
|
||||
className={classes.lineCopyButton}
|
||||
onClick={() => selection.copySelection()}
|
||||
>
|
||||
<CopyIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
)}
|
||||
<a
|
||||
role="row"
|
||||
target="_self"
|
||||
href={`#line-${lineNumber}`}
|
||||
className={classes.lineNumber}
|
||||
onClick={event => handleSelectLine(lineNumber, event)}
|
||||
onKeyPress={event => handleSelectLine(lineNumber, event)}
|
||||
>
|
||||
{lineNumber}
|
||||
</a>
|
||||
<LogLine
|
||||
line={line}
|
||||
classes={classes}
|
||||
searchText={search.searchText}
|
||||
highlightResultIndex={
|
||||
search.resultLine === lineNumber
|
||||
? search.resultLineIndex
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</FixedSizeList>
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
);
|
||||
}
|
||||
@@ -14,5 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { LazyLogViewer as LogViewer } from './LazyLogViewer';
|
||||
export { LogViewer } from './LogViewer';
|
||||
export type { LogViewerProps } from './LogViewer';
|
||||
|
||||
Reference in New Issue
Block a user