Move plugins to "sonar/desktop/plugins"
Summary: Plugins moved from "sonar/desktop/src/plugins" to "sonar/desktop/plugins". Fixed all the paths after moving. New "desktop" folder structure: - `src` - Flipper desktop app JS code executing in Electron Renderer (Chrome) process. - `static` - Flipper desktop app JS code executing in Electron Main (Node.js) process. - `plugins` - Flipper desktop JS plugins. - `pkg` - Flipper packaging lib and CLI tool. - `doctor` - Flipper diagnostics lib and CLI tool. - `scripts` - Build scripts for Flipper desktop app. - `headless` - Headless version of Flipper desktop app. - `headless-tests` - Integration tests running agains Flipper headless version. Reviewed By: mweststrate Differential Revision: D20344186 fbshipit-source-id: d020da970b2ea1e001f9061a8782bfeb54e31ba0
This commit is contained in:
committed by
Facebook GitHub Bot
parent
beb5c85e69
commit
10d990c32c
228
desktop/plugins/logs/LogWatcher.tsx
Normal file
228
desktop/plugins/logs/LogWatcher.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* 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.state.highlightedRow != null
|
||||
) {
|
||||
this.props.onChange(
|
||||
this.props.counters.filter(
|
||||
({label}) => label !== this.state.highlightedRow,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.onAdd();
|
||||
}
|
||||
};
|
||||
|
||||
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}
|
||||
/>
|
||||
</WatcherPanel>
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
}
|
||||
60
desktop/plugins/logs/__tests__/index.node.js
Normal file
60
desktop/plugins/logs/__tests__/index.node.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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 {addEntriesToState, processEntry} from '../index.tsx';
|
||||
|
||||
const entry = {
|
||||
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 currentState = addEntriesToState([processEntry(entry, 'key1')]);
|
||||
const processedEntry = processEntry(
|
||||
{
|
||||
...entry,
|
||||
message: 'new message',
|
||||
},
|
||||
'key2',
|
||||
);
|
||||
const newState = addEntriesToState([processedEntry], currentState);
|
||||
expect(newState.rows.length).toBe(2);
|
||||
expect(newState.entries.length).toBe(2);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
667
desktop/plugins/logs/index.tsx
Normal file
667
desktop/plugins/logs/index.tsx
Normal file
@@ -0,0 +1,667 @@
|
||||
/**
|
||||
* 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,
|
||||
TableColumnOrder,
|
||||
TableColumnSizes,
|
||||
TableColumns,
|
||||
Props as PluginProps,
|
||||
BaseAction,
|
||||
DeviceLogEntry,
|
||||
} from 'flipper';
|
||||
import {Counter} from './LogWatcher';
|
||||
|
||||
import {
|
||||
Text,
|
||||
ManagedTableClass,
|
||||
Button,
|
||||
colors,
|
||||
ContextMenu,
|
||||
FlexColumn,
|
||||
Glyph,
|
||||
DetailSidebar,
|
||||
FlipperDevicePlugin,
|
||||
SearchableTable,
|
||||
styled,
|
||||
Device,
|
||||
createPaste,
|
||||
textContent,
|
||||
KeyboardActions,
|
||||
} from 'flipper';
|
||||
import LogWatcher from './LogWatcher';
|
||||
import React from 'react';
|
||||
import {MenuTemplate} from 'src/ui/components/ContextMenu';
|
||||
|
||||
const LOG_WATCHER_LOCAL_STORAGE_KEY = 'LOG_WATCHER_LOCAL_STORAGE_KEY';
|
||||
|
||||
type Entries = ReadonlyArray<{
|
||||
readonly row: TableBodyRow;
|
||||
readonly entry: DeviceLogEntry;
|
||||
}>;
|
||||
|
||||
type BaseState = {
|
||||
readonly rows: ReadonlyArray<TableBodyRow>;
|
||||
readonly entries: Entries;
|
||||
readonly key2entry: {readonly [key: string]: DeviceLogEntry};
|
||||
};
|
||||
|
||||
type AdditionalState = {
|
||||
readonly highlightedRows: ReadonlySet<string>;
|
||||
readonly counters: ReadonlyArray<Counter>;
|
||||
};
|
||||
|
||||
type State = BaseState & AdditionalState;
|
||||
|
||||
type PersistedState = {};
|
||||
|
||||
const Icon = styled(Glyph)({
|
||||
marginTop: 5,
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function keepKeys<A>(obj: A, keys: Array<string>): A {
|
||||
const result: A = {} as A;
|
||||
for (const key in obj) {
|
||||
if (keys.includes(key)) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
pid: {
|
||||
value: 'PID',
|
||||
},
|
||||
tid: {
|
||||
value: 'TID',
|
||||
},
|
||||
tag: {
|
||||
value: 'Tag',
|
||||
},
|
||||
app: {
|
||||
value: 'App',
|
||||
},
|
||||
message: {
|
||||
value: 'Message',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const INITIAL_COLUMN_ORDER = [
|
||||
{
|
||||
key: 'type',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
key: 'time',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
key: 'pid',
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
key: 'tid',
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
key: 'tag',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
key: 'app',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
const HiddenScrollText = styled(Text)({
|
||||
alignSelf: 'baseline',
|
||||
userSelect: 'none',
|
||||
lineHeight: '130%',
|
||||
marginTop: 5,
|
||||
paddingBottom: 3,
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
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',
|
||||
}));
|
||||
|
||||
function pad(chunk: any, len: number): string {
|
||||
let str = String(chunk);
|
||||
while (str.length < len) {
|
||||
str = `0${str}`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export function addEntriesToState(
|
||||
items: Entries,
|
||||
state: BaseState = {
|
||||
rows: [],
|
||||
entries: [],
|
||||
key2entry: {},
|
||||
} as const,
|
||||
): BaseState {
|
||||
const rows = [...state.rows];
|
||||
const entries = [...state.entries];
|
||||
const key2entry = {...state.key2entry};
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const {entry, row} = items[i];
|
||||
entries.push({row, entry});
|
||||
key2entry[row.key] = 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);
|
||||
}
|
||||
|
||||
return {
|
||||
entries,
|
||||
rows,
|
||||
key2entry,
|
||||
};
|
||||
}
|
||||
|
||||
export function addRowIfNeeded(
|
||||
rows: Array<TableBodyRow>,
|
||||
row: TableBodyRow,
|
||||
entry: DeviceLogEntry,
|
||||
previousEntry: DeviceLogEntry | null,
|
||||
) {
|
||||
const previousRow = rows.length > 0 ? rows[rows.length - 1] : 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 {
|
||||
rows.push(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 default class LogTable extends FlipperDevicePlugin<
|
||||
State,
|
||||
BaseAction,
|
||||
PersistedState
|
||||
> {
|
||||
static keyboardActions: KeyboardActions = [
|
||||
'clear',
|
||||
'goToBottom',
|
||||
'createPaste',
|
||||
];
|
||||
|
||||
batchTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
static supportsDevice(device: Device) {
|
||||
return (
|
||||
device.os === 'iOS' || device.os === 'Android' || device.os === 'Metro'
|
||||
);
|
||||
}
|
||||
|
||||
onKeyboardAction = (action: string) => {
|
||||
if (action === 'clear') {
|
||||
this.clearLogs();
|
||||
} else if (action === 'goToBottom') {
|
||||
this.goToBottom();
|
||||
} else if (action === 'createPaste') {
|
||||
this.createPaste();
|
||||
}
|
||||
};
|
||||
|
||||
restoreSavedCounters = (): Array<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,
|
||||
}));
|
||||
};
|
||||
|
||||
calculateHighlightedRows = (
|
||||
deepLinkPayload: string | null,
|
||||
rows: ReadonlyArray<TableBodyRow>,
|
||||
): Set<string> => {
|
||||
const highlightedRows = new Set<string>();
|
||||
if (!deepLinkPayload) {
|
||||
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;
|
||||
};
|
||||
|
||||
tableRef: ManagedTableClass | undefined;
|
||||
columns: TableColumns;
|
||||
columnSizes: TableColumnSizes;
|
||||
columnOrder: TableColumnOrder;
|
||||
logListener: Symbol | undefined;
|
||||
|
||||
batch: Array<{
|
||||
readonly row: TableBodyRow;
|
||||
readonly entry: DeviceLogEntry;
|
||||
}> = [];
|
||||
queued: boolean = false;
|
||||
counter: number = 0;
|
||||
|
||||
constructor(props: PluginProps<PersistedState>) {
|
||||
super(props);
|
||||
const supportedColumns = this.device.supportedColumns();
|
||||
this.columns = keepKeys(COLUMNS, supportedColumns);
|
||||
this.columnSizes = keepKeys(COLUMN_SIZE, supportedColumns);
|
||||
this.columnOrder = INITIAL_COLUMN_ORDER.filter(obj =>
|
||||
supportedColumns.includes(obj.key),
|
||||
);
|
||||
|
||||
const initialState = addEntriesToState(
|
||||
this.device
|
||||
.getLogs()
|
||||
.map(log => processEntry(log, String(this.counter++))),
|
||||
this.state,
|
||||
);
|
||||
this.state = {
|
||||
...initialState,
|
||||
highlightedRows: this.calculateHighlightedRows(
|
||||
props.deepLinkPayload,
|
||||
initialState.rows,
|
||||
),
|
||||
counters: this.restoreSavedCounters(),
|
||||
};
|
||||
|
||||
this.logListener = this.device.addLogListener((entry: DeviceLogEntry) => {
|
||||
const processedEntry = processEntry(entry, String(this.counter++));
|
||||
this.incrementCounterIfNeeded(processedEntry.entry);
|
||||
this.scheduleEntryForBatch(processedEntry);
|
||||
});
|
||||
}
|
||||
|
||||
incrementCounterIfNeeded = (entry: DeviceLogEntry) => {
|
||||
let counterUpdated = false;
|
||||
const counters = this.state.counters.map(counter => {
|
||||
if (entry.message.match(counter.expression)) {
|
||||
counterUpdated = true;
|
||||
if (counter.notify) {
|
||||
new Notification(`${counter.label}`, {
|
||||
body: 'The watched log message appeared',
|
||||
});
|
||||
}
|
||||
return {
|
||||
...counter,
|
||||
count: counter.count + 1,
|
||||
};
|
||||
} else {
|
||||
return counter;
|
||||
}
|
||||
});
|
||||
if (counterUpdated) {
|
||||
this.setState({counters});
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
this.batch.push(item);
|
||||
|
||||
if (!this.queued) {
|
||||
this.queued = true;
|
||||
|
||||
this.batchTimer = setTimeout(() => {
|
||||
const thisBatch = this.batch;
|
||||
this.batch = [];
|
||||
this.queued = false;
|
||||
this.setState(state => addEntriesToState(thisBatch, state));
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.batchTimer) {
|
||||
clearTimeout(this.batchTimer);
|
||||
}
|
||||
|
||||
if (this.logListener) {
|
||||
this.device.removeLogListener(this.logListener);
|
||||
}
|
||||
}
|
||||
|
||||
clearLogs = () => {
|
||||
this.device.clearLogs().catch(e => {
|
||||
console.error('Failed to clear logs: ', e);
|
||||
});
|
||||
this.setState({
|
||||
entries: [],
|
||||
rows: [],
|
||||
highlightedRows: new Set(),
|
||||
key2entry: {},
|
||||
counters: this.state.counters.map(counter => ({
|
||||
...counter,
|
||||
count: 0,
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
createPaste = () => {
|
||||
let paste = '';
|
||||
const mapFn = (row: TableBodyRow) =>
|
||||
Object.keys(COLUMNS)
|
||||
.map(key => textContent(row.columns[key].value))
|
||||
.join('\t');
|
||||
|
||||
if (this.state.highlightedRows.size > 0) {
|
||||
// create paste from selection
|
||||
paste = this.state.rows
|
||||
.filter(row => this.state.highlightedRows.has(row.key))
|
||||
.map(mapFn)
|
||||
.join('\n');
|
||||
} else {
|
||||
// create paste with all rows
|
||||
paste = this.state.rows.map(mapFn).join('\n');
|
||||
}
|
||||
createPaste(paste);
|
||||
};
|
||||
|
||||
setTableRef = (ref: ManagedTableClass) => {
|
||||
this.tableRef = ref;
|
||||
};
|
||||
|
||||
goToBottom = () => {
|
||||
if (this.tableRef != null) {
|
||||
this.tableRef.scrollToBottom();
|
||||
}
|
||||
};
|
||||
|
||||
onRowHighlighted = (highlightedRows: Array<string>) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
highlightedRows: new Set(highlightedRows),
|
||||
});
|
||||
};
|
||||
|
||||
renderSidebar = () => {
|
||||
return (
|
||||
<LogWatcher
|
||||
counters={this.state.counters}
|
||||
onChange={counters =>
|
||||
this.setState({counters}, () =>
|
||||
window.localStorage.setItem(
|
||||
LOG_WATCHER_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(this.state.counters),
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
static ContextMenu = styled(ContextMenu)({
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
buildContextMenuItems: () => MenuTemplate = () => [
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: 'Clear all',
|
||||
click: this.clearLogs,
|
||||
},
|
||||
];
|
||||
|
||||
render() {
|
||||
return (
|
||||
<LogTable.ContextMenu
|
||||
buildItems={this.buildContextMenuItems}
|
||||
component={FlexColumn}>
|
||||
<SearchableTable
|
||||
innerRef={this.setTableRef}
|
||||
floating={false}
|
||||
multiline={true}
|
||||
columnSizes={this.columnSizes}
|
||||
columnOrder={this.columnOrder}
|
||||
columns={this.columns}
|
||||
rows={this.state.rows}
|
||||
highlightedRows={this.state.highlightedRows}
|
||||
onRowHighlighted={this.onRowHighlighted}
|
||||
multiHighlight={true}
|
||||
defaultFilters={DEFAULT_FILTERS}
|
||||
zebra={false}
|
||||
actions={<Button onClick={this.clearLogs}>Clear Logs</Button>}
|
||||
allowRegexSearch={true}
|
||||
// If the logs is opened through deeplink, then don't scroll as the row is highlighted
|
||||
stickyBottom={
|
||||
!(this.props.deepLinkPayload && this.state.highlightedRows.size > 0)
|
||||
}
|
||||
/>
|
||||
<DetailSidebar>{this.renderSidebar()}</DetailSidebar>
|
||||
</LogTable.ContextMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
14
desktop/plugins/logs/package.json
Normal file
14
desktop/plugins/logs/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "DeviceLogs",
|
||||
"version": "1.0.0",
|
||||
"main": "index.tsx",
|
||||
"license": "MIT",
|
||||
"keywords": ["flipper-plugin"],
|
||||
"dependencies": {},
|
||||
"title": "Logs",
|
||||
"icon": "arrow-right",
|
||||
"bugs": {
|
||||
"email": "oncall+flipper@xmail.facebook.com",
|
||||
"url": "https://fb.workplace.com/groups/flippersupport/"
|
||||
}
|
||||
}
|
||||
4
desktop/plugins/logs/yarn.lock
Normal file
4
desktop/plugins/logs/yarn.lock
Normal file
@@ -0,0 +1,4 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user