Added props/state/context inspection to KeyValue
This commit is contained in:
parent
a6d3f30f95
commit
7a94ad4e8a
|
@ -13,6 +13,7 @@ const arrayOfArrays = [
|
|||
[['a', 'b', 'c'], ['d', 'e', 'f'], ['h', 'i', 'j']],
|
||||
[['k', 'l', 'm'], ['n', 'o', 'p'], ['q', 'r', 's']],
|
||||
[['t', 'u', 'v'], ['w', 'x', 'y'], ['z']],
|
||||
[],
|
||||
];
|
||||
|
||||
const objectOfObjects = {
|
||||
|
@ -31,6 +32,7 @@ const objectOfObjects = {
|
|||
i: 8,
|
||||
j: 9,
|
||||
},
|
||||
qux: {},
|
||||
};
|
||||
|
||||
export default function Hydration() {
|
||||
|
|
|
@ -315,18 +315,24 @@ describe('InspectedElementContext', () => {
|
|||
typed_array,
|
||||
date,
|
||||
} = (inspectedElement: any).props;
|
||||
expect(html_element[meta.inspectable]).toBe(false);
|
||||
expect(html_element[meta.name]).toBe('DIV');
|
||||
expect(html_element[meta.type]).toBe('html_element');
|
||||
expect(fn[meta.inspectable]).toBe(false);
|
||||
expect(fn[meta.name]).toBe('exmapleFunction');
|
||||
expect(fn[meta.type]).toBe('function');
|
||||
expect(symbol[meta.inspectable]).toBe(false);
|
||||
expect(symbol[meta.name]).toBe('Symbol(symbol)');
|
||||
expect(symbol[meta.type]).toBe('symbol');
|
||||
expect(react_element[meta.inspectable]).toBe(false);
|
||||
expect(react_element[meta.name]).toBe('span');
|
||||
expect(react_element[meta.type]).toBe('react_element');
|
||||
expect(array_buffer[meta.meta].length).toBe(3);
|
||||
expect(array_buffer[meta.size]).toBe(3);
|
||||
expect(array_buffer[meta.inspectable]).toBe(false);
|
||||
expect(array_buffer[meta.name]).toBe('ArrayBuffer');
|
||||
expect(array_buffer[meta.type]).toBe('array_buffer');
|
||||
expect(typed_array[meta.meta].length).toBe(3);
|
||||
expect(typed_array[meta.size]).toBe(3);
|
||||
expect(typed_array[meta.inspectable]).toBe(false);
|
||||
expect(typed_array[meta.name]).toBe('Uint8Array');
|
||||
expect(typed_array[meta.type]).toBe('typed_array');
|
||||
expect(date[meta.type]).toBe('date');
|
||||
|
|
|
@ -132,10 +132,10 @@ describe('InspectedElementContext', () => {
|
|||
expect(symbol[meta.type]).toBe('symbol');
|
||||
expect(react_element[meta.name]).toBe('span');
|
||||
expect(react_element[meta.type]).toBe('react_element');
|
||||
expect(array_buffer[meta.meta].length).toBe(3);
|
||||
expect(array_buffer[meta.size]).toBe(3);
|
||||
expect(array_buffer[meta.name]).toBe('ArrayBuffer');
|
||||
expect(array_buffer[meta.type]).toBe('array_buffer');
|
||||
expect(typed_array[meta.meta].length).toBe(3);
|
||||
expect(typed_array[meta.size]).toBe(3);
|
||||
expect(typed_array[meta.name]).toBe('Uint8Array');
|
||||
expect(typed_array[meta.type]).toBe('typed_array');
|
||||
expect(date[meta.type]).toBe('date');
|
||||
|
|
|
@ -2168,7 +2168,7 @@ export function attach(
|
|||
((mostRecentlyInspectedElement: any): InspectedElement),
|
||||
path
|
||||
),
|
||||
mergeInspectedPaths,
|
||||
currentlyInspectedPaths,
|
||||
path
|
||||
),
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
|
||||
|
@ -15,14 +15,10 @@ export default function ExpandCollapseToggle({
|
|||
isOpen,
|
||||
setIsOpen,
|
||||
}: ExpandCollapseToggleProps) {
|
||||
const handleClick = useCallback(() => {
|
||||
setIsOpen(prevIsOpen => !prevIsOpen);
|
||||
}, [setIsOpen]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.ExpandCollapseToggle}
|
||||
onClick={handleClick}
|
||||
onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
|
||||
title={`${isOpen ? 'Collapse' : 'Expand'} prop value`}
|
||||
>
|
||||
<ButtonIcon type={isOpen ? 'expanded' : 'collapsed'} />
|
||||
|
|
|
@ -8,10 +8,13 @@ import KeyValue from './KeyValue';
|
|||
import { serializeDataForCopy } from '../utils';
|
||||
import styles from './InspectedElementTree.css';
|
||||
|
||||
import type { InspectPath } from './SelectedElement';
|
||||
|
||||
type OverrideValueFn = (path: Array<string | number>, value: any) => void;
|
||||
|
||||
type Props = {|
|
||||
data: Object | null,
|
||||
inspectPath?: InspectPath,
|
||||
label: string,
|
||||
overrideValueFn?: ?OverrideValueFn,
|
||||
showWhenEmpty?: boolean,
|
||||
|
@ -19,6 +22,7 @@ type Props = {|
|
|||
|
||||
export default function InspectedElementTree({
|
||||
data,
|
||||
inspectPath,
|
||||
label,
|
||||
overrideValueFn,
|
||||
showWhenEmpty = false,
|
||||
|
@ -32,7 +36,6 @@ export default function InspectedElementTree({
|
|||
if (isEmpty && !showWhenEmpty) {
|
||||
return null;
|
||||
} else {
|
||||
// TODO Add click and key handlers for toggling element open/close state.
|
||||
return (
|
||||
<div className={styles.InspectedElementTree}>
|
||||
<div className={styles.HeaderRow}>
|
||||
|
@ -49,6 +52,7 @@ export default function InspectedElementTree({
|
|||
<KeyValue
|
||||
key={name}
|
||||
depth={1}
|
||||
inspectPath={inspectPath}
|
||||
name={name}
|
||||
overrideValueFn={overrideValueFn}
|
||||
path={[name]}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import type { Element } from 'react';
|
||||
import EditableValue from './EditableValue';
|
||||
import ExpandCollapseToggle from './ExpandCollapseToggle';
|
||||
|
@ -8,11 +8,14 @@ import { getMetaValueLabel } from '../utils';
|
|||
import { meta } from '../../../hydration';
|
||||
import styles from './KeyValue.css';
|
||||
|
||||
import type { InspectPath } from './SelectedElement';
|
||||
|
||||
type OverrideValueFn = (path: Array<string | number>, value: any) => void;
|
||||
|
||||
type KeyValueProps = {|
|
||||
depth: number,
|
||||
hidden?: boolean,
|
||||
inspectPath?: InspectPath,
|
||||
name: string,
|
||||
overrideValueFn?: ?OverrideValueFn,
|
||||
path?: Array<any>,
|
||||
|
@ -24,6 +27,7 @@ type KeyValueProps = {|
|
|||
|
||||
export default function KeyValue({
|
||||
depth,
|
||||
inspectPath,
|
||||
hidden,
|
||||
name,
|
||||
overrideValueFn,
|
||||
|
@ -31,11 +35,27 @@ export default function KeyValue({
|
|||
value,
|
||||
}: KeyValueProps) {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const prevIsOpenRef = useRef(isOpen);
|
||||
|
||||
const toggleIsOpen = useCallback(
|
||||
() => setIsOpen(prevIsOpen => !prevIsOpen),
|
||||
[]
|
||||
);
|
||||
const isInspectable =
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
value[meta.inspectable] &&
|
||||
value[meta.size] !== 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isInspectable &&
|
||||
isOpen &&
|
||||
!prevIsOpenRef.current &&
|
||||
typeof inspectPath === 'function'
|
||||
) {
|
||||
inspectPath(path);
|
||||
}
|
||||
prevIsOpenRef.current = isOpen;
|
||||
}, [inspectPath, isInspectable, isOpen, path]);
|
||||
|
||||
const toggleIsOpen = () => setIsOpen(prevIsOpen => !prevIsOpen);
|
||||
|
||||
const dataType = typeof value;
|
||||
const isSimpleType =
|
||||
|
@ -81,12 +101,19 @@ export default function KeyValue({
|
|||
</div>
|
||||
);
|
||||
} else if (value.hasOwnProperty(meta.type)) {
|
||||
// TODO (hydration) show UI to load its data?
|
||||
// TODO Is this type even necessary? Can we just drop it?
|
||||
children = (
|
||||
<div key="root" className={styles.Item} hidden={hidden} style={style}>
|
||||
<div className={styles.ExpandCollapseToggleSpacer} />
|
||||
<span className={styles.Name}>{name}</span>
|
||||
{isInspectable ? (
|
||||
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
) : (
|
||||
<div className={styles.ExpandCollapseToggleSpacer} />
|
||||
)}
|
||||
<span
|
||||
className={styles.Name}
|
||||
onClick={isInspectable ? toggleIsOpen : undefined}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
<span className={styles.Value}>{getMetaValueLabel(value)}</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -98,6 +125,7 @@ export default function KeyValue({
|
|||
<KeyValue
|
||||
key={index}
|
||||
depth={depth + 1}
|
||||
inspectPath={inspectPath}
|
||||
hidden={hidden || !isOpen}
|
||||
name={index}
|
||||
overrideValueFn={overrideValueFn}
|
||||
|
@ -133,6 +161,7 @@ export default function KeyValue({
|
|||
<KeyValue
|
||||
key={name}
|
||||
depth={depth + 1}
|
||||
inspectPath={inspectPath}
|
||||
hidden={hidden || !isOpen}
|
||||
name={name}
|
||||
overrideValueFn={overrideValueFn}
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
|
||||
import styles from './SelectedElement.css';
|
||||
|
||||
import type { GetPath } from './InspectedElementContext';
|
||||
import type { Element, InspectedElement } from './types';
|
||||
import type { ElementType } from 'src/types';
|
||||
|
||||
|
@ -38,7 +39,7 @@ export default function SelectedElement(_: Props) {
|
|||
const store = useContext(StoreContext);
|
||||
const { dispatch: modalDialogDispatch } = useContext(ModalDialogContext);
|
||||
|
||||
const { read } = useContext(InspectedElementContext);
|
||||
const { getPath, read } = useContext(InspectedElementContext);
|
||||
|
||||
const element =
|
||||
inspectedElementID !== null
|
||||
|
@ -200,7 +201,11 @@ export default function SelectedElement(_: Props) {
|
|||
|
||||
{inspectedElement !== null && (
|
||||
<InspectedElementView
|
||||
key={
|
||||
inspectedElementID /* Ensure state resets between seleted Elements */
|
||||
}
|
||||
element={element}
|
||||
getPath={getPath}
|
||||
inspectedElement={inspectedElement}
|
||||
/>
|
||||
)}
|
||||
|
@ -208,8 +213,11 @@ export default function SelectedElement(_: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export type InspectPath = (path: Array<string | number>) => void;
|
||||
|
||||
type InspectedElementViewProps = {|
|
||||
element: Element,
|
||||
getPath: GetPath,
|
||||
inspectedElement: InspectedElement,
|
||||
|};
|
||||
|
||||
|
@ -217,6 +225,7 @@ const IS_SUSPENDED = 'Suspended';
|
|||
|
||||
function InspectedElementView({
|
||||
element,
|
||||
getPath,
|
||||
inspectedElement,
|
||||
}: InspectedElementViewProps) {
|
||||
const { id, type } = element;
|
||||
|
@ -236,6 +245,25 @@ function InspectedElementView({
|
|||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const inspectContextPath = useCallback(
|
||||
(path: Array<string | number>) => {
|
||||
getPath(id, ['context', ...path]);
|
||||
},
|
||||
[getPath, id]
|
||||
);
|
||||
const inspectPropsPath = useCallback(
|
||||
(path: Array<string | number>) => {
|
||||
getPath(id, ['props', ...path]);
|
||||
},
|
||||
[getPath, id]
|
||||
);
|
||||
const inspectStatePath = useCallback(
|
||||
(path: Array<string | number>) => {
|
||||
getPath(id, ['state', ...path]);
|
||||
},
|
||||
[getPath, id]
|
||||
);
|
||||
|
||||
let overrideContextFn = null;
|
||||
let overridePropsFn = null;
|
||||
let overrideStateFn = null;
|
||||
|
@ -279,6 +307,7 @@ function InspectedElementView({
|
|||
<InspectedElementTree
|
||||
label="props"
|
||||
data={props}
|
||||
inspectPath={inspectPropsPath}
|
||||
overrideValueFn={overridePropsFn}
|
||||
showWhenEmpty
|
||||
/>
|
||||
|
@ -294,6 +323,7 @@ function InspectedElementView({
|
|||
<InspectedElementTree
|
||||
label="state"
|
||||
data={state}
|
||||
inspectPath={inspectStatePath}
|
||||
overrideValueFn={overrideStateFn}
|
||||
/>
|
||||
)}
|
||||
|
@ -301,6 +331,7 @@ function InspectedElementView({
|
|||
<InspectedElementTree
|
||||
label="context"
|
||||
data={context}
|
||||
inspectPath={inspectContextPath}
|
||||
overrideValueFn={overrideContextFn}
|
||||
/>
|
||||
{events !== null && events.length > 0 && <EventsTree events={events} />}
|
||||
|
|
|
@ -91,7 +91,7 @@ export function getMetaValueLabel(data: Object): string | null {
|
|||
case 'data_view':
|
||||
case 'array':
|
||||
case 'typed_array':
|
||||
return `${name}[${data[meta.meta].length}]`;
|
||||
return `${name}[${data[meta.size]}]`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
101
src/hydration.js
101
src/hydration.js
|
@ -19,13 +19,22 @@ import {
|
|||
import { getDisplayName, getInObject, setInObject } from './utils';
|
||||
|
||||
export const meta = {
|
||||
name: Symbol('name'),
|
||||
type: Symbol('type'),
|
||||
inspectable: Symbol('inspectable'),
|
||||
inspected: Symbol('inspected'),
|
||||
meta: Symbol('meta'),
|
||||
proto: Symbol('proto'),
|
||||
name: Symbol('name'),
|
||||
readonly: Symbol('readonly'),
|
||||
size: Symbol('size'),
|
||||
type: Symbol('type'),
|
||||
};
|
||||
|
||||
type Dehydrated = {|
|
||||
inspectable: boolean,
|
||||
name: string | null,
|
||||
readonly?: boolean,
|
||||
size?: number,
|
||||
type: string,
|
||||
|};
|
||||
|
||||
// This threshold determines the depth at which the bridge "dehydrates" nested data.
|
||||
// Dehydration means that we don't serialize the data for e.g. postMessage or stringify,
|
||||
// unless the frontend explicitly requests it (e.g. a user clicks to expand a props object).
|
||||
|
@ -82,29 +91,33 @@ function getPropType(data: Object): string | null {
|
|||
*/
|
||||
function createDehydrated(
|
||||
type: string,
|
||||
inspectable: boolean,
|
||||
data: Object,
|
||||
cleaned: Array<Array<string | number>>,
|
||||
path: Array<string | number>
|
||||
): Object {
|
||||
const meta = {};
|
||||
|
||||
if (type === 'array' || type === 'typed_array') {
|
||||
meta.length = data.length;
|
||||
}
|
||||
if (type === 'iterator' || type === 'typed_array') {
|
||||
meta.readOnly = true;
|
||||
}
|
||||
|
||||
): Dehydrated {
|
||||
cleaned.push(path);
|
||||
|
||||
return {
|
||||
const dehydrated: Dehydrated = {
|
||||
inspectable,
|
||||
type,
|
||||
meta,
|
||||
name:
|
||||
!data.constructor || data.constructor.name === 'Object'
|
||||
? ''
|
||||
: data.constructor.name,
|
||||
};
|
||||
|
||||
if (type === 'array' || type === 'typed_array') {
|
||||
dehydrated.size = data.length;
|
||||
} else if (type === 'object') {
|
||||
dehydrated.size = Object.keys(data).length;
|
||||
}
|
||||
|
||||
if (type === 'iterator' || type === 'typed_array') {
|
||||
dehydrated.readonly = true;
|
||||
}
|
||||
|
||||
return dehydrated;
|
||||
}
|
||||
|
||||
function isInspectedPath(
|
||||
|
@ -145,13 +158,14 @@ export function dehydrate(
|
|||
path: Array<string | number>,
|
||||
inspectedPaths: Object,
|
||||
level?: number = 0
|
||||
): string | Object {
|
||||
): string | Dehydrated | { [key: string]: string | Dehydrated } {
|
||||
const type = getPropType(data);
|
||||
|
||||
switch (type) {
|
||||
case 'html_element':
|
||||
cleaned.push(path);
|
||||
return {
|
||||
inspectable: false,
|
||||
name: data.tagName,
|
||||
type: 'html_element',
|
||||
};
|
||||
|
@ -159,6 +173,7 @@ export function dehydrate(
|
|||
case 'function':
|
||||
cleaned.push(path);
|
||||
return {
|
||||
inspectable: false,
|
||||
name: data.name,
|
||||
type: 'function',
|
||||
};
|
||||
|
@ -171,8 +186,9 @@ export function dehydrate(
|
|||
case 'symbol':
|
||||
cleaned.push(path);
|
||||
return {
|
||||
type: 'symbol',
|
||||
inspectable: false,
|
||||
name: data.toString(),
|
||||
type: 'symbol',
|
||||
};
|
||||
|
||||
// React Elements aren't very inspector-friendly,
|
||||
|
@ -180,6 +196,7 @@ export function dehydrate(
|
|||
case 'react_element':
|
||||
cleaned.push(path);
|
||||
return {
|
||||
inspectable: false,
|
||||
name: getDisplayNameForReactElement(data),
|
||||
type: 'react_element',
|
||||
};
|
||||
|
@ -189,18 +206,16 @@ export function dehydrate(
|
|||
case 'data_view':
|
||||
cleaned.push(path);
|
||||
return {
|
||||
type,
|
||||
inspectable: false,
|
||||
name: type === 'data_view' ? 'DataView' : 'ArrayBuffer',
|
||||
meta: {
|
||||
length: data.byteLength,
|
||||
uninspectable: true,
|
||||
},
|
||||
size: data.byteLength,
|
||||
type,
|
||||
};
|
||||
|
||||
case 'array':
|
||||
const arrayPathCheck = isInspectedPath(path, inspectedPaths);
|
||||
if (level >= LEVEL_THRESHOLD && !arrayPathCheck) {
|
||||
return createDehydrated(type, data, cleaned, path);
|
||||
return createDehydrated(type, true, data, cleaned, path);
|
||||
}
|
||||
return data.map((item, i) =>
|
||||
dehydrate(
|
||||
|
@ -214,24 +229,24 @@ export function dehydrate(
|
|||
|
||||
case 'typed_array':
|
||||
case 'iterator':
|
||||
return createDehydrated(type, data, cleaned, path);
|
||||
return createDehydrated(type, false, data, cleaned, path);
|
||||
|
||||
case 'date':
|
||||
cleaned.push(path);
|
||||
return {
|
||||
inspectable: false,
|
||||
name: data.toString(),
|
||||
type: 'date',
|
||||
meta: {
|
||||
uninspectable: true,
|
||||
},
|
||||
};
|
||||
|
||||
case 'object':
|
||||
const objectPathCheck = isInspectedPath(path, inspectedPaths);
|
||||
if (level >= LEVEL_THRESHOLD && !objectPathCheck) {
|
||||
return createDehydrated(type, data, cleaned, path);
|
||||
return createDehydrated(type, true, data, cleaned, path);
|
||||
} else {
|
||||
const res = {};
|
||||
const object = {};
|
||||
for (let name in data) {
|
||||
res[name] = dehydrate(
|
||||
object[name] = dehydrate(
|
||||
data[name],
|
||||
cleaned,
|
||||
path.concat([name]),
|
||||
|
@ -239,7 +254,7 @@ export function dehydrate(
|
|||
objectPathCheck ? 1 : level + 1
|
||||
);
|
||||
}
|
||||
return res;
|
||||
return object;
|
||||
}
|
||||
|
||||
default:
|
||||
|
@ -252,16 +267,16 @@ export function fillInPath(
|
|||
path: Array<string | number>,
|
||||
value: any
|
||||
) {
|
||||
const length = path.length;
|
||||
const parent = getInObject(object, path.slice(0, length - 1));
|
||||
if (object != null) {
|
||||
delete parent[meta.name];
|
||||
delete parent[meta.type];
|
||||
delete parent[meta.meta];
|
||||
delete parent[meta.inspected];
|
||||
|
||||
setInObject(object, path, value);
|
||||
const target = getInObject(object, path);
|
||||
if (target != null) {
|
||||
delete target[meta.inspectable];
|
||||
delete target[meta.inspected];
|
||||
delete target[meta.name];
|
||||
delete target[meta.readonly];
|
||||
delete target[meta.size];
|
||||
delete target[meta.type];
|
||||
}
|
||||
setInObject(object, path, value);
|
||||
}
|
||||
|
||||
export function hydrate(
|
||||
|
@ -280,9 +295,11 @@ export function hydrate(
|
|||
|
||||
// Replace the string keys with Symbols so they're non-enumerable.
|
||||
const replaced: { [key: Symbol]: boolean | string } = {};
|
||||
replaced[meta.inspectable] = !!value.inspectable;
|
||||
replaced[meta.inspected] = false;
|
||||
replaced[meta.meta] = value.meta;
|
||||
replaced[meta.name] = value.name;
|
||||
replaced[meta.size] = value.size;
|
||||
replaced[meta.readonly] = !!value.readonly;
|
||||
replaced[meta.type] = value.type;
|
||||
|
||||
parent[last] = replaced;
|
||||
|
|
Loading…
Reference in New Issue