core-components: proper LogViewer selection and copy

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-12-05 19:45:10 +01:00
committed by blam
parent cb3246ef89
commit d06c6a4cfe
3 changed files with 101 additions and 5 deletions
@@ -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);
}
},
};
}