Move DataInspector to flipper-plugin

Summary:
This diff moves the rest of the DataInspector jungle to flipper-plugin. No actual improvements are made yet, but the code is decoupled from Electron and the legacy theming. For example by using Antd based context menus.

Note that `ManagedDataInspector` is now rebranded `DataInspector`, as that is the only variation that should (and is) used publicly.

For the interactionTracker removal, see next diff.

SearchableDataInspector will be addressed in a next diff

Reviewed By: passy

Differential Revision: D27603125

fbshipit-source-id: 188bd000260e4e4704202ce02c7fc98319f0bc22
This commit is contained in:
Michel Weststrate
2021-04-07 07:52:47 -07:00
committed by Facebook GitHub Bot
parent 9030a98c6e
commit 53c557f923
31 changed files with 221 additions and 242 deletions

View File

@@ -72,7 +72,6 @@
"rsocket-types": "^0.0.25", "rsocket-types": "^0.0.25",
"semver": "^7.3.5", "semver": "^7.3.5",
"split2": "^3.2.2", "split2": "^3.2.2",
"string-natural-compare": "^3.0.0",
"tmp": "^0.2.1", "tmp": "^0.2.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"which": "^2.0.1", "which": "^2.0.1",

View File

@@ -95,15 +95,14 @@ export {
export { export {
DataValueExtractor, DataValueExtractor,
DataInspectorExpanded, DataInspectorExpanded,
} from './ui/components/data-inspector/DataInspector';
export {default as DataInspector} from './ui/components/data-inspector/DataInspector';
export {default as ManagedDataInspector} from './ui/components/data-inspector/ManagedDataInspector';
export {default as SearchableDataInspector} from './ui/components/data-inspector/SearchableDataInspector';
export {
_DataDescription as DataDescription,
DataDescriptionType, DataDescriptionType,
DataDescription,
DataInspector,
MarkerTimeline,
} from 'flipper-plugin'; } from 'flipper-plugin';
export {_HighlightManager as HighlightManager} from 'flipper-plugin'; export {DataInspector as ManagedDataInspector} from 'flipper-plugin';
export {default as SearchableDataInspector} from './ui/components/data-inspector/SearchableDataInspector';
export {HighlightManager} from 'flipper-plugin';
export {default as Tabs} from './ui/components/Tabs'; export {default as Tabs} from './ui/components/Tabs';
export {default as Tab} from './ui/components/Tab'; export {default as Tab} from './ui/components/Tab';
export {default as Input} from './ui/components/Input'; export {default as Input} from './ui/components/Input';
@@ -160,7 +159,6 @@ export {default as VerticalRule} from './ui/components/VerticalRule';
export {default as Label} from './ui/components/Label'; export {default as Label} from './ui/components/Label';
export {default as Heading} from './ui/components/Heading'; export {default as Heading} from './ui/components/Heading';
export {Filter} from './ui/components/filter/types'; export {Filter} from './ui/components/filter/types';
export {default as MarkerTimeline} from './ui/components/MarkerTimeline';
export {default as StackTrace} from './ui/components/StackTrace'; export {default as StackTrace} from './ui/components/StackTrace';
export { export {
SearchBox, SearchBox,

View File

@@ -7,7 +7,7 @@
* @format * @format
*/ */
import ManagedDataInspector from '../ui/components/data-inspector/ManagedDataInspector'; import {DataInspector} from 'flipper-plugin';
import Panel from '../ui/components/Panel'; import Panel from '../ui/components/Panel';
import {colors} from '../ui/components/colors'; import {colors} from '../ui/components/colors';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
@@ -232,7 +232,7 @@ function renderSidebarSection(
case 'json': case 'json':
return ( return (
<Panel floating={false} heading={section.title} key={index}> <Panel floating={false} heading={section.title} key={index}>
<ManagedDataInspector data={section.content} expandRoot={true} /> <DataInspector data={section.content} expandRoot={true} />
</Panel> </Panel>
); );
case 'toolbar': case 'toolbar':
@@ -240,7 +240,7 @@ function renderSidebarSection(
default: default:
return ( return (
<Panel floating={false} heading={'Details'} key={index}> <Panel floating={false} heading={'Details'} key={index}>
<ManagedDataInspector data={section} expandRoot={true} /> <DataInspector data={section} expandRoot={true} />
</Panel> </Panel>
); );
} }

View File

@@ -39,6 +39,7 @@ type Props<C> = {
* to show menu items. * to show menu items.
* *
* Separators can be added by `{type: 'separator'}` * Separators can be added by `{type: 'separator'}`
* @depreacted https://ant.design/components/dropdown/#components-dropdown-demo-context-menu
*/ */
export default forwardRef(function ContextMenu<C>( export default forwardRef(function ContextMenu<C>(
{items, buildItems, component, children, ...otherProps}: Props<C>, {items, buildItems, component, children, ...otherProps}: Props<C>,

View File

@@ -28,6 +28,7 @@ export const ContextMenuContext = createContext<ContextMenuManager | undefined>(
/** /**
* Flipper's root is already wrapped with this component, so plugins should not * Flipper's root is already wrapped with this component, so plugins should not
* need to use this. ContextMenu is what you probably want to use. * need to use this. ContextMenu is what you probably want to use.
* @deprecated use https://ant.design/components/dropdown/#components-dropdown-demo-context-menu
*/ */
const ContextMenuProvider: React.FC<{}> = memo(function ContextMenuProvider({ const ContextMenuProvider: React.FC<{}> = memo(function ContextMenuProvider({
children, children,

View File

@@ -65,6 +65,9 @@ type Props = {
forceOpts?: Opts; forceOpts?: Opts;
}; };
/**
* @deprecated use Popover from antd
*/
export default class Popover extends PureComponent<Props> { export default class Popover extends PureComponent<Props> {
_ref?: Element | null; _ref?: Element | null;

View File

@@ -16,6 +16,7 @@ import {PopoverContext} from './PopoverProvider';
* UI framework. * UI framework.
* I don't recommend using this, as it will likely be removed in future. * I don't recommend using this, as it will likely be removed in future.
* Must be nested under a PopoverProvider at some level, usually it is at the top level app so you shouldn't need to add it. * Must be nested under a PopoverProvider at some level, usually it is at the top level app so you shouldn't need to add it.
* @deprecated use Popover from Antd
*/ */
export default function Popover2(props: { export default function Popover2(props: {
id: string; id: string;

View File

@@ -13,7 +13,7 @@ import {colors} from './colors';
import ManagedTable from './table/ManagedTable'; import ManagedTable from './table/ManagedTable';
import FlexColumn from './FlexColumn'; import FlexColumn from './FlexColumn';
import Text from './Text'; import Text from './Text';
import ManagedDataInspector from './data-inspector/ManagedDataInspector'; import {DataInspector} from 'flipper-plugin';
import Input from './Input'; import Input from './Input';
import View from './View'; import View from './View';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
@@ -160,7 +160,7 @@ export class Console extends Component<Props, State> {
columns: { columns: {
command: { command: {
value: result.isSuccess ? ( value: result.isSuccess ? (
<ManagedDataInspector <DataInspector
data={result.value} data={result.value}
expandRoot={true} expandRoot={true}
collapsed={true} collapsed={true}

View File

@@ -10,11 +10,11 @@
import React from 'react'; import React from 'react';
import debounceRender from 'react-debounce-render'; import debounceRender from 'react-debounce-render';
import ManagedDataInspector, { import {DataInspector} from 'flipper-plugin';
ManagedDataInspectorProps,
} from './ManagedDataInspector';
import Searchable, {SearchableProps} from '../searchable/Searchable'; import Searchable, {SearchableProps} from '../searchable/Searchable';
type ManagedDataInspectorProps = any; // TODO!
type Props = ManagedDataInspectorProps & SearchableProps; type Props = ManagedDataInspectorProps & SearchableProps;
function filter(data: any, searchTerm: string): any { function filter(data: any, searchTerm: string): any {
@@ -35,10 +35,7 @@ function filter(data: any, searchTerm: string): any {
// Naive shallow filters matching wrapper for ManagedDataInspector // Naive shallow filters matching wrapper for ManagedDataInspector
function SearchableDataInspector(props: Props) { function SearchableDataInspector(props: Props) {
return ( return (
<ManagedDataInspector <DataInspector {...props} data={filter(props.data, props.searchTerm)} />
{...props}
data={filter(props.data, props.searchTerm)}
/>
); );
} }

View File

@@ -12,7 +12,7 @@ import {PluginClient} from '../../../plugin';
import Client from '../../../Client'; import Client from '../../../Client';
import {Logger} from '../../../fb-interfaces/Logger'; import {Logger} from '../../../fb-interfaces/Logger';
import Panel from '../Panel'; import Panel from '../Panel';
import ManagedDataInspector from '../data-inspector/ManagedDataInspector'; import {DataInspector} from 'flipper-plugin';
import {Component} from 'react'; import {Component} from 'react';
import {Console} from '../console'; import {Console} from '../console';
import GK from '../../../fb-stubs/GK'; import GK from '../../../fb-stubs/GK';
@@ -64,7 +64,7 @@ class InspectorSidebarSection extends Component<InspectorSidebarSectionProps> {
const {id} = this.props; const {id} = this.props;
return ( return (
<Panel heading={id} floating={false} grow={false}> <Panel heading={id} floating={false} grow={false}>
<ManagedDataInspector <DataInspector
data={this.props.data} data={this.props.data}
setValue={this.props.onValueChanged ? this.setValue : undefined} setValue={this.props.onValueChanged ? this.setValue : undefined}
extractValue={this.extractValue} extractValue={this.extractValue}

View File

@@ -43,12 +43,8 @@ export {ManagedTableProps_immutable} from './components/table/ManagedTable_immut
export {Value} from './components/table/TypeBasedValueRenderer'; export {Value} from './components/table/TypeBasedValueRenderer';
export {renderValue} from './components/table/TypeBasedValueRenderer'; export {renderValue} from './components/table/TypeBasedValueRenderer';
export { export {DataValueExtractor, DataInspectorExpanded} from 'flipper-plugin';
DataValueExtractor, export {DataInspector as ManagedDataInspector} from 'flipper-plugin';
DataInspectorExpanded,
} from './components/data-inspector/DataInspector';
export {default as DataInspector} from './components/data-inspector/DataInspector';
export {default as ManagedDataInspector} from './components/data-inspector/ManagedDataInspector';
export {default as SearchableDataInspector} from './components/data-inspector/SearchableDataInspector'; export {default as SearchableDataInspector} from './components/data-inspector/SearchableDataInspector';
// tabs // tabs
@@ -130,8 +126,6 @@ export {default as Heading} from './components/Heading';
// filters // filters
export {Filter} from './components/filter/types'; export {Filter} from './components/filter/types';
export {default as MarkerTimeline} from './components/MarkerTimeline';
export {default as StackTrace} from './components/StackTrace'; export {default as StackTrace} from './components/StackTrace';
export { export {

View File

@@ -16,10 +16,12 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-element-to-jsx-string": "^14.3.2", "react-element-to-jsx-string": "^14.3.2",
"react-virtual": "^2.6.1" "react-virtual": "^2.6.1",
"string-natural-compare": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^26.0.22", "@types/jest": "^26.0.22",
"@types/string-natural-compare": "^3.0.0",
"jest-mock-console": "^1.0.1", "jest-mock-console": "^1.0.1",
"typescript": "^4.1.2" "typescript": "^4.1.2"
}, },

View File

@@ -28,11 +28,14 @@ test('Correct top level API exposed', () => {
// Note, all `exposedAPIs` should be documented in `flipper-plugin.mdx` // Note, all `exposedAPIs` should be documented in `flipper-plugin.mdx`
expect(exposedAPIs.sort()).toMatchInlineSnapshot(` expect(exposedAPIs.sort()).toMatchInlineSnapshot(`
Array [ Array [
"DataDescription",
"DataFormatter", "DataFormatter",
"DataInspector",
"DataSource", "DataSource",
"DataTable", "DataTable",
"DetailSidebar", "DetailSidebar",
"Layout", "Layout",
"MarkerTimeline",
"NUX", "NUX",
"TestUtils", "TestUtils",
"Tracked", "Tracked",
@@ -58,8 +61,10 @@ test('Correct top level API exposed', () => {
Array [ Array [
"Atom", "Atom",
"DataDescriptionType", "DataDescriptionType",
"DataInspectorExpanded",
"DataTableColumn", "DataTableColumn",
"DataTableManager", "DataTableManager",
"DataValueExtractor",
"DefaultKeyboardAction", "DefaultKeyboardAction",
"Device", "Device",
"DeviceLogEntry", "DeviceLogEntry",
@@ -68,6 +73,7 @@ test('Correct top level API exposed', () => {
"DeviceType", "DeviceType",
"Draft", "Draft",
"FlipperLib", "FlipperLib",
"HighlightManager",
"Idler", "Idler",
"LogLevel", "LogLevel",
"LogTypes", "LogTypes",

View File

@@ -85,22 +85,22 @@ export {createDataSource, DataSource} from './state/DataSource';
export {DataTable, DataTableColumn} from './ui/data-table/DataTable'; export {DataTable, DataTableColumn} from './ui/data-table/DataTable';
export {DataTableManager} from './ui/data-table/DataTableManager'; export {DataTableManager} from './ui/data-table/DataTableManager';
export {
HighlightContext as _HighlightContext,
HighlightProvider as _HighlightProvider,
HighlightManager as _HighlightManager,
useHighlighter as _useHighlighter,
} from './ui/Highlight';
export { export {
Interactive as _Interactive, Interactive as _Interactive,
InteractiveProps as _InteractiveProps, InteractiveProps as _InteractiveProps,
} from './ui/Interactive'; } from './ui/Interactive';
export {HighlightManager} from './ui/Highlight';
export {
DataValueExtractor,
DataInspectorExpanded,
} from './ui/data-inspector/DataInspector';
export { export {
DataDescription as _DataDescription,
DataDescriptionType, DataDescriptionType,
DataDescription,
} from './ui/data-inspector/DataDescription'; } from './ui/data-inspector/DataDescription';
export {MarkerTimeline} from './ui/MarkerTimeline';
export {ManagedDataInspector as DataInspector} from './ui/data-inspector/ManagedDataInspector';
export {useMemoize} from './utils/useMemoize'; export {useMemoize} from './utils/useMemoize';

View File

@@ -17,10 +17,9 @@ import React, {
useContext, useContext,
} from 'react'; } from 'react';
import {debounce} from 'lodash'; import {debounce} from 'lodash';
import {theme} from './theme';
const Highlighted = styled.span({ const Highlighted = styled.span({
backgroundColor: theme.textColorActive, backgroundColor: '#fcd872',
}); });
export interface HighlightManager { export interface HighlightManager {

View File

@@ -7,12 +7,12 @@
* @format * @format
*/ */
import React from 'react';
import {Component} from 'react'; import {Component} from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import Text from './Text'; import {theme} from './theme';
import FlexRow from './FlexRow'; import {Layout} from './Layout';
import {colors} from './colors'; import {Typography} from 'antd';
import React from 'react';
type DataPoint = { type DataPoint = {
time: number; time: number;
@@ -40,7 +40,7 @@ const Markers = styled.div<{totalTime: number}>((props) => ({
'::before': { '::before': {
content: '""', content: '""',
width: 1, width: 1,
borderLeft: `1px dotted ${colors.light30}`, borderLeft: `1px dotted ${theme.disabledColor}`,
position: 'absolute', position: 'absolute',
top: 5, top: 5,
bottom: 20, bottom: 20,
@@ -49,7 +49,7 @@ const Markers = styled.div<{totalTime: number}>((props) => ({
})); }));
Markers.displayName = 'MarkerTimeline:Markers'; Markers.displayName = 'MarkerTimeline:Markers';
const Point = styled(FlexRow)<{ const Point = styled(Layout.Horizontal)<{
positionY: number; positionY: number;
onClick: MouseEventHandler | undefined; onClick: MouseEventHandler | undefined;
number: number | undefined; number: number | undefined;
@@ -91,7 +91,7 @@ const Point = styled(FlexRow)<{
marginRight: 6, marginRight: 6,
zIndex: 3, zIndex: 3,
boxShadow: props.selected boxShadow: props.selected
? `0 0 0 2px ${colors.macOSTitleBarIconSelected}` ? `0 0 0 2px ${theme.backgroundTransparentHover}`
: undefined, : undefined,
}, },
'::after': { '::after': {
@@ -101,23 +101,23 @@ const Point = styled(FlexRow)<{
top: -20, top: -20,
left: 0, left: 0,
height: 2, height: 2,
background: colors.white, background: theme.backgroundDefault,
borderTop: `1px solid ${colors.light30}`, borderTop: `1px solid ${theme.dividerColor}`,
borderBottom: `1px solid ${colors.light30}`, borderBottom: `1px solid ${theme.dividerColor}`,
transform: `skewY(-10deg)`, transform: `skewY(-10deg)`,
}, },
})); }));
Point.displayName = 'MakerTimeline:Point'; Point.displayName = 'MakerTimeline:Point';
const Time = styled.span({ const Time = styled.span({
color: colors.light30, color: theme.dividerColor,
fontWeight: 300, fontWeight: 300,
marginRight: 4, marginRight: 4,
marginTop: -2, marginTop: -2,
}); });
Time.displayName = 'MakerTimeline:Time'; Time.displayName = 'MakerTimeline:Time';
const Code = styled(Text)({ const Code = styled(Typography.Text)({
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
marginTop: -1, marginTop: -1,
@@ -137,7 +137,7 @@ type State = {
timePoints: Array<TimePoint>; timePoints: Array<TimePoint>;
}; };
export default class MarkerTimeline extends Component<Props, State> { export class MarkerTimeline extends Component<Props, State> {
static defaultProps = { static defaultProps = {
lineHeight: 22, lineHeight: 22,
maxGap: 100, maxGap: 100,
@@ -172,7 +172,7 @@ export default class MarkerTimeline extends Component<Props, State> {
for (let i = 0; i < sortedMarkers.length; i++) { for (let i = 0; i < sortedMarkers.length; i++) {
const [timestamp, points] = sortedMarkers[i]; const [timestamp, points] = sortedMarkers[i];
let isCut = false; let isCut = false;
const color = sortedMarkers[i][1][0].color || colors.white; const color = sortedMarkers[i][1][0].color || theme.backgroundDefault;
if (i > 0) { if (i > 0) {
const relativeTimestamp = timestamp - sortedMarkers[i - 1][0]; const relativeTimestamp = timestamp - sortedMarkers[i - 1][0];

View File

@@ -7,7 +7,7 @@
* @format * @format
*/ */
import MarkerTimeline from '../MarkerTimeline'; import {MarkerTimeline} from '../MarkerTimeline';
test('merges points with same timestamp', () => { test('merges points with same timestamp', () => {
const points = [ const points = [

View File

@@ -7,7 +7,7 @@
* @format * @format
*/ */
import {TestUtils} from 'flipper-plugin'; import {TestUtils} from '../../';
import React from 'react'; import React from 'react';
import {getNuxKey} from '../NUX'; import {getNuxKey} from '../NUX';

View File

@@ -8,7 +8,7 @@
*/ */
import {render, fireEvent} from '@testing-library/react'; import {render, fireEvent} from '@testing-library/react';
import {TestUtils} from 'flipper-plugin'; import {TestUtils} from '../../';
import {sleep} from '../../utils/sleep'; import {sleep} from '../../utils/sleep';
import React, {Component} from 'react'; import React, {Component} from 'react';
import { import {

View File

@@ -3,7 +3,7 @@
exports[`handles single point 1`] = ` exports[`handles single point 1`] = `
Array [ Array [
Object { Object {
"color": "#ffffff", "color": "var(--flipper-background-default)",
"isCut": false, "isCut": false,
"markerKeys": Array [ "markerKeys": Array [
"1", "1",

View File

@@ -8,24 +8,22 @@
*/ */
import {Typography, Popover, Input, Select, Checkbox} from 'antd'; import {Typography, Popover, Input, Select, Checkbox} from 'antd';
// TODO: restore import {DataInspectorSetValue} from './DataInspector'; import {DataInspectorSetValue} from './DataInspector';
import {PureComponent} from 'react'; import {PureComponent} from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import {SketchPicker, CompactPicker} from 'react-color'; import {SketchPicker, CompactPicker} from 'react-color';
import React, {KeyboardEvent} from 'react'; import React, {KeyboardEvent} from 'react';
import {HighlightContext} from '../Highlight'; import {HighlightContext} from '../Highlight';
import {parseColor} from '../../utils/parseColor'; import {parseColor} from '../../utils/parseColor';
// import TimelineDataDescription from './TimelineDataDescription'; TODO: import {TimelineDataDescription} from './TimelineDataDescription';
import {theme} from '../theme'; import {theme} from '../theme';
import {EditOutlined} from '@ant-design/icons'; import {EditOutlined} from '@ant-design/icons';
import type {CheckboxChangeEvent} from 'antd/lib/checkbox'; import type {CheckboxChangeEvent} from 'antd/lib/checkbox';
const {Link} = Typography; const {Link} = Typography;
type DataInspectorSetValue = (path: Array<string>, val: any) => void;
// Based on FIG UI Core, TODO: does that still makes sense? // Based on FIG UI Core, TODO: does that still makes sense?
const presetColors = Object.values({ export const presetColors = Object.values({
blue: '#4267b2', // Blue - Active-state nav glyphs, nav bars, links, buttons blue: '#4267b2', // Blue - Active-state nav glyphs, nav bars, links, buttons
green: '#42b72a', // Green - Confirmation, success, commerce and status green: '#42b72a', // Green - Confirmation, success, commerce and status
red: '#FC3A4B', // Red - Badges, error states red: '#FC3A4B', // Red - Badges, error states
@@ -214,6 +212,7 @@ class NumberTextEditor extends PureComponent<{
ref={this.onNumberTextRef} ref={this.onNumberTextRef}
onBlur={this.onNumberTextBlur} onBlur={this.onNumberTextBlur}
value={this.props.value} value={this.props.value}
style={{fontSize: 11}}
/> />
); );
} }
@@ -547,24 +546,22 @@ class DataDescriptionContainer extends PureComponent<{
switch (type) { switch (type) {
case 'timeline': { case 'timeline': {
return null; return (
// TODO: <>
// return ( <TimelineDataDescription
// <> canSetCurrent={editable}
// <TimelineDataDescription timeline={JSON.parse(val)}
// canSetCurrent={editable} onClick={(id) => {
// timeline={JSON.parse(val)} this.props.commit({
// onClick={(id) => { value: id,
// this.props.commit({ keep: true,
// value: id, clear: false,
// keep: true, set: true,
// clear: false, });
// set: true, }}
// }); />
// }} </>
// /> );
// </>
// );
} }
case 'number': case 'number':
@@ -632,6 +629,8 @@ class DataDescriptionContainer extends PureComponent<{
set: true, set: true,
}) })
} }
size="small"
style={{fontSize: 11}}
/> />
); );
} }

View File

@@ -7,27 +7,30 @@
* @format * @format
*/ */
import {_DataDescription} from 'flipper-plugin'; import {DataDescription} from './DataDescription';
import {MenuTemplate} from '../ContextMenu'; import {
import {memo, useMemo, useRef, useState, useEffect, useCallback} from 'react'; memo,
import ContextMenu from '../ContextMenu'; useMemo,
import Tooltip from '../Tooltip'; useRef,
useState,
useEffect,
useCallback,
createContext,
useContext,
} from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import createPaste from '../../../fb-stubs/createPaste';
import {reportInteraction} from '../../../utils/InteractionTracker';
import DataPreview, {DataValueExtractor, InspectorName} from './DataPreview'; import DataPreview, {DataValueExtractor, InspectorName} from './DataPreview';
import {getSortedKeys} from './utils'; import {getSortedKeys} from './utils';
import {colors} from '../colors';
import {clipboard} from 'electron';
import React from 'react'; import React from 'react';
import {TooltipOptions} from '../TooltipProvider'; import {useHighlighter, HighlightManager} from '../Highlight';
import { import {Dropdown, Menu, Tooltip} from 'antd';
_useHighlighter as useHighlighter, import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
_HighlightManager, import {safeStringify} from '../../utils/safeStringify';
} from 'flipper-plugin';
export {DataValueExtractor} from './DataPreview'; export {DataValueExtractor} from './DataPreview';
export const RootDataContext = createContext<() => any>(() => ({}));
const BaseContainer = styled.div<{depth?: number; disabled?: boolean}>( const BaseContainer = styled.div<{depth?: number; disabled?: boolean}>(
(props) => ({ (props) => ({
fontFamily: 'Menlo, monospace', fontFamily: 'Menlo, monospace',
@@ -42,7 +45,7 @@ const BaseContainer = styled.div<{depth?: number; disabled?: boolean}>(
BaseContainer.displayName = 'DataInspector:BaseContainer'; BaseContainer.displayName = 'DataInspector:BaseContainer';
const RecursiveBaseWrapper = styled.span({ const RecursiveBaseWrapper = styled.span({
color: colors.red, color: '#FC3A4B',
}); });
RecursiveBaseWrapper.displayName = 'DataInspector:RecursiveBaseWrapper'; RecursiveBaseWrapper.displayName = 'DataInspector:RecursiveBaseWrapper';
@@ -66,18 +69,13 @@ const ExpandControl = styled.span({
ExpandControl.displayName = 'DataInspector:ExpandControl'; ExpandControl.displayName = 'DataInspector:ExpandControl';
const Added = styled.div({ const Added = styled.div({
backgroundColor: colors.tealTint70, backgroundColor: '#d2f0ea',
}); });
const Removed = styled.div({ const Removed = styled.div({
backgroundColor: colors.cherryTint70, backgroundColor: '#fbccd2',
}); });
const nameTooltipOptions: TooltipOptions = {
position: 'toLeft',
showTail: true,
};
export type DataInspectorSetValue = (path: Array<string>, val: any) => void; export type DataInspectorSetValue = (path: Array<string>, val: any) => void;
export type DataInspectorDeleteValue = (path: Array<string>) => void; export type DataInspectorDeleteValue = (path: Array<string>) => void;
@@ -145,7 +143,7 @@ type DataInspectorProps = {
onRenderName?: ( onRenderName?: (
path: Array<string>, path: Array<string>,
name: string, name: string,
highlighter: _HighlightManager, highlighter: HighlightManager,
) => React.ReactElement; ) => React.ReactElement;
/** /**
* Render callback that can be used to customize the rendering of object values. * Render callback that can be used to customize the rendering of object values.
@@ -205,50 +203,29 @@ const defaultValueExtractor: DataValueExtractor = (value: any) => {
} }
}; };
const rootContextMenuCache: WeakMap< function getRootContextMenu(root: Object) {
Object, const lib = tryGetFlipperLibImplementation();
Array<Electron.MenuItemConstructorOptions> return (
> = new WeakMap(); <Menu>
<Menu.Item
function getRootContextMenu( key="copyClipboard"
data: Object, onClick={() => {
): Array<Electron.MenuItemConstructorOptions> { lib?.writeTextToClipboard(safeStringify(root));
const cached = rootContextMenuCache.get(data); }}>
if (cached != null) { Copy tree
return cached; </Menu.Item>
} <Menu.Divider />
{lib?.isFB && (
let stringValue: string; <Menu.Item
try { key="createPaste"
stringValue = JSON.stringify(data, null, 2); onClick={() => {
} catch (e) { lib?.createPaste(safeStringify(root));
stringValue = '<circular structure>'; }}>
} Create paste from tree
const menu: Array<Electron.MenuItemConstructorOptions> = [ </Menu.Item>
{ )}
label: 'Copy entire tree', </Menu>
click: () => clipboard.writeText(stringValue),
},
{
type: 'separator',
},
{
label: 'Create paste',
click: () => {
createPaste(stringValue);
},
},
];
if (typeof data === 'object' && data !== null) {
rootContextMenuCache.set(data, menu);
} else {
console.error(
'[data-inspector] Ignoring unsupported data type for cache: ',
data,
typeof data,
); );
}
return menu;
} }
function isPureObject(obj: Object) { function isPureObject(obj: Object) {
@@ -345,6 +322,7 @@ const DataInspector: React.FC<DataInspectorProps> = memo(
setValue: setValueProp, setValue: setValueProp,
}) { }) {
const highlighter = useHighlighter(); const highlighter = useHighlighter();
const getRoot = useContext(RootDataContext);
const shouldExpand = useRef(false); const shouldExpand = useRef(false);
const expandHandle = useRef(undefined as any); const expandHandle = useRef(undefined as any);
@@ -434,10 +412,6 @@ const DataInspector: React.FC<DataInspectorProps> = memo(
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
cancelIdleCallback(expandHandle.current); cancelIdleCallback(expandHandle.current);
const isExpanded = shouldBeExpanded(expandedPaths, path, collapsed); const isExpanded = shouldBeExpanded(expandedPaths, path, collapsed);
reportInteraction('DataInspector', path.join(':'))(
isExpanded ? 'collapsed' : 'expanded',
undefined,
);
setExpanded(path, !isExpanded); setExpanded(path, !isExpanded);
}, [expandedPaths, path, collapsed]); }, [expandedPaths, path, collapsed]);
@@ -534,11 +508,7 @@ const DataInspector: React.FC<DataInspectorProps> = memo(
} }
if (expandRoot === true) { if (expandRoot === true) {
return ( return <>{propertyNodesContainer}</>;
<ContextMenu component="span" items={getRootContextMenu(data)}>
{propertyNodesContainer}
</ContextMenu>
);
} }
// create name components // create name components
@@ -551,7 +521,7 @@ const DataInspector: React.FC<DataInspectorProps> = memo(
<Tooltip <Tooltip
title={tooltips != null && tooltips[name]} title={tooltips != null && tooltips[name]}
key="name" key="name"
options={nameTooltipOptions}> placement="left">
<InspectorName>{text}</InspectorName> <InspectorName>{text}</InspectorName>
</Tooltip>, </Tooltip>,
); );
@@ -562,7 +532,7 @@ const DataInspector: React.FC<DataInspectorProps> = memo(
let descriptionOrPreview; let descriptionOrPreview;
if (renderExpanded || !isExpandable) { if (renderExpanded || !isExpandable) {
descriptionOrPreview = ( descriptionOrPreview = (
<_DataDescription <DataDescription
path={path} path={path}
setValue={setValue} setValue={setValue}
type={type} type={type}
@@ -607,50 +577,58 @@ const DataInspector: React.FC<DataInspectorProps> = memo(
} }
} }
const contextMenuItems: MenuTemplate = []; function getContextMenu() {
const lib = tryGetFlipperLibImplementation();
if (isExpandable) { return (
contextMenuItems.push( <Menu>
{ <Menu.Item
label: shouldExpand.current ? 'Collapse' : 'Expand', key="copyClipboard"
click: handleClick, onClick={() => {
}, lib?.writeTextToClipboard(safeStringify(getRoot()));
{ }}>
type: 'separator', Copy tree
}, </Menu.Item>
{lib?.isFB && (
<Menu.Item
key="createPaste"
onClick={() => {
lib?.createPaste(safeStringify(getRoot()));
}}>
Create paste from tree
</Menu.Item>
)}
<Menu.Divider />
<Menu.Item
key="copyValue"
onClick={() => {
lib?.writeTextToClipboard(safeStringify(data));
}}>
Copy value
</Menu.Item>
{!isExpandable && onDelete ? (
<Menu.Item
key="delete"
onClick={() => {
handleDelete(path);
}}>
Delete
</Menu.Item>
) : null}
</Menu>
); );
} }
contextMenuItems.push(
{
label: 'Copy',
click: () =>
clipboard.writeText((window.getSelection() || '').toString()),
},
{
label: 'Copy value',
click: () => clipboard.writeText(JSON.stringify(data, null, 2)),
},
);
if (!isExpandable && onDelete) {
contextMenuItems.push({
label: 'Delete',
click: () => handleDelete(path),
});
}
return ( return (
<BaseContainer <BaseContainer
depth={depth} depth={depth}
disabled={!!setValueProp && !!setValue === false}> disabled={!!setValueProp && !!setValue === false}>
<ContextMenu component="span" items={contextMenuItems}> <Dropdown overlay={getContextMenu} trigger={['contextMenu']}>
<PropertyContainer onClick={isExpandable ? handleClick : undefined}> <PropertyContainer onClick={isExpandable ? handleClick : undefined}>
{expandedPaths && <ExpandControl>{expandGlyph}</ExpandControl>} {expandedPaths && <ExpandControl>{expandGlyph}</ExpandControl>}
{descriptionOrPreview} {descriptionOrPreview}
{wrapperStart} {wrapperStart}
</PropertyContainer> </PropertyContainer>
</ContextMenu> </Dropdown>
{propertyNodesContainer} {propertyNodesContainer}
{wrapperEnd} {wrapperEnd}
</BaseContainer> </BaseContainer>

View File

@@ -7,12 +7,11 @@
* @format * @format
*/ */
import {DataDescriptionType, _DataDescription} from 'flipper-plugin'; import {DataDescriptionType, DataDescription} from './DataDescription';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import {getSortedKeys} from './utils'; import {getSortedKeys} from './utils';
import {PureComponent} from 'react'; import {PureComponent} from 'react';
import React from 'react'; import React from 'react';
import {colors} from '../colors';
export type DataValueExtractor = ( export type DataValueExtractor = (
value: any, value: any,
@@ -29,7 +28,7 @@ export type DataValueExtractor = (
| null; | null;
export const InspectorName = styled.span({ export const InspectorName = styled.span({
color: colors.grapeDark1, color: '#7b64c0',
}); });
InspectorName.displayName = 'DataInspector:InspectorName'; InspectorName.displayName = 'DataInspector:InspectorName';
@@ -79,7 +78,7 @@ export default class DataPreview extends PureComponent<{
const {type, value} = res; const {type, value} = res;
return ( return (
<_DataDescription <DataDescription
key={index} key={index}
type={type} type={type}
value={value} value={value}

View File

@@ -7,12 +7,12 @@
* @format * @format
*/ */
import {DataInspectorExpanded} from './DataInspector'; import {DataInspectorExpanded, RootDataContext} from './DataInspector';
import {PureComponent} from 'react'; import {PureComponent} from 'react';
import DataInspector from './DataInspector'; import DataInspector from './DataInspector';
import React from 'react'; import React from 'react';
import {DataValueExtractor} from './DataPreview'; import {DataValueExtractor} from './DataPreview';
import {_HighlightProvider, _HighlightManager} from 'flipper-plugin'; import {HighlightProvider, HighlightManager} from '../Highlight';
export type ManagedDataInspectorProps = { export type ManagedDataInspectorProps = {
/** /**
@@ -47,7 +47,7 @@ export type ManagedDataInspectorProps = {
onRenderName?: ( onRenderName?: (
path: Array<string>, path: Array<string>,
name: string, name: string,
highlighter: _HighlightManager, highlighter: HighlightManager,
) => React.ReactElement; ) => React.ReactElement;
/** /**
* Render callback that can be used to customize the rendering of object values. * Render callback that can be used to customize the rendering of object values.
@@ -83,7 +83,7 @@ const EMPTY_ARRAY: any[] = [];
* If you require lower level access to the state then use `DataInspector` * If you require lower level access to the state then use `DataInspector`
* directly. * directly.
*/ */
export default class ManagedDataInspector extends PureComponent< export class ManagedDataInspector extends PureComponent<
ManagedDataInspectorProps, ManagedDataInspectorProps,
ManagedDataInspectorState ManagedDataInspectorState
> { > {
@@ -171,9 +171,15 @@ export default class ManagedDataInspector extends PureComponent<
}); });
}; };
// make sure this fn is a stable ref to not invalidate the whole tree on new data
getRootData = () => {
return this.props.data;
};
render() { render() {
return ( return (
<_HighlightProvider text={this.props.filter}> <RootDataContext.Provider value={this.getRootData}>
<HighlightProvider text={this.props.filter}>
<DataInspector <DataInspector
data={this.props.data} data={this.props.data}
diff={this.props.diff} diff={this.props.diff}
@@ -191,7 +197,8 @@ export default class ManagedDataInspector extends PureComponent<
depth={0} depth={0}
parentAncestry={EMPTY_ARRAY} parentAncestry={EMPTY_ARRAY}
/> />
</_HighlightProvider> </HighlightProvider>
</RootDataContext.Provider>
); );
} }
} }

View File

@@ -7,12 +7,12 @@
* @format * @format
*/ */
import ManagedDataInspector from './ManagedDataInspector'; import {ManagedDataInspector} from './ManagedDataInspector';
import {Component, ReactNode} from 'react'; import {Component, ReactNode} from 'react';
import {colors} from '../colors';
import React from 'react'; import React from 'react';
import MarkerTimeline from '../MarkerTimeline'; import {MarkerTimeline} from '../MarkerTimeline';
import Button from '../Button'; import {Button} from 'antd';
import {presetColors} from './DataDescription';
type TimePoint = { type TimePoint = {
moment: number; moment: number;
@@ -37,7 +37,7 @@ type State = {
selected: string; selected: string;
}; };
export default class TimelineDataDescription extends Component<Props, State> { export class TimelineDataDescription extends Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = {selected: props.timeline.current}; this.state = {selected: props.timeline.current};
@@ -50,7 +50,7 @@ export default class TimelineDataDescription extends Component<Props, State> {
label: value.display, label: value.display,
time: value.moment - firstMoment, time: value.moment - firstMoment,
color: color:
Object.entries(colors).find(([k, _]) => k === value.color)?.[1] ?? Object.entries(presetColors).find(([k, _]) => k === value.color)?.[1] ??
value.color, value.color,
key: value.key, key: value.key,
})); }));

View File

@@ -10,8 +10,8 @@
import * as React from 'react'; import * as React from 'react';
import {render, fireEvent, waitFor, act} from '@testing-library/react'; import {render, fireEvent, waitFor, act} from '@testing-library/react';
import ManagedDataInspector from '../ManagedDataInspector'; import {ManagedDataInspector} from '../ManagedDataInspector';
import {sleep} from '../../../../utils'; import {sleep} from '../../../utils/sleep';
const mocks = { const mocks = {
requestIdleCallback(fn: Function) { requestIdleCallback(fn: Function) {
@@ -124,7 +124,7 @@ test('can filter for data', async () => {
<span> <span>
"j "j
<span <span
class="css-zr1u3c-Highlighted eiud9hg0" class="css-i709sw-Highlighted eiud9hg0"
> >
son son
</span> </span>

View File

@@ -26,4 +26,3 @@
/// <reference path="nodejs.d.ts" /> /// <reference path="nodejs.d.ts" />
/// <reference path="npm-api.d.ts" /> /// <reference path="npm-api.d.ts" />
/// <reference path="openssl-wrapper.d.ts" /> /// <reference path="openssl-wrapper.d.ts" />
/// <reference path="string-natural-compare.d.ts" />

View File

@@ -1,12 +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
*/
declare module 'string-natural-compare' {
export default function naturalCompare(a: string, b: string): number;
}

View File

@@ -2982,6 +2982,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff"
integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==
"@types/string-natural-compare@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/string-natural-compare/-/string-natural-compare-3.0.0.tgz#44d0970987df4b89697993015d4447a26d1f2d66"
integrity sha512-fXFxr8Isabw+XVgZ1Clsi7slyv2cGTIK/U9niVkUCva5O96eiC1CjdOPLKQnxrOVvVYJPeAEkLT3VPTlgOnz0w==
"@types/tar@^4.0.3": "@types/tar@^4.0.3":
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/tar/-/tar-4.0.3.tgz#e2cce0b8ff4f285293243f5971bd7199176ac489" resolved "https://registry.yarnpkg.com/@types/tar/-/tar-4.0.3.tgz#e2cce0b8ff4f285293243f5971bd7199176ac489"

View File

@@ -781,6 +781,9 @@ See `View > Flipper Style Guide` inside the Flipper application for more details
### DataTable ### DataTable
### DataFormatter ### DataFormatter
### DataInspector
### DataDescription
### MarkerTimeline
Coming soon. Coming soon.