Files
flipper/desktop/plugins/leak_canary/index.tsx
Michel Weststrate afa2c6322a make lint error on missing keys
Summary: When exploratory testing Flipper, I generally see quite some React key warnings. So it seems that plugin devs often miss them. This diff will configure linting more aggressively to address that (it's not fool proof, but will find the most common cases).

Reviewed By: nikoant

Differential Revision: D26722707

fbshipit-source-id: e0d2b56de2422e1147f52c8e9150d00c7ee64bd2
2021-03-02 01:17:13 -08:00

276 lines
7.0 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 React from 'react';
import {
Panel,
FlexRow,
ElementsInspector,
FlexColumn,
ManagedDataInspector,
Sidebar,
Toolbar,
Checkbox,
FlipperPlugin,
Button,
styled,
} from 'flipper';
import {Element} from 'flipper';
import {processLeaks} from './processLeakString';
type State = {
leaks: Leak[];
selectedIdx: number | null;
selectedEid: string | null;
showFullClassPaths: boolean;
leaksCount: number;
};
type LeakReport = {
leaks: string[];
};
type LeakCanary2Report = {
leaks: Leak2[];
};
export type Fields = {[key: string]: string};
export type Leak = {
title: string;
root: string;
elements: {[key: string]: Element};
elementsSimple: {[key: string]: Element};
instanceFields: {[key: string]: Fields};
staticFields: {[key: string]: Fields};
retainedSize: string;
details?: string;
};
export type Leak2 = {
title: string;
root: string;
elements: {[key: string]: Element};
retainedSize: string;
details: string;
};
const Window = styled(FlexRow)({
height: '100%',
flex: 1,
});
const ToolbarItem = styled(FlexRow)({
alignItems: 'center',
marginLeft: '8px',
});
export default class LeakCanary<PersistedState> extends FlipperPlugin<
State,
{type: 'LeakCanary'},
PersistedState
> {
state: State = {
leaks: [],
selectedIdx: null,
selectedEid: null,
showFullClassPaths: false,
leaksCount: 0,
};
init() {
this.client.subscribe('reportLeak', (results: LeakReport) => {
this._addNewLeaks(processLeaks(results.leaks));
});
this.client.subscribe('reportLeak2', (results: LeakCanary2Report) => {
this._addNewLeaks(results.leaks.map(this._adaptLeak2));
});
}
_addNewLeaks = (incomingLeaks: Leak[]) => {
// We only process new leaks instead of replacing the whole list in order
// to both avoid redundant processing and to preserve the expanded/
// collapsed state of the tree view
const newLeaks = incomingLeaks.slice(this.state.leaksCount);
const leaks = this.state.leaks;
for (let i = 0; i < newLeaks.length; i++) {
leaks.push(newLeaks[i]);
}
this.setState({
leaks: leaks,
leaksCount: leaks.length,
});
};
_adaptLeak2 = (leak: Leak2): Leak => {
return {
title: leak.title,
root: leak.root,
elements: leak.elements,
elementsSimple: leak.elements,
staticFields: {},
instanceFields: {},
retainedSize: leak.retainedSize,
details: leak.details,
};
};
_clearLeaks = () => {
this.setState({
leaks: [],
leaksCount: 0,
selectedIdx: null,
selectedEid: null,
});
this.client.call('clear');
};
_selectElement = (leakIdx: number, eid: string) => {
this.setState({
selectedIdx: leakIdx,
selectedEid: eid,
});
};
_toggleElement = (leakIdx: number, eid: string) => {
const {leaks} = this.state;
const leak = leaks[leakIdx];
const element = leak.elements[eid];
const elementSimple = leak.elementsSimple[eid];
if (!element || !elementSimple) {
return;
}
element.expanded = !element.expanded;
elementSimple.expanded = !elementSimple.expanded;
this.setState({
leaks: leaks,
});
};
/**
* Given a specific string value, determines what DataInspector type to treat
* it as. Ensures that numbers, bools, etc render correctly.
*/
_extractValue(
value: any,
_: number, // depth
): {mutable: boolean; type: string; value: any} {
if (!isNaN(value)) {
return {mutable: false, type: 'number', value: value};
} else if (value == 'true' || value == 'false') {
return {mutable: false, type: 'boolean', value: value};
} else if (value == 'null') {
return {mutable: false, type: 'null', value: value};
}
return {mutable: false, type: 'enum', value: value};
}
renderSidebar() {
const {selectedIdx, selectedEid, leaks} = this.state;
if (selectedIdx == null || selectedEid == null) {
return null;
}
const leak = leaks[selectedIdx];
const staticFields = leak.staticFields[selectedEid];
const instanceFields = leak.instanceFields[selectedEid];
return (
<Sidebar position="right" width={600} minWidth={300} maxWidth={900}>
{instanceFields && (
<Panel heading={'Instance'} floating={false} grow={false}>
<ManagedDataInspector
data={instanceFields}
expandRoot={true}
extractValue={this._extractValue}
/>
</Panel>
)}
{staticFields && (
<Panel heading={'Static'} floating={false} grow={false}>
<ManagedDataInspector
data={staticFields}
expandRoot={true}
extractValue={this._extractValue}
/>
</Panel>
)}
{leak.details && (
<Panel heading={'Details'} floating={false} grow={false}>
<pre>{leak.details}</pre>
</Panel>
)}
</Sidebar>
);
}
render() {
const {selectedIdx, selectedEid, showFullClassPaths} = this.state;
const sidebar = this.renderSidebar();
return (
<Window>
<FlexColumn grow={true}>
<FlexColumn grow={true} scrollable={true}>
{this.state.leaks.map((leak: Leak, idx: number) => {
const elements = showFullClassPaths
? leak.elements
: leak.elementsSimple;
const selected = selectedIdx == idx ? selectedEid : null;
return (
<Panel
key={idx}
collapsable={false}
padded={false}
heading={leak.title}
floating={false}
accessory={leak.retainedSize}>
<ElementsInspector
onElementSelected={(eid) => {
this._selectElement(idx, eid);
}}
onElementHovered={() => {}}
onElementExpanded={(eid /*, deep*/) => {
this._toggleElement(idx, eid);
}}
selected={selected}
searchResults={null}
root={leak.root}
elements={elements}
/>
</Panel>
);
})}
</FlexColumn>
<Toolbar>
<ToolbarItem>
<Button onClick={this._clearLeaks}>Clear</Button>
</ToolbarItem>
<ToolbarItem>
<Checkbox
checked={showFullClassPaths}
onChange={(checked: boolean) => {
this.setState({showFullClassPaths: checked});
}}
/>
Show full class path
</ToolbarItem>
</Toolbar>
</FlexColumn>
{sidebar}
</Window>
);
}
}