core-components: proper LogViewer selection and copy
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -14,7 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
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';
|
||||
@@ -23,6 +25,7 @@ import clsx from 'clsx';
|
||||
import { LogLine } from './LogLine';
|
||||
import { LogViewerControls } from './LogViewerControls';
|
||||
import { useLogViewerSearch } from './useLogViewerSearch';
|
||||
import { useLogViewerSelection } from './useLogViewerSelection';
|
||||
|
||||
export interface LogViewerProps {
|
||||
text: string;
|
||||
@@ -31,13 +34,13 @@ export interface LogViewerProps {
|
||||
export function LogViewer(props: LogViewerProps) {
|
||||
const classes = useStyles();
|
||||
const listRef = useRef<FixedSizeList | null>(null);
|
||||
const [selectedLine, setSelectedLine] = useState<number>();
|
||||
|
||||
// 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) {
|
||||
@@ -45,6 +48,14 @@ export function LogViewer(props: LogViewerProps) {
|
||||
}
|
||||
}, [search.resultLine]);
|
||||
|
||||
const handleSelectLine = (
|
||||
line: number,
|
||||
event: { shiftKey: boolean; preventDefault: () => void },
|
||||
) => {
|
||||
event.preventDefault();
|
||||
selection.setSelection(line, event.shiftKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
@@ -68,16 +79,25 @@ export function LogViewer(props: LogViewerProps) {
|
||||
<div
|
||||
style={{ ...style }}
|
||||
className={clsx(classes.line, {
|
||||
[classes.lineSelected]: selectedLine === lineNumber,
|
||||
[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={() => setSelectedLine(lineNumber)}
|
||||
onKeyPress={() => setSelectedLine(lineNumber)}
|
||||
onClick={event => handleSelectLine(lineNumber, event)}
|
||||
onKeyPress={event => handleSelectLine(lineNumber, event)}
|
||||
>
|
||||
{lineNumber}
|
||||
</a>
|
||||
|
||||
@@ -34,6 +34,7 @@ export const useStyles = makeStyles(theme => ({
|
||||
fontSize: theme.typography.fontSize,
|
||||
},
|
||||
line: {
|
||||
position: 'relative',
|
||||
whiteSpace: 'pre',
|
||||
|
||||
'&:hover': {
|
||||
@@ -47,6 +48,11 @@ export const useStyles = makeStyles(theme => ({
|
||||
background: theme.palette.action.selected,
|
||||
},
|
||||
},
|
||||
lineCopyButton: {
|
||||
position: 'absolute',
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
lineNumber: {
|
||||
display: 'inline-block',
|
||||
textAlign: 'end',
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 { errorApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { AnsiLine } from './AnsiProcessor';
|
||||
|
||||
export function useLogViewerSelection(lines: AnsiLine[]) {
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const [sel, setSelection] = useState<{ start: number; end: number }>();
|
||||
const start = sel ? Math.min(sel.start, sel.end) : undefined;
|
||||
const end = sel ? Math.max(sel.start, sel.end) : undefined;
|
||||
|
||||
const [{ error }, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
errorApi.post(error);
|
||||
}
|
||||
}, [error, errorApi]);
|
||||
|
||||
return {
|
||||
shouldShowButton(line: number) {
|
||||
return start === line || end === line;
|
||||
},
|
||||
isSelected(line: number) {
|
||||
if (!sel) {
|
||||
return false;
|
||||
}
|
||||
return start! <= line && line <= end!;
|
||||
},
|
||||
setSelection(line: number, add: boolean) {
|
||||
if (add) {
|
||||
setSelection(s =>
|
||||
s ? { start: s.start, end: line } : { start: line, end: line },
|
||||
);
|
||||
} else {
|
||||
setSelection(s =>
|
||||
s?.start === line && s?.end === line
|
||||
? undefined
|
||||
: { start: line, end: line },
|
||||
);
|
||||
}
|
||||
},
|
||||
copySelection() {
|
||||
if (sel) {
|
||||
const copyText = lines
|
||||
.slice(sel.start - 1, sel.end)
|
||||
.map(l => l.text)
|
||||
.join('\n');
|
||||
copyToClipboard(copyText);
|
||||
setSelection(undefined);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user