core-components: basic line selection and filtering for LogViewer

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-12-04 17:43:17 +01:00
committed by blam
parent 8af6baa2d1
commit 738a0b1b77
3 changed files with 116 additions and 39 deletions
@@ -35,6 +35,7 @@ describe('AnsiProcessor', () => {
modifiers: {},
},
],
text: 'foobarbaz',
lineNumber: 1,
},
]);
@@ -55,6 +56,7 @@ describe('AnsiProcessor', () => {
modifiers: {},
},
],
text: 'foo bar: baz',
lineNumber: 1,
},
]);
@@ -68,7 +70,7 @@ a\x1b[34mb\x1b[39mc
x\x1b[44my\x1b[49mz
`),
).toEqual([
{ chunks: [{ text: '', modifiers: {} }], lineNumber: 1 },
{ chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 1 },
{
chunks: [
{
@@ -84,6 +86,7 @@ x\x1b[44my\x1b[49mz
modifiers: {},
},
],
text: 'abc',
lineNumber: 2,
},
{
@@ -101,9 +104,10 @@ x\x1b[44my\x1b[49mz
modifiers: {},
},
],
text: 'xyz',
lineNumber: 3,
},
{ chunks: [{ text: '', modifiers: {} }], lineNumber: 4 },
{ chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 4 },
]);
});
@@ -114,7 +118,7 @@ x\x1b[44my\x1b[49mz
a\x1b[45mb\x1b[35mc
x\x1b[39my\x1b[49mz`),
).toEqual([
{ chunks: [{ text: '', modifiers: {} }], lineNumber: 1 },
{ chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 1 },
{
chunks: [
{
@@ -130,6 +134,7 @@ x\x1b[39my\x1b[49mz`),
modifiers: { foreground: 'magenta', background: 'magenta' },
},
],
text: 'abc',
lineNumber: 2,
},
{
@@ -147,6 +152,7 @@ x\x1b[39my\x1b[49mz`),
modifiers: {},
},
],
text: 'xyz',
lineNumber: 3,
},
]);
@@ -157,7 +163,7 @@ x\x1b[39my\x1b[49mz`),
const out1 = processor.process(`
a\x1b[36mb\x1b[3mc`);
expect(out1).toEqual([
{ chunks: [{ text: '', modifiers: {} }], lineNumber: 1 },
{ chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 1 },
{
chunks: [
{
@@ -173,6 +179,7 @@ a\x1b[36mb\x1b[3mc`);
modifiers: { foreground: 'cyan', italic: true },
},
],
text: 'abc',
lineNumber: 2,
},
]);
@@ -181,7 +188,7 @@ a\x1b[36mb\x1b[3mc`);
a\x1b[36mb\x1b[3mc
x\x1b[39my\x1b[23mz`);
expect(out2).toEqual([
{ chunks: [{ text: '', modifiers: {} }], lineNumber: 1 },
{ chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 1 },
{
chunks: [
{
@@ -197,6 +204,7 @@ x\x1b[39my\x1b[23mz`);
modifiers: { foreground: 'cyan', italic: true },
},
],
text: 'abc',
lineNumber: 2,
},
{
@@ -214,6 +222,7 @@ x\x1b[39my\x1b[23mz`);
modifiers: {},
},
],
text: 'xyz',
lineNumber: 3,
},
]);
@@ -82,14 +82,25 @@ export interface AnsiChunk {
}
export class AnsiLine {
text: string;
constructor(
readonly lineNumber: number = 1,
readonly chunks: AnsiChunk[] = [],
) {}
) {
this.text = chunks.map(c => c.text).join('');
}
lastChunk(): AnsiChunk | undefined {
return this.chunks[this.chunks.length - 1];
}
replaceLastChunk(newChunks?: AnsiChunk[]) {
if (newChunks) {
this.chunks.splice(this.chunks.length - 1, 1, ...newChunks);
this.text = this.chunks.map(c => c.text).join('');
}
}
}
export class AnsiProcessor {
@@ -115,18 +126,14 @@ export class AnsiProcessor {
lastChunk?.modifiers,
lastLine?.lineNumber,
);
this.text = text;
lastLine.chunks.splice(
lastLine.chunks.length - 1,
1,
...newLines[0]?.chunks,
);
lastLine.replaceLastChunk(newLines[0]?.chunks);
this.lines[lastLineIndex] = lastLine;
this.lines.push(...newLines.slice(1));
} else {
this.lines = this.processLines(text);
this.text = text;
}
this.text = text;
return this.lines;
}
@@ -15,12 +15,16 @@
*/
import { makeStyles } from '@material-ui/core/styles';
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList } from 'react-window';
import { AnsiProcessor } from './AnsiProcessor';
import startCase from 'lodash/startCase';
import * as colors from '@material-ui/core/colors';
import clsx from 'clsx';
import TextField from '@material-ui/core/TextField';
const HEADER_SIZE = 40;
export interface LogViewerProps {
text: string;
@@ -48,18 +52,38 @@ export interface ChunkModifiers {
const useStyles = makeStyles(theme => ({
root: {
background: theme.palette.background.paper,
},
header: {
height: HEADER_SIZE,
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
},
log: {
fontFamily: '"Monaco", monospace',
fontSize: theme.typography.fontSize,
background: theme.palette.background.paper,
},
line: {
whiteSpace: 'pre',
'&:hover': {
background: theme.palette.action.hover,
},
},
lineSelected: {
background: theme.palette.action.selected,
'&:hover': {
background: theme.palette.action.selected,
},
},
lineNumber: {
display: 'inline-block',
textAlign: 'end',
width: 60,
marginRight: theme.spacing(1),
cursor: 'pointer',
},
modifierBold: {
fontWeight: theme.typography.fontWeightBold,
@@ -152,44 +176,81 @@ function getModifierClasses(
)}` as keyof typeof classes;
classNames.push(classes[key]);
}
return classNames.join(' ');
return classNames.length > 0 ? classNames.join(' ') : undefined;
}
export function LogViewer(props: LogViewerProps) {
const { noLineNumbers } = props;
const classes = useStyles();
const [selectedLine, setSelectedLine] = useState<number>();
const [filter, setFilter] = useState('');
// The processor keeps state that optimizes appending to the text
const processor = useMemo(() => new AnsiProcessor(), []);
const lines = processor.process(props.text);
const filteredLines = useMemo(() => {
if (!filter) {
return lines;
}
return lines.filter(line => line.text.includes(filter));
}, [lines, filter]);
return (
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
className={classes.root}
height={height}
width={width}
itemData={lines}
itemSize={20}
itemCount={lines.length}
>
{({ index, style, data }) => (
<div style={{ ...style }} className={classes.line}>
{!noLineNumbers && (
<span className={classes.lineNumber}>{index + 1}</span>
)}
{data[index].chunks.map(({ text, modifiers }, i) => (
<span
key={i}
className={getModifierClasses(classes, modifiers)}
<div style={{ width, height }} className={classes.root}>
<div className={classes.header}>
<TextField
size="small"
variant="standard"
placeholder="Search"
value={filter}
onChange={e => setFilter(e.target.value)}
/>
</div>
<FixedSizeList
className={classes.log}
height={height - HEADER_SIZE}
width={width}
itemData={filteredLines}
itemSize={20}
itemCount={filteredLines.length}
>
{({ index, style, data }) => {
const { chunks, lineNumber } = data[index];
return (
<div
style={{ ...style }}
className={clsx(classes.line, {
[classes.lineSelected]: selectedLine === lineNumber,
})}
>
{text}
</span>
))}
</div>
)}
</FixedSizeList>
{!noLineNumbers && (
<a
role="row"
target="_self"
href={`#line-${lineNumber}`}
className={classes.lineNumber}
onClick={() => setSelectedLine(lineNumber)}
onKeyPress={() => setSelectedLine(lineNumber)}
>
{lineNumber}
</a>
)}
{chunks.map(({ text, modifiers }, i) => (
<span
key={i}
className={getModifierClasses(classes, modifiers)}
>
{text}
</span>
))}
</div>
);
}}
</FixedSizeList>
</div>
)}
</AutoSizer>
);