Files
flipper/desktop/plugins/public/logs/index.tsx
Michel Weststrate 3882357579 Factor out realDevice [7/n]
Summary: `device.realDevice` was the escape hatch used in Sandy plugins to give access to device specific features like taking screenshots, clearing logs or accessing `adb`. Since in decapitated Flipper that won't be possible anymore (since plugins run in the client but device implementations on the server), all escape hatches have been bridged in this stack, and we can get of the `realDevice` interaction, by explicitly exposing those cases, which makes it type safe as well.

Reviewed By: passy

Differential Revision: D31079509

fbshipit-source-id: c9ec2e044d0dec0ccb1de287cf424907b198f818
2021-09-22 09:03:33 -07:00

249 lines
5.7 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;
};
function createColumnConfig(
_os: 'iOS' | 'Android' | 'Metro',
): DataTableColumn<ExtendedLogEntry>[] {
return [
{
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: true,
},
{
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.truncate(400),
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.onDeviceLogEntry((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);
}
}
async function clearLogs() {
await client.device.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();
const columns = createColumnConfig(client.device.os as any);
return {
columns,
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={plugin.columns}
enableAutoScroll
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}
/>
);
}