core-components: basic line selection and filtering for LogViewer
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user