Logs2 -> Logs
Summary: Use Logs2 plugin now as the default logs plugin (by overwriting it). See the rest of this stack Changelog: The device logs plugin has been fully rewritten. It is faster and more reponsive, formats urls and json, and supports line wrapping and text selection. Beyond that it is now possible to sort and filter on all columns and pause and resume the log stream. Reviewed By: nikoant Differential Revision: D27048528 fbshipit-source-id: e18386fec6846ac3568f33a3578f4742213ecaca
This commit is contained in:
committed by
Facebook GitHub Bot
parent
d293b2b0e5
commit
9d3c48fd84
@@ -1,234 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {TableBodyRow} from 'flipper';
|
||||
|
||||
import {
|
||||
PureComponent,
|
||||
FlexColumn,
|
||||
Panel,
|
||||
Input,
|
||||
Toolbar,
|
||||
Text,
|
||||
ManagedTable,
|
||||
Button,
|
||||
colors,
|
||||
styled,
|
||||
} from 'flipper';
|
||||
import React from 'react';
|
||||
|
||||
export type Counter = {
|
||||
readonly expression: RegExp;
|
||||
readonly count: number;
|
||||
readonly notify: boolean;
|
||||
readonly label: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
readonly onChange: (counters: ReadonlyArray<Counter>) => void;
|
||||
readonly counters: ReadonlyArray<Counter>;
|
||||
};
|
||||
|
||||
type State = {
|
||||
readonly input: string;
|
||||
readonly highlightedRow: string | null;
|
||||
};
|
||||
|
||||
const ColumnSizes = {
|
||||
expression: '70%',
|
||||
count: '15%',
|
||||
notify: 'flex',
|
||||
};
|
||||
|
||||
const Columns = {
|
||||
expression: {
|
||||
value: 'Expression',
|
||||
resizable: false,
|
||||
},
|
||||
count: {
|
||||
value: 'Count',
|
||||
resizable: false,
|
||||
},
|
||||
notify: {
|
||||
value: 'Notify',
|
||||
resizable: false,
|
||||
},
|
||||
};
|
||||
|
||||
const Count = styled(Text)({
|
||||
alignSelf: 'center',
|
||||
background: colors.macOSHighlightActive,
|
||||
color: colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
textAlign: 'center',
|
||||
borderRadius: '999em',
|
||||
padding: '4px 9px 3px',
|
||||
lineHeight: '100%',
|
||||
marginLeft: 'auto',
|
||||
});
|
||||
|
||||
const Checkbox = styled(Input)({
|
||||
lineHeight: '100%',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
height: 'auto',
|
||||
alignSelf: 'center',
|
||||
});
|
||||
|
||||
const ExpressionInput = styled(Input)({
|
||||
flexGrow: 1,
|
||||
});
|
||||
|
||||
const WatcherPanel = styled(Panel)({
|
||||
minHeight: 200,
|
||||
});
|
||||
|
||||
export default class LogWatcher extends PureComponent<Props, State> {
|
||||
state = {
|
||||
input: '',
|
||||
highlightedRow: null,
|
||||
};
|
||||
|
||||
_inputRef: HTMLInputElement | undefined;
|
||||
|
||||
onAdd = () => {
|
||||
if (
|
||||
this.props.counters.findIndex(({label}) => label === this.state.input) >
|
||||
-1 ||
|
||||
this.state.input.length === 0
|
||||
) {
|
||||
// prevent duplicates
|
||||
return;
|
||||
}
|
||||
this.props.onChange([
|
||||
...this.props.counters,
|
||||
{
|
||||
label: this.state.input,
|
||||
expression: new RegExp(this.state.input, 'gi'),
|
||||
notify: false,
|
||||
count: 0,
|
||||
},
|
||||
]);
|
||||
this.setState({input: ''});
|
||||
};
|
||||
|
||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
input: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
resetCount = (index: number) => {
|
||||
const newCounters = [...this.props.counters];
|
||||
newCounters[index] = {
|
||||
...newCounters[index],
|
||||
count: 0,
|
||||
};
|
||||
this.props.onChange(newCounters);
|
||||
};
|
||||
|
||||
buildRows = (): Array<TableBodyRow> => {
|
||||
return this.props.counters.map(({label, count, notify}, i) => ({
|
||||
columns: {
|
||||
expression: {
|
||||
value: <Text code={true}>{label}</Text>,
|
||||
},
|
||||
count: {
|
||||
value: <Count onClick={() => this.resetCount(i)}>{count}</Count>,
|
||||
},
|
||||
notify: {
|
||||
value: (
|
||||
<Checkbox
|
||||
type="checkbox"
|
||||
checked={notify}
|
||||
onChange={() => this.setNotification(i, !notify)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
key: label,
|
||||
}));
|
||||
};
|
||||
|
||||
setNotification = (index: number, notify: boolean) => {
|
||||
const newCounters: Array<Counter> = [...this.props.counters];
|
||||
newCounters[index] = {
|
||||
...newCounters[index],
|
||||
notify,
|
||||
};
|
||||
this.props.onChange(newCounters);
|
||||
};
|
||||
|
||||
onRowHighlighted = (rows: Array<string>) => {
|
||||
this.setState({
|
||||
highlightedRow: rows.length === 1 ? rows[0] : null,
|
||||
});
|
||||
};
|
||||
|
||||
onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
this.handleDelete();
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.onAdd();
|
||||
}
|
||||
};
|
||||
|
||||
handleDelete = () => {
|
||||
if (this.state.highlightedRow != null) {
|
||||
this.props.onChange(
|
||||
this.props.counters.filter(
|
||||
({label}) => label !== this.state.highlightedRow,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FlexColumn grow={true} tabIndex={-1} onKeyDown={this.onKeyDown}>
|
||||
<WatcherPanel
|
||||
heading="Expression Watcher"
|
||||
floating={false}
|
||||
collapsable={true}
|
||||
padded={false}>
|
||||
<Toolbar>
|
||||
<ExpressionInput
|
||||
value={this.state.input}
|
||||
placeholder="Expression..."
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onSubmit}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.onAdd}
|
||||
disabled={this.state.input.length === 0}>
|
||||
Add counter
|
||||
</Button>
|
||||
</Toolbar>
|
||||
<ManagedTable
|
||||
onRowHighlighted={this.onRowHighlighted}
|
||||
columnSizes={ColumnSizes}
|
||||
columns={Columns}
|
||||
rows={this.buildRows()}
|
||||
autoHeight={true}
|
||||
floating={false}
|
||||
zebra={false}
|
||||
buildContextMenuItems={() => {
|
||||
return [{label: 'Delete', click: this.handleDelete}];
|
||||
}}
|
||||
/>
|
||||
</WatcherPanel>
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {sleep, TestUtils} from 'flipper-plugin';
|
||||
import {addEntriesToState, processEntry} from '../index';
|
||||
import {DeviceLogEntry} from 'flipper';
|
||||
import * as LogsPlugin from '../index';
|
||||
|
||||
const entry: DeviceLogEntry = {
|
||||
tag: 'OpenGLRenderer',
|
||||
pid: 18384,
|
||||
tid: 18409,
|
||||
message: 'Swap behavior 1',
|
||||
date: new Date('Feb 28 2013 19:00:00 EST'),
|
||||
type: 'debug',
|
||||
};
|
||||
|
||||
test('processEntry', () => {
|
||||
const key = 'key';
|
||||
const processedEntry = processEntry(entry, key);
|
||||
expect(processedEntry.entry).toEqual(entry);
|
||||
expect(processedEntry.row.key).toBe(key);
|
||||
expect(typeof processedEntry.row.height).toBe('number');
|
||||
});
|
||||
|
||||
test('addEntriesToState without current state', () => {
|
||||
const processedEntry = processEntry(entry, 'key');
|
||||
const newState = addEntriesToState([processedEntry]);
|
||||
|
||||
expect(newState.rows.length).toBe(1);
|
||||
expect(newState.entries.length).toBe(1);
|
||||
expect(newState.entries[0]).toEqual(processedEntry);
|
||||
});
|
||||
|
||||
test('addEntriesToState with current state', () => {
|
||||
const firstProcessedEntry = processEntry(entry, 'key1');
|
||||
const currentState = addEntriesToState([firstProcessedEntry]);
|
||||
const secondProcessedEntry = processEntry(
|
||||
{
|
||||
...entry,
|
||||
message: 'new message',
|
||||
},
|
||||
'key2',
|
||||
);
|
||||
const newState = addEntriesToState([secondProcessedEntry], currentState);
|
||||
expect(newState.entries.length).toBe(2);
|
||||
expect(newState.rows.length).toBe(2);
|
||||
expect(newState.rows[0]).toEqual(firstProcessedEntry.row);
|
||||
expect(newState.rows[1]).toEqual(secondProcessedEntry.row);
|
||||
});
|
||||
|
||||
test('addEntriesToState increase counter on duplicate message', () => {
|
||||
const currentState = addEntriesToState([processEntry(entry, 'key1')]);
|
||||
const processedEntry = processEntry(entry, 'key2');
|
||||
const newState = addEntriesToState([processedEntry], currentState);
|
||||
expect(newState.rows.length).toBe(1);
|
||||
expect(newState.entries.length).toBe(2);
|
||||
expect(newState.rows[0].columns.type.value.props.children).toBe(2);
|
||||
});
|
||||
|
||||
test('addEntriesToState with reversed direction (add to front)', () => {
|
||||
const firstProcessedEntry = processEntry(entry, 'key1');
|
||||
const currentState = addEntriesToState([firstProcessedEntry]);
|
||||
expect(currentState.rows.length).toBe(1);
|
||||
expect(currentState.entries.length).toBe(1);
|
||||
const secondProcessedEntry = processEntry(
|
||||
{
|
||||
...entry,
|
||||
message: 'second message',
|
||||
date: new Date('Feb 28 2013 19:01:00 EST'),
|
||||
},
|
||||
'key2',
|
||||
);
|
||||
const newState = addEntriesToState(
|
||||
[secondProcessedEntry],
|
||||
currentState,
|
||||
'down',
|
||||
);
|
||||
expect(newState.entries.length).toBe(2);
|
||||
expect(newState.rows.length).toBe(2);
|
||||
});
|
||||
|
||||
test('export / import plugin does work', async () => {
|
||||
const {
|
||||
instance,
|
||||
exportStateAsync,
|
||||
sendLogEntry,
|
||||
} = TestUtils.startDevicePlugin(LogsPlugin);
|
||||
|
||||
sendLogEntry({
|
||||
date: new Date(1611854112859),
|
||||
message: 'test1',
|
||||
pid: 0,
|
||||
tag: 'test',
|
||||
tid: 1,
|
||||
type: 'error',
|
||||
app: 'X',
|
||||
});
|
||||
sendLogEntry({
|
||||
date: new Date(1611854117859),
|
||||
message: 'test2',
|
||||
pid: 2,
|
||||
tag: 'test',
|
||||
tid: 3,
|
||||
type: 'warn',
|
||||
app: 'Y',
|
||||
});
|
||||
|
||||
// batching and storage is async atm
|
||||
await sleep(500);
|
||||
|
||||
const data = await exportStateAsync();
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"logs": Array [
|
||||
Object {
|
||||
"app": "X",
|
||||
"date": 2021-01-28T17:15:12.859Z,
|
||||
"message": "test1",
|
||||
"pid": 0,
|
||||
"tag": "test",
|
||||
"tid": 1,
|
||||
"type": "error",
|
||||
},
|
||||
Object {
|
||||
"app": "Y",
|
||||
"date": 2021-01-28T17:15:17.859Z,
|
||||
"message": "test2",
|
||||
"pid": 2,
|
||||
"tag": "test",
|
||||
"tid": 3,
|
||||
"type": "warn",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
expect(instance.rows.get().length).toBe(2);
|
||||
|
||||
// Run a second import
|
||||
{
|
||||
const {exportStateAsync} = TestUtils.startDevicePlugin(LogsPlugin, {
|
||||
initialState: data,
|
||||
});
|
||||
|
||||
expect(await exportStateAsync()).toEqual(data);
|
||||
}
|
||||
});
|
||||
176
desktop/plugins/logs/__tests__/logs.node.tsx
Normal file
176
desktop/plugins/logs/__tests__/logs.node.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {sleep, TestUtils} from 'flipper-plugin';
|
||||
import * as LogsPlugin from '../index';
|
||||
|
||||
const entry1 = {
|
||||
date: new Date(1611854112859),
|
||||
message: 'test1',
|
||||
pid: 0,
|
||||
tag: 'test',
|
||||
tid: 1,
|
||||
type: 'error',
|
||||
app: 'X',
|
||||
} as const;
|
||||
const entry2 = {
|
||||
date: new Date(1611854117859),
|
||||
message: 'test2',
|
||||
pid: 2,
|
||||
tag: 'test',
|
||||
tid: 3,
|
||||
type: 'warn',
|
||||
app: 'Y',
|
||||
} as const;
|
||||
const entry3 = {
|
||||
date: new Date(1611854112859),
|
||||
message: 'test3',
|
||||
pid: 0,
|
||||
tag: 'test',
|
||||
tid: 1,
|
||||
type: 'error',
|
||||
app: 'X',
|
||||
} as const;
|
||||
|
||||
test('it will merge equal rows', () => {
|
||||
const {instance, sendLogEntry} = TestUtils.startDevicePlugin(LogsPlugin);
|
||||
|
||||
sendLogEntry(entry1);
|
||||
sendLogEntry(entry2);
|
||||
sendLogEntry({
|
||||
...entry2,
|
||||
date: new Date(1611954117859),
|
||||
});
|
||||
sendLogEntry(entry3);
|
||||
|
||||
expect(instance.rows.records()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"app": "X",
|
||||
"count": 1,
|
||||
"date": 2021-01-28T17:15:12.859Z,
|
||||
"message": "test1",
|
||||
"pid": 0,
|
||||
"tag": "test",
|
||||
"tid": 1,
|
||||
"type": "error",
|
||||
},
|
||||
Object {
|
||||
"app": "Y",
|
||||
"count": 2,
|
||||
"date": 2021-01-28T17:15:17.859Z,
|
||||
"message": "test2",
|
||||
"pid": 2,
|
||||
"tag": "test",
|
||||
"tid": 3,
|
||||
"type": "warn",
|
||||
},
|
||||
Object {
|
||||
"app": "X",
|
||||
"count": 1,
|
||||
"date": 2021-01-28T17:15:12.859Z,
|
||||
"message": "test3",
|
||||
"pid": 0,
|
||||
"tag": "test",
|
||||
"tid": 1,
|
||||
"type": "error",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('it supports deeplink and select nodes + navigating to bottom', async () => {
|
||||
const {
|
||||
instance,
|
||||
sendLogEntry,
|
||||
triggerDeepLink,
|
||||
act,
|
||||
triggerMenuEntry,
|
||||
} = TestUtils.renderDevicePlugin(LogsPlugin);
|
||||
|
||||
sendLogEntry(entry1);
|
||||
sendLogEntry(entry2);
|
||||
sendLogEntry(entry3);
|
||||
|
||||
expect(instance.tableManagerRef).not.toBeUndefined();
|
||||
expect(instance.tableManagerRef.current?.getSelectedItems()).toEqual([]);
|
||||
|
||||
act(() => {
|
||||
triggerDeepLink('test2');
|
||||
});
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
expect(instance.tableManagerRef.current?.getSelectedItems()).toEqual([
|
||||
{
|
||||
...entry2,
|
||||
count: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
act(() => {
|
||||
triggerMenuEntry('goToBottom');
|
||||
});
|
||||
expect(instance.tableManagerRef.current?.getSelectedItems()).toEqual([
|
||||
{
|
||||
...entry3,
|
||||
count: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('export / import plugin does work', async () => {
|
||||
const {
|
||||
instance,
|
||||
exportStateAsync,
|
||||
sendLogEntry,
|
||||
} = TestUtils.startDevicePlugin(LogsPlugin);
|
||||
|
||||
sendLogEntry(entry1);
|
||||
sendLogEntry(entry2);
|
||||
|
||||
const data = await exportStateAsync();
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"logs": Array [
|
||||
Object {
|
||||
"app": "X",
|
||||
"count": 1,
|
||||
"date": 2021-01-28T17:15:12.859Z,
|
||||
"message": "test1",
|
||||
"pid": 0,
|
||||
"tag": "test",
|
||||
"tid": 1,
|
||||
"type": "error",
|
||||
},
|
||||
Object {
|
||||
"app": "Y",
|
||||
"count": 1,
|
||||
"date": 2021-01-28T17:15:17.859Z,
|
||||
"message": "test2",
|
||||
"pid": 2,
|
||||
"tag": "test",
|
||||
"tid": 3,
|
||||
"type": "warn",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
expect(instance.rows.size).toBe(2);
|
||||
|
||||
// Run a second import
|
||||
{
|
||||
const {exportStateAsync} = TestUtils.startDevicePlugin(LogsPlugin, {
|
||||
initialState: data,
|
||||
});
|
||||
|
||||
expect(await exportStateAsync()).toEqual(data);
|
||||
}
|
||||
});
|
||||
@@ -7,372 +7,123 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {TableBodyRow, TableRowSortOrder} from 'flipper';
|
||||
import {
|
||||
Device,
|
||||
DevicePluginClient,
|
||||
DeviceLogEntry,
|
||||
createState,
|
||||
usePlugin,
|
||||
createDataSource,
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
theme,
|
||||
DataTableManager,
|
||||
createState,
|
||||
useValue,
|
||||
DataFormatter,
|
||||
} from 'flipper-plugin';
|
||||
import {Counter} from './LogWatcher';
|
||||
|
||||
import {
|
||||
ManagedTableClass,
|
||||
Button,
|
||||
colors,
|
||||
ContextMenu,
|
||||
FlexColumn,
|
||||
DetailSidebar,
|
||||
SearchableTable,
|
||||
styled,
|
||||
textContent,
|
||||
MenuTemplate,
|
||||
} from 'flipper';
|
||||
import LogWatcher from './LogWatcher';
|
||||
import React, {useCallback, createRef, MutableRefObject} from 'react';
|
||||
import {Icon, LogCount, HiddenScrollText} from './logComponents';
|
||||
import {pad, getLineCount} from './logUtils';
|
||||
PlayCircleOutlined,
|
||||
PauseCircleOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import React, {createRef, CSSProperties} from 'react';
|
||||
import {Badge, Button} from 'antd';
|
||||
|
||||
const LOG_WATCHER_LOCAL_STORAGE_KEY = 'LOG_WATCHER_LOCAL_STORAGE_KEY';
|
||||
import {baseRowStyle, logTypes} from './logTypes';
|
||||
|
||||
type Entries = ReadonlyArray<{
|
||||
readonly row: TableBodyRow;
|
||||
readonly entry: DeviceLogEntry;
|
||||
}>;
|
||||
|
||||
type BaseState = {
|
||||
readonly rows: ReadonlyArray<TableBodyRow>;
|
||||
readonly entries: Entries;
|
||||
export type ExtendedLogEntry = DeviceLogEntry & {
|
||||
count: number;
|
||||
};
|
||||
|
||||
const COLUMN_SIZE = {
|
||||
type: 40,
|
||||
time: 120,
|
||||
pid: 60,
|
||||
tid: 60,
|
||||
tag: 120,
|
||||
app: 200,
|
||||
message: 'flex',
|
||||
} as const;
|
||||
|
||||
const COLUMNS = {
|
||||
type: {
|
||||
value: '',
|
||||
},
|
||||
time: {
|
||||
value: 'Time',
|
||||
sortable: true,
|
||||
},
|
||||
pid: {
|
||||
value: 'PID',
|
||||
},
|
||||
tid: {
|
||||
value: 'TID',
|
||||
},
|
||||
tag: {
|
||||
value: 'Tag',
|
||||
},
|
||||
app: {
|
||||
value: 'App',
|
||||
},
|
||||
message: {
|
||||
value: 'Message',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const COLUMN_ORDER = [
|
||||
const columns: DataTableColumn<ExtendedLogEntry>[] = [
|
||||
{
|
||||
key: 'type',
|
||||
visible: true,
|
||||
title: '',
|
||||
width: 30,
|
||||
filters: Object.entries(logTypes).map(([value, config]) => ({
|
||||
label: config.label,
|
||||
value,
|
||||
enabled: config.enabled,
|
||||
})),
|
||||
onRender(entry) {
|
||||
return entry.count > 1 ? (
|
||||
<Badge
|
||||
count={entry.count}
|
||||
size="small"
|
||||
style={{
|
||||
marginTop: 4,
|
||||
color: theme.white,
|
||||
background:
|
||||
(logTypes[entry.type]?.style as any)?.color ??
|
||||
theme.textColorSecondary,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
logTypes[entry.type]?.icon
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'time',
|
||||
visible: true,
|
||||
key: 'date',
|
||||
title: 'Time',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
key: 'pid',
|
||||
title: 'PID',
|
||||
width: 60,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
key: 'tid',
|
||||
title: 'TID',
|
||||
width: 60,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
key: 'tag',
|
||||
visible: true,
|
||||
title: 'Tag',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
key: 'app',
|
||||
visible: true,
|
||||
title: 'App',
|
||||
width: 160,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
key: 'message',
|
||||
visible: true,
|
||||
},
|
||||
] as const;
|
||||
|
||||
const LOG_TYPES: {
|
||||
[level: string]: {
|
||||
label: string;
|
||||
color: string;
|
||||
icon?: React.ReactNode;
|
||||
style?: Object;
|
||||
};
|
||||
} = {
|
||||
verbose: {
|
||||
label: 'Verbose',
|
||||
color: colors.purple,
|
||||
},
|
||||
debug: {
|
||||
label: 'Debug',
|
||||
color: colors.grey,
|
||||
},
|
||||
info: {
|
||||
label: 'Info',
|
||||
icon: <Icon name="info-circle" color={colors.cyan} />,
|
||||
color: colors.cyan,
|
||||
},
|
||||
warn: {
|
||||
label: 'Warn',
|
||||
style: {
|
||||
backgroundColor: colors.yellowTint,
|
||||
color: colors.yellow,
|
||||
fontWeight: 500,
|
||||
},
|
||||
icon: <Icon name="caution-triangle" color={colors.yellow} />,
|
||||
color: colors.yellow,
|
||||
},
|
||||
error: {
|
||||
label: 'Error',
|
||||
style: {
|
||||
backgroundColor: colors.redTint,
|
||||
color: colors.red,
|
||||
fontWeight: 500,
|
||||
},
|
||||
icon: <Icon name="caution-octagon" color={colors.red} />,
|
||||
color: colors.red,
|
||||
},
|
||||
fatal: {
|
||||
label: 'Fatal',
|
||||
style: {
|
||||
backgroundColor: colors.redTint,
|
||||
color: colors.red,
|
||||
fontWeight: 700,
|
||||
},
|
||||
icon: <Icon name="stop" color={colors.red} />,
|
||||
color: colors.red,
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_FILTERS = [
|
||||
{
|
||||
type: 'enum',
|
||||
enum: Object.keys(LOG_TYPES).map((value) => ({
|
||||
label: LOG_TYPES[value].label,
|
||||
value,
|
||||
})),
|
||||
key: 'type',
|
||||
value: [],
|
||||
persistent: true,
|
||||
title: 'Message',
|
||||
wrap: true,
|
||||
formatters: [DataFormatter.prettyPrintJson, DataFormatter.linkify],
|
||||
},
|
||||
];
|
||||
|
||||
export function addEntriesToState(
|
||||
items: Entries,
|
||||
state: BaseState = {
|
||||
rows: [],
|
||||
entries: [],
|
||||
} as const,
|
||||
addDirection: 'up' | 'down' = 'up',
|
||||
): BaseState {
|
||||
const rows = [...state.rows];
|
||||
const entries = [...state.entries];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const {entry, row} = items[i];
|
||||
entries.push({row, entry});
|
||||
|
||||
let previousEntry: DeviceLogEntry | null = null;
|
||||
|
||||
if (i > 0) {
|
||||
previousEntry = items[i - 1].entry;
|
||||
} else if (state.rows.length > 0 && state.entries.length > 0) {
|
||||
previousEntry = state.entries[state.entries.length - 1].entry;
|
||||
}
|
||||
|
||||
addRowIfNeeded(rows, row, entry, previousEntry, addDirection);
|
||||
}
|
||||
|
||||
return {
|
||||
entries,
|
||||
rows,
|
||||
};
|
||||
function getRowStyle(entry: DeviceLogEntry): CSSProperties | undefined {
|
||||
return (logTypes[entry.type]?.style as any) ?? baseRowStyle;
|
||||
}
|
||||
|
||||
export function addRowIfNeeded(
|
||||
rows: Array<TableBodyRow>,
|
||||
row: TableBodyRow,
|
||||
entry: DeviceLogEntry,
|
||||
previousEntry: DeviceLogEntry | null,
|
||||
addDirection: 'up' | 'down' = 'up',
|
||||
) {
|
||||
const previousRow =
|
||||
rows.length > 0
|
||||
? addDirection === 'up'
|
||||
? rows[rows.length - 1]
|
||||
: rows[0]
|
||||
: null;
|
||||
if (
|
||||
previousRow &&
|
||||
previousEntry &&
|
||||
entry.message === previousEntry.message &&
|
||||
entry.tag === previousEntry.tag &&
|
||||
previousRow.type != null
|
||||
) {
|
||||
// duplicate log, increase counter
|
||||
const count =
|
||||
previousRow.columns.type.value &&
|
||||
previousRow.columns.type.value.props &&
|
||||
typeof previousRow.columns.type.value.props.children === 'number'
|
||||
? previousRow.columns.type.value.props.children + 1
|
||||
: 2;
|
||||
const type = LOG_TYPES[previousRow.type] || LOG_TYPES.debug;
|
||||
previousRow.columns.type.value = (
|
||||
<LogCount backgroundColor={type.color}>{count}</LogCount>
|
||||
);
|
||||
} else {
|
||||
if (addDirection === 'up') {
|
||||
rows.push(row);
|
||||
} else {
|
||||
rows.unshift(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function processEntry(
|
||||
entry: DeviceLogEntry,
|
||||
key: string,
|
||||
): {
|
||||
row: TableBodyRow;
|
||||
entry: DeviceLogEntry;
|
||||
} {
|
||||
const {icon, style} = LOG_TYPES[entry.type] || LOG_TYPES.debug;
|
||||
// build the item, it will either be batched or added straight away
|
||||
return {
|
||||
entry,
|
||||
row: {
|
||||
columns: {
|
||||
type: {
|
||||
value: icon,
|
||||
align: 'center',
|
||||
},
|
||||
time: {
|
||||
value: (
|
||||
<HiddenScrollText code={true}>
|
||||
{entry.date.toTimeString().split(' ')[0] +
|
||||
'.' +
|
||||
pad(entry.date.getMilliseconds(), 3)}
|
||||
</HiddenScrollText>
|
||||
),
|
||||
},
|
||||
message: {
|
||||
value: (
|
||||
<HiddenScrollText code={true}>{entry.message}</HiddenScrollText>
|
||||
),
|
||||
},
|
||||
tag: {
|
||||
value: <HiddenScrollText code={true}>{entry.tag}</HiddenScrollText>,
|
||||
isFilterable: true,
|
||||
},
|
||||
pid: {
|
||||
value: (
|
||||
<HiddenScrollText code={true}>{String(entry.pid)}</HiddenScrollText>
|
||||
),
|
||||
isFilterable: true,
|
||||
},
|
||||
tid: {
|
||||
value: (
|
||||
<HiddenScrollText code={true}>{String(entry.tid)}</HiddenScrollText>
|
||||
),
|
||||
isFilterable: true,
|
||||
},
|
||||
app: {
|
||||
value: <HiddenScrollText code={true}>{entry.app}</HiddenScrollText>,
|
||||
isFilterable: true,
|
||||
},
|
||||
},
|
||||
height: getLineCount(entry.message) * 15 + 10, // 15px per line height + 8px padding
|
||||
style,
|
||||
type: entry.type,
|
||||
filterValue: entry.message,
|
||||
key,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function supportsDevice(device: Device) {
|
||||
return (
|
||||
device.os === 'Android' ||
|
||||
device.os === 'Metro' ||
|
||||
(device.os === 'iOS' && device.deviceType !== 'physical')
|
||||
);
|
||||
}
|
||||
|
||||
type ExportedState = {
|
||||
logs: DeviceLogEntry[];
|
||||
};
|
||||
|
||||
export function devicePlugin(client: DevicePluginClient) {
|
||||
let counter = 0;
|
||||
let batch: Array<{
|
||||
readonly row: TableBodyRow;
|
||||
readonly entry: DeviceLogEntry;
|
||||
}> = [];
|
||||
let queued: boolean = false;
|
||||
let batchTimer: NodeJS.Timeout | undefined;
|
||||
const tableRef: MutableRefObject<ManagedTableClass | null> = createRef();
|
||||
|
||||
const rows = createState<ReadonlyArray<TableBodyRow>>([]);
|
||||
const entries = createState<Entries>([]);
|
||||
const highlightedRows = createState<ReadonlySet<string>>(new Set());
|
||||
const counters = createState<ReadonlyArray<Counter>>(restoreSavedCounters());
|
||||
const timeDirection = createState<'up' | 'down'>('up');
|
||||
const isDeeplinked = createState(false);
|
||||
|
||||
client.onExport<ExportedState>(async () => {
|
||||
return {
|
||||
logs: entries
|
||||
.get()
|
||||
.slice(-100 * 1000)
|
||||
.map((e) => e.entry),
|
||||
};
|
||||
});
|
||||
|
||||
client.onImport<ExportedState>((data) => {
|
||||
const imported = addEntriesToState(
|
||||
data.logs.map((log) => processEntry(log, '' + counter++)),
|
||||
);
|
||||
rows.set(imported.rows);
|
||||
entries.set(imported.entries);
|
||||
const rows = createDataSource<ExtendedLogEntry>([], {
|
||||
limit: 200000,
|
||||
persist: 'logs',
|
||||
});
|
||||
const isPaused = createState(true);
|
||||
const tableManagerRef = createRef<
|
||||
undefined | DataTableManager<ExtendedLogEntry>
|
||||
>();
|
||||
|
||||
client.onDeepLink((payload: unknown) => {
|
||||
if (typeof payload === 'string') {
|
||||
highlightedRows.set(calculateHighlightedRows(payload, rows.get()));
|
||||
isDeeplinked.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
client.onDeactivate(() => {
|
||||
isDeeplinked.set(false);
|
||||
tableRef.current = null;
|
||||
});
|
||||
|
||||
client.onDestroy(() => {
|
||||
if (batchTimer) {
|
||||
clearTimeout(batchTimer);
|
||||
// timeout as we want to await restoring any previous scroll positin first, then scroll to the
|
||||
setTimeout(() => {
|
||||
let hasMatch = false;
|
||||
rows.view.output(0, rows.view.size).forEach((row, index) => {
|
||||
if (row.message.includes(payload)) {
|
||||
tableManagerRef.current?.selectItem(index, hasMatch);
|
||||
hasMatch = true;
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -391,237 +142,97 @@ export function devicePlugin(client: DevicePluginClient) {
|
||||
},
|
||||
);
|
||||
|
||||
client.device.onLogEntry((entry: DeviceLogEntry) => {
|
||||
const processedEntry = processEntry(entry, '' + counter++);
|
||||
incrementCounterIfNeeded(processedEntry.entry);
|
||||
scheduleEntryForBatch(processedEntry);
|
||||
});
|
||||
let logDisposer: (() => void) | undefined;
|
||||
|
||||
// TODO: make local storage abstraction T69990351
|
||||
function restoreSavedCounters(): Counter[] {
|
||||
const savedCounters =
|
||||
window.localStorage.getItem(LOG_WATCHER_LOCAL_STORAGE_KEY) || '[]';
|
||||
return JSON.parse(savedCounters).map((counter: Counter) => ({
|
||||
...counter,
|
||||
expression: new RegExp(counter.label, 'gi'),
|
||||
count: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
function calculateHighlightedRows(
|
||||
deepLinkPayload: unknown,
|
||||
rows: ReadonlyArray<TableBodyRow>,
|
||||
): Set<string> {
|
||||
const highlightedRows = new Set<string>();
|
||||
if (typeof deepLinkPayload !== 'string') {
|
||||
return highlightedRows;
|
||||
}
|
||||
|
||||
// Run through array from last to first, because we want to show the last
|
||||
// time it the log we are looking for appeared.
|
||||
for (let i = rows.length - 1; i >= 0; i--) {
|
||||
const filterValue = rows[i].filterValue;
|
||||
if (filterValue != null && filterValue.includes(deepLinkPayload)) {
|
||||
highlightedRows.add(rows[i].key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (highlightedRows.size <= 0) {
|
||||
// Check if the individual lines in the deeplinkPayload is matched or not.
|
||||
const arr = deepLinkPayload.split('\n');
|
||||
for (const msg of arr) {
|
||||
for (let i = rows.length - 1; i >= 0; i--) {
|
||||
const filterValue = rows[i].filterValue;
|
||||
if (filterValue != null && filterValue.includes(msg)) {
|
||||
highlightedRows.add(rows[i].key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return highlightedRows;
|
||||
}
|
||||
|
||||
function incrementCounterIfNeeded(entry: DeviceLogEntry) {
|
||||
let counterUpdated = false;
|
||||
const newCounters = counters.get().map((counter) => {
|
||||
if (entry.message.match(counter.expression)) {
|
||||
counterUpdated = true;
|
||||
if (counter.notify) {
|
||||
// TODO: use new notifications system T69990351
|
||||
new Notification(`${counter.label}`, {
|
||||
body: 'The watched log message appeared',
|
||||
function resumePause() {
|
||||
if (isPaused.get() && client.device.isConnected) {
|
||||
// start listening to the logs
|
||||
isPaused.set(false);
|
||||
logDisposer = client.device.onLogEntry((entry: DeviceLogEntry) => {
|
||||
const lastIndex = rows.size - 1;
|
||||
const previousRow = rows.get(lastIndex);
|
||||
if (
|
||||
previousRow &&
|
||||
previousRow.message === entry.message &&
|
||||
previousRow.tag === entry.tag &&
|
||||
previousRow.type === entry.type
|
||||
) {
|
||||
rows.update(lastIndex, {
|
||||
...previousRow,
|
||||
count: previousRow.count + 1,
|
||||
});
|
||||
} else {
|
||||
rows.append({
|
||||
...entry,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
return {
|
||||
...counter,
|
||||
count: counter.count + 1,
|
||||
};
|
||||
} else {
|
||||
return counter;
|
||||
}
|
||||
});
|
||||
if (counterUpdated) {
|
||||
counters.set(newCounters);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleEntryForBatch(item: {
|
||||
row: TableBodyRow;
|
||||
entry: DeviceLogEntry;
|
||||
}) {
|
||||
// batch up logs to be processed every 250ms, if we have lots of log
|
||||
// messages coming in, then calling an setState 200+ times is actually
|
||||
// pretty expensive
|
||||
batch.push(item);
|
||||
|
||||
if (!queued) {
|
||||
queued = true;
|
||||
|
||||
batchTimer = setTimeout(() => {
|
||||
const thisBatch = batch;
|
||||
batch = [];
|
||||
queued = false;
|
||||
const newState = addEntriesToState(
|
||||
thisBatch,
|
||||
{
|
||||
rows: rows.get(),
|
||||
entries: entries.get(),
|
||||
},
|
||||
timeDirection.get(),
|
||||
);
|
||||
rows.set(newState.rows);
|
||||
entries.set(newState.entries);
|
||||
}, 100);
|
||||
});
|
||||
} else {
|
||||
logDisposer?.();
|
||||
isPaused.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
// Non public Android specific api
|
||||
(client.device.realDevice as any)?.clearLogs?.();
|
||||
entries.set([]);
|
||||
rows.set([]);
|
||||
highlightedRows.set(new Set());
|
||||
counters.update((counters) => {
|
||||
for (const counter of counters) {
|
||||
counter.count = 0;
|
||||
}
|
||||
});
|
||||
rows.clear();
|
||||
tableManagerRef.current?.clearSelection();
|
||||
}
|
||||
|
||||
function createPaste() {
|
||||
let paste = '';
|
||||
const mapFn = (row: TableBodyRow) =>
|
||||
Object.keys(COLUMNS)
|
||||
.map((key) => textContent(row.columns[key].value))
|
||||
.join('\t');
|
||||
|
||||
if (highlightedRows.get().size > 0) {
|
||||
// create paste from selection
|
||||
paste = rows
|
||||
.get()
|
||||
.filter((row) => highlightedRows.get().has(row.key))
|
||||
.map(mapFn)
|
||||
.join('\n');
|
||||
} else {
|
||||
// create paste with all rows
|
||||
paste = rows.get().map(mapFn).join('\n');
|
||||
let selection = tableManagerRef.current?.getSelectedItems();
|
||||
if (!selection?.length) {
|
||||
selection = rows.view.output(0, rows.view.size);
|
||||
}
|
||||
if (selection?.length) {
|
||||
client.createPaste(JSON.stringify(selection, null, 2));
|
||||
}
|
||||
client.createPaste(paste);
|
||||
}
|
||||
|
||||
function goToBottom() {
|
||||
tableRef.current?.scrollToBottom();
|
||||
tableManagerRef?.current?.selectItem(rows.view.size - 1);
|
||||
}
|
||||
|
||||
// start listening to the logs
|
||||
resumePause();
|
||||
|
||||
return {
|
||||
isConnected: client.device.isConnected,
|
||||
isPaused,
|
||||
tableManagerRef,
|
||||
rows,
|
||||
highlightedRows,
|
||||
counters,
|
||||
isDeeplinked,
|
||||
tableRef,
|
||||
onRowHighlighted(selectedRows: Array<string>) {
|
||||
highlightedRows.set(new Set(selectedRows));
|
||||
},
|
||||
clearLogs,
|
||||
onSort(order: TableRowSortOrder) {
|
||||
rows.set(rows.get().slice().reverse());
|
||||
timeDirection.set(order.direction);
|
||||
},
|
||||
updateCounters(newCounters: readonly Counter[]) {
|
||||
counters.set(newCounters);
|
||||
// TODO: make local storage abstraction T69989583
|
||||
window.localStorage.setItem(
|
||||
LOG_WATCHER_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(newCounters),
|
||||
);
|
||||
},
|
||||
resumePause,
|
||||
};
|
||||
}
|
||||
|
||||
const DeviceLogsContextMenu = styled(ContextMenu)({
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export function Component() {
|
||||
const plugin = usePlugin(devicePlugin);
|
||||
const rows = useValue(plugin.rows);
|
||||
const highlightedRows = useValue(plugin.highlightedRows);
|
||||
const isDeeplinked = useValue(plugin.isDeeplinked);
|
||||
|
||||
const buildContextMenuItems = useCallback(
|
||||
(): MenuTemplate => [
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: 'Clear all',
|
||||
click: plugin.clearLogs,
|
||||
},
|
||||
],
|
||||
[plugin.clearLogs],
|
||||
);
|
||||
|
||||
const paused = useValue(plugin.isPaused);
|
||||
return (
|
||||
<DeviceLogsContextMenu
|
||||
buildItems={buildContextMenuItems}
|
||||
component={FlexColumn}>
|
||||
<SearchableTable
|
||||
innerRef={plugin.tableRef}
|
||||
floating={false}
|
||||
multiline={true}
|
||||
columnSizes={COLUMN_SIZE}
|
||||
columnOrder={COLUMN_ORDER}
|
||||
columns={COLUMNS}
|
||||
rows={rows}
|
||||
highlightedRows={highlightedRows}
|
||||
onRowHighlighted={plugin.onRowHighlighted}
|
||||
multiHighlight={true}
|
||||
defaultFilters={DEFAULT_FILTERS}
|
||||
zebra={false}
|
||||
actions={<Button onClick={plugin.clearLogs}>Clear Logs</Button>}
|
||||
allowRegexSearch={true}
|
||||
// If the logs is opened through deeplink, then don't scroll as the row is highlighted
|
||||
stickyBottom={!(isDeeplinked && highlightedRows.size > 0)}
|
||||
initialSortOrder={{key: 'time', direction: 'up'}}
|
||||
onSort={plugin.onSort}
|
||||
/>
|
||||
<DetailSidebar>
|
||||
<Sidebar />
|
||||
</DetailSidebar>
|
||||
</DeviceLogsContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar() {
|
||||
const plugin = usePlugin(devicePlugin);
|
||||
const counters = useValue(plugin.counters);
|
||||
return (
|
||||
<LogWatcher
|
||||
counters={counters}
|
||||
onChange={(counters) => {
|
||||
plugin.updateCounters(counters);
|
||||
}}
|
||||
<DataTable<ExtendedLogEntry>
|
||||
dataSource={plugin.rows}
|
||||
columns={columns}
|
||||
autoScroll
|
||||
onRowStyle={getRowStyle}
|
||||
extraActions={
|
||||
plugin.isConnected ? (
|
||||
<>
|
||||
<Button
|
||||
title={`Click to ${paused ? 'resume' : 'pause'} the log stream`}
|
||||
danger={paused}
|
||||
onClick={plugin.resumePause}>
|
||||
{paused ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
|
||||
</Button>
|
||||
<Button title="Clear logs" onClick={plugin.clearLogs}>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
tableManagerRef={plugin.tableManagerRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {Text, colors, Glyph, styled} from 'flipper';
|
||||
|
||||
export const Icon = styled(Glyph)({
|
||||
marginTop: 5,
|
||||
});
|
||||
|
||||
export const HiddenScrollText = styled(Text)({
|
||||
userSelect: 'none',
|
||||
alignSelf: 'baseline',
|
||||
lineHeight: '130%',
|
||||
marginTop: 5,
|
||||
paddingBottom: 3,
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
export const LogCount = styled.div<{backgroundColor: string}>(
|
||||
({backgroundColor}) => ({
|
||||
backgroundColor,
|
||||
borderRadius: '999em',
|
||||
fontSize: 11,
|
||||
marginTop: 4,
|
||||
minWidth: 16,
|
||||
height: 16,
|
||||
color: colors.white,
|
||||
textAlign: 'center',
|
||||
lineHeight: '16px',
|
||||
paddingLeft: 4,
|
||||
paddingRight: 4,
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
}),
|
||||
);
|
||||
78
desktop/plugins/logs/logTypes.tsx
Normal file
78
desktop/plugins/logs/logTypes.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {theme} from 'flipper-plugin';
|
||||
import {WarningFilled, CloseCircleFilled} from '@ant-design/icons';
|
||||
import React, {CSSProperties} from 'react';
|
||||
|
||||
const iconStyle = {
|
||||
fontSize: '16px',
|
||||
};
|
||||
|
||||
export const baseRowStyle = {
|
||||
...theme.monospace,
|
||||
};
|
||||
|
||||
export const logTypes: {
|
||||
[level: string]: {
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
style?: CSSProperties;
|
||||
enabled: boolean;
|
||||
};
|
||||
} = {
|
||||
verbose: {
|
||||
label: 'Verbose',
|
||||
style: {
|
||||
...baseRowStyle,
|
||||
color: theme.textColorSecondary,
|
||||
},
|
||||
enabled: false,
|
||||
},
|
||||
debug: {
|
||||
label: 'Debug',
|
||||
style: {
|
||||
...baseRowStyle,
|
||||
color: theme.textColorSecondary,
|
||||
},
|
||||
enabled: false,
|
||||
},
|
||||
info: {
|
||||
label: 'Info',
|
||||
enabled: true,
|
||||
},
|
||||
warn: {
|
||||
label: 'Warn',
|
||||
style: {
|
||||
...baseRowStyle,
|
||||
color: theme.warningColor,
|
||||
},
|
||||
icon: <WarningFilled style={iconStyle} />,
|
||||
enabled: true,
|
||||
},
|
||||
error: {
|
||||
label: 'Error',
|
||||
style: {
|
||||
...baseRowStyle,
|
||||
color: theme.errorColor,
|
||||
},
|
||||
icon: <CloseCircleFilled style={iconStyle} />,
|
||||
enabled: true,
|
||||
},
|
||||
fatal: {
|
||||
label: 'Fatal',
|
||||
style: {
|
||||
...baseRowStyle,
|
||||
background: theme.errorColor,
|
||||
color: theme.white,
|
||||
},
|
||||
icon: <CloseCircleFilled style={iconStyle} />,
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
export function getLineCount(str: string): number {
|
||||
let count = 1;
|
||||
if (!(typeof str === 'string')) {
|
||||
return 0;
|
||||
}
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (str[i] === '\n') {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export function pad(chunk: any, len: number): string {
|
||||
let str = String(chunk);
|
||||
while (str.length < len) {
|
||||
str = `0${str}`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
Reference in New Issue
Block a user