Files
flipper/desktop/plugins/logs/index.tsx
Michel Weststrate 9d3c48fd84 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
2021-03-16 15:03:49 -07:00

239 lines
5.4 KiB
TypeScript

/**
* 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 {
DevicePluginClient,
DeviceLogEntry,
usePlugin,
createDataSource,
DataTable,
DataTableColumn,
theme,
DataTableManager,
createState,
useValue,
DataFormatter,
} from 'flipper-plugin';
import {
PlayCircleOutlined,
PauseCircleOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import React, {createRef, CSSProperties} from 'react';
import {Badge, Button} from 'antd';
import {baseRowStyle, logTypes} from './logTypes';
export type ExtendedLogEntry = DeviceLogEntry & {
count: number;
};
const columns: DataTableColumn<ExtendedLogEntry>[] = [
{
key: 'type',
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: 'date',
title: 'Time',
width: 120,
},
{
key: 'pid',
title: 'PID',
width: 60,
visible: false,
},
{
key: 'tid',
title: 'TID',
width: 60,
visible: false,
},
{
key: 'tag',
title: 'Tag',
width: 160,
},
{
key: 'app',
title: 'App',
width: 160,
visible: false,
},
{
key: 'message',
title: 'Message',
wrap: true,
formatters: [DataFormatter.prettyPrintJson, DataFormatter.linkify],
},
];
function getRowStyle(entry: DeviceLogEntry): CSSProperties | undefined {
return (logTypes[entry.type]?.style as any) ?? baseRowStyle;
}
export function devicePlugin(client: DevicePluginClient) {
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') {
// 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);
}
});
client.addMenuEntry(
{
action: 'clear',
handler: clearLogs,
},
{
action: 'createPaste',
handler: createPaste,
},
{
action: 'goToBottom',
handler: goToBottom,
},
);
let logDisposer: (() => void) | undefined;
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,
});
}
});
} else {
logDisposer?.();
isPaused.set(true);
}
}
function clearLogs() {
// Non public Android specific api
(client.device.realDevice as any)?.clearLogs?.();
rows.clear();
tableManagerRef.current?.clearSelection();
}
function createPaste() {
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));
}
}
function goToBottom() {
tableManagerRef?.current?.selectItem(rows.view.size - 1);
}
// start listening to the logs
resumePause();
return {
isConnected: client.device.isConnected,
isPaused,
tableManagerRef,
rows,
clearLogs,
resumePause,
};
}
export function Component() {
const plugin = usePlugin(devicePlugin);
const paused = useValue(plugin.isPaused);
return (
<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}
/>
);
}