feat: Log 组件支持日志检索功能;重构 Log 组件操作按钮样式和位置 (#4776)

* feat: Log 组件支持日志检索功能;重构 Log 组件操作按钮样式和位置

* 调整显示行数图标

* 调整显示行数图标

Co-authored-by: lvchenguang <lvchenguang@zhihu.com>
This commit is contained in:
mr-chenguang 2022-07-06 13:20:20 +08:00 committed by GitHub
parent fff1670035
commit 4d84083d99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 177 additions and 114 deletions

View File

@ -171,4 +171,4 @@ public class StreamingResponseBodyController {
| rowHeight | `number` | | 设置每行高度,将会开启虚拟渲染 |
| maxLength | `number` | | 最大显示行数 |
| disableColor | `boolean` | | 关闭 ANSI 颜色支持 |
| operation | `Array` | | 可选日志操作:['stop','clear','showLineNumber'] |
| operation | `Array` | | 可选日志操作:['stop','clear','showLineNumber','filter'] |

View File

@ -26,27 +26,55 @@
font-style: italic;
margin-right: 5px;
}
&-highlight {
background: #ffffb6;
display: inline-block;
color: #000;
}
}
&-operation {
position: absolute;
top: 0;
left: -16px;
.#{$ns}Button {
margin-right: var(--gap-xs);
position: relative;
background: rgba(28, 28, 28, 0.5);
& > a {
display: inline-flex;
flex-direction: row;
line-height: var(--Form-input-lineHeight);
font-size: var(--Form-input-fontSize);
flex-wrap: nowrap;
align-items: center;
justify-content: flex-end;
height: 30px;
width: 30px;
padding-left: 8px;
transition: all var(--animation-duration) ease-in-out;
border: var(--Form-input-borderWidth) solid transparent;
border-radius: var(--Form-input-borderRadius);
overflow: hidden;
height: 30px;
width: 30px;
}
&-hidden {
background: #f0f2f5;
width: 24px;
height: 48px;
box-shadow: 0 4px 6px 0 rgb(8 14 26 / 4%), 0 1px 10px 0 rgb(8 14 26 / 5%),
0 2px 4px -1px rgb(8 14 26 / 6%);
border-radius: 0 2px 2px 0;
padding-top: 14px;
padding-left: 6px;
cursor: pointer;
a {
color: #000;
transition: all 0.3s;
}
& > a:hover {
background: var(--Form-input-onHover-bg);
color: var(--link-color);
}
& > a > svg {
margin-right: 8px;
}
& > a.is-disabled {
cursor: not-allowed;
color: #6b7280;
}
& > a.is-disabled:hover {
cursor: not-allowed;
background: transparent;
color: #6b7280;
}
}

View File

@ -97,6 +97,7 @@ import Post from '../icons/post.svg';
import DotIcon from '../icons/dot.svg';
import Invisible from '../icons/invisible.svg';
import DateIcon from '../icons/date.svg';
import InvisibleIcon from '../icons/invisible.svg';
// 兼容原来的用法,后续不直接试用。
@ -220,6 +221,8 @@ registerIcon('desk-empty', DeskEmptyIcon);
registerIcon('invisible', Invisible);
registerIcon('plus-fine', PlusFineIcon);
registerIcon('date', DateIcon);
registerIcon('remove', RemoveIcon);
registerIcon('invisible', InvisibleIcon);
export function Icon({
icon,

View File

@ -2,23 +2,17 @@
* @file
*/
import React from 'react';
import {Renderer, RendererProps} from 'amis-core';
import {buildApi, isApiOutdated, Renderer, RendererProps} from 'amis-core';
import {BaseSchema} from '../Schema';
import Ansi from 'ansi-to-react';
import {buildApi, isApiOutdated} from 'amis-core';
import {VirtualList} from 'amis-ui';
import {Button} from 'amis-ui';
import {
InputClearIcon,
LeftArrowIcon,
MinusIcon,
PauseIcon,
PlusIcon,
ReloadIcon,
RightArrowIcon
} from 'amis-ui';
import {Icon, SearchBox, VirtualList} from 'amis-ui';
export type LogOperation = 'stop' | 'restart' | 'showLineNumber' | 'clear';
export type LogOperation =
| 'stop'
| 'restart'
| 'showLineNumber'
| 'clear'
| 'filter';
/**
*
@ -83,9 +77,10 @@ export interface LogProps
export interface LogState {
lastLine: string;
logs: string[];
originLogs: string[];
refresh: boolean;
showLineNumber: boolean;
showOperation: boolean;
filterWord: string;
}
export class Log extends React.Component<LogProps, LogState> {
@ -105,9 +100,10 @@ export class Log extends React.Component<LogProps, LogState> {
state: LogState = {
lastLine: '',
logs: [],
originLogs: [],
refresh: true,
showLineNumber: false,
showOperation: false
filterWord: ''
};
constructor(props: LogProps) {
@ -162,33 +158,40 @@ export class Log extends React.Component<LogProps, LogState> {
}
}
refresh = () => {
refresh = (e: React.MouseEvent<HTMLElement>) => {
let origin = this.state.refresh;
this.setState({
refresh: !origin
});
if (!origin) {
this.clear();
this.clear(e);
this.loadLogs();
}
e.preventDefault();
};
clear = () => {
clear = (e: React.MouseEvent<HTMLElement>) => {
this.setState({
logs: [],
lastLine: ''
});
e.preventDefault();
};
changeShowLineNumber = () => {
this.setState({
showLineNumber: !this.state.showLineNumber
});
};
changeFilterWord = (value: string) => {
let logs = this.state.originLogs;
if (
value !== '' &&
value !== undefined &&
value !== null &&
value.length > 0
) {
logs = logs.filter(line => line.includes(value));
}
changeShowOperation = () => {
this.setState({
showOperation: !this.state.showOperation
filterWord: value,
logs: logs
});
};
@ -244,11 +247,14 @@ export class Log extends React.Component<LogProps, LogState> {
logs = logs.concat(lines);
this.setState({
logs: logs,
originLogs: logs,
lastLine: lastline
});
}
}
this.changeFilterWord(this.state.filterWord);
if (done) {
this.isDone = true;
return;
@ -259,6 +265,37 @@ export class Log extends React.Component<LogProps, LogState> {
}
}
renderHighlightWord(line: string) {
const {classnames: cx} = this.props;
let {filterWord} = this.state;
if (filterWord === '') {
return line;
}
let items = line.split(filterWord);
return items.map((item, index) => {
if (index < items.length - 1) {
return (
<span>
{item}
<span className={cx('Log-line-highlight')}>{filterWord}</span>
</span>
);
}
return item;
});
}
renderHighlightWordWithAnsi(line: string) {
let {filterWord} = this.state;
if (filterWord === '') {
return line;
}
return line.replaceAll(
filterWord,
`\u001b[43;1m\u001b[30;1m${filterWord}\u001b[0m`
);
}
/**
*
*/
@ -269,7 +306,11 @@ export class Log extends React.Component<LogProps, LogState> {
{showLineNumber && (
<span className={cx('Log-line-number')}>{index + 1} </span>
)}
{disableColor ? line : <Ansi useClasses>{line}</Ansi>}
{disableColor ? (
this.renderHighlightWord(line)
) : (
<Ansi useClasses>{this.renderHighlightWordWithAnsi(line)}</Ansi>
)}
</div>
);
}
@ -284,11 +325,10 @@ export class Log extends React.Component<LogProps, LogState> {
rowHeight,
disableColor,
translate: __,
operation,
env
operation
} = this.props;
const {refresh, showLineNumber, showOperation} = this.state;
const {refresh, showLineNumber} = this.state;
let loading = __(placeholder);
@ -320,9 +360,11 @@ export class Log extends React.Component<LogProps, LogState> {
<span className={cx('Log-line-number')}>{index + 1} </span>
)}
{disableColor ? (
logs[index]
this.renderHighlightWord(logs[index])
) : (
<Ansi useClasses>{logs[index]}</Ansi>
<Ansi useClasses>
{this.renderHighlightWordWithAnsi(logs[index])}
</Ansi>
)}
</div>
)}
@ -336,6 +378,61 @@ export class Log extends React.Component<LogProps, LogState> {
return (
<div className={cx('Log', className)}>
<div className={cx('Log-operation')}>
{operation && operation?.length > 0 && (
<>
{operation.includes('stop') && (
<a
title={__('stop')}
className={!refresh ? 'is-disabled' : ''}
onClick={this.refresh}
>
<Icon icon="pause" />
</a>
)}
{operation.includes('restart') && (
<a
title={__('reload')}
className={refresh ? 'is-disabled' : ''}
onClick={this.refresh}
>
<Icon icon="refresh" />
</a>
)}
{operation.includes('showLineNumber') && (
<a
title={
showLineNumber
? __('Log.notShowLineNumber')
: __('Log.showLineNumber')
}
onClick={e => {
this.setState({showLineNumber: !showLineNumber});
e.preventDefault();
}}
>
<Icon icon={showLineNumber ? 'invisible' : 'view'} />
</a>
)}
{operation.includes('clear') && (
<a onClick={this.clear} title={__('clear')}>
<Icon icon="remove" />
</a>
)}
{operation && operation.includes('filter') && (
<SearchBox
className={cx('Log-filter-box')}
placeholder="过滤词"
onChange={this.changeFilterWord}
/>
)}
</>
)}
</div>
<div
ref={this.logRef}
className={cx('Log-body')}
@ -343,71 +440,6 @@ export class Log extends React.Component<LogProps, LogState> {
>
{useVirtualRender ? lines : lines.length ? lines : loading}
</div>
<div className={cx('Log-operation')}>
{operation &&
operation?.length > 0 &&
(showOperation ? (
<>
{operation.includes('stop') && (
<Button
size="sm"
title="__('stop')"
disabled={!refresh}
onClick={this.refresh}
>
<PauseIcon />
</Button>
)}
{operation.includes('restart') && (
<Button
size="sm"
title={__('reload')}
disabled={refresh}
onClick={this.refresh}
>
<ReloadIcon />
</Button>
)}
{operation.includes('showLineNumber') && (
<Button
size="sm"
title={
showLineNumber
? __('Log.notShowLineNumber')
: __('Log.showLineNumber')
}
onClick={this.changeShowLineNumber}
>
{showLineNumber ? <MinusIcon /> : <PlusIcon />}
</Button>
)}
{operation.includes('clear') && (
<Button size="sm" title={__('clear')} onClick={this.clear}>
<InputClearIcon />
</Button>
)}
<Button
size="sm"
title={__('Log.collapse')}
onClick={this.changeShowOperation}
>
<LeftArrowIcon />
</Button>
</>
) : (
<div
title={__('Log.expand')}
className={cx('Log-operation-hidden')}
onClick={this.changeShowOperation}
>
<RightArrowIcon />
</div>
))}
</div>
</div>
);
}