Added props/state/context inspection to KeyValue
Brian Vaughn committed
Jun 17, 2019 at 11:30 UTC
7a94ad4e8a071ebaf8adb329cb4939be07b8b269
10 files changed
+149
-64
shells/dev/app/InspectableElements/Hydration.js
+2
@@ -13,6 +13,7 @@ const arrayOfArrays = [
13
[['a', 'b', 'c'], ['d', 'e', 'f'], ['h', 'i', 'j']],
14
[['k', 'l', 'm'], ['n', 'o', 'p'], ['q', 'r', 's']],
15
[['t', 'u', 'v'], ['w', 'x', 'y'], ['z']],
16
+ [],
17
];
18
19
const objectOfObjects = {
@@ -31,6 +32,7 @@ const objectOfObjects = {
32
i: 8,
33
j: 9,
34
},
35
+ qux: {},
36
};
37
38
export default function Hydration() {
src/__tests__/inspectedElementContext-test.js
+8
-2
@@ -315,18 +315,24 @@ describe('InspectedElementContext', () => {
315
typed_array,
316
date,
317
} = (inspectedElement: any).props;
318
+ expect(html_element[meta.inspectable]).toBe(false);
319
expect(html_element[meta.name]).toBe('DIV');
320
expect(html_element[meta.type]).toBe('html_element');
321
+ expect(fn[meta.inspectable]).toBe(false);
322
expect(fn[meta.name]).toBe('exmapleFunction');
323
expect(fn[meta.type]).toBe('function');
324
+ expect(symbol[meta.inspectable]).toBe(false);
325
expect(symbol[meta.name]).toBe('Symbol(symbol)');
326
expect(symbol[meta.type]).toBe('symbol');
327
+ expect(react_element[meta.inspectable]).toBe(false);
328
expect(react_element[meta.name]).toBe('span');
329
expect(react_element[meta.type]).toBe('react_element');
326
- expect(array_buffer[meta.meta].length).toBe(3);
330
+ expect(array_buffer[meta.size]).toBe(3);
331
+ expect(array_buffer[meta.inspectable]).toBe(false);
332
expect(array_buffer[meta.name]).toBe('ArrayBuffer');
333
expect(array_buffer[meta.type]).toBe('array_buffer');
329
- expect(typed_array[meta.meta].length).toBe(3);
334
+ expect(typed_array[meta.size]).toBe(3);
335
+ expect(typed_array[meta.inspectable]).toBe(false);
336
expect(typed_array[meta.name]).toBe('Uint8Array');
337
expect(typed_array[meta.type]).toBe('typed_array');
338
expect(date[meta.type]).toBe('date');
src/__tests__/legacy/inspectElement-test.js
+2
-2
@@ -132,10 +132,10 @@ describe('InspectedElementContext', () => {
132
expect(symbol[meta.type]).toBe('symbol');
133
expect(react_element[meta.name]).toBe('span');
134
expect(react_element[meta.type]).toBe('react_element');
135
- expect(array_buffer[meta.meta].length).toBe(3);
135
+ expect(array_buffer[meta.size]).toBe(3);
136
expect(array_buffer[meta.name]).toBe('ArrayBuffer');
137
expect(array_buffer[meta.type]).toBe('array_buffer');
138
- expect(typed_array[meta.meta].length).toBe(3);
138
+ expect(typed_array[meta.size]).toBe(3);
139
expect(typed_array[meta.name]).toBe('Uint8Array');
140
expect(typed_array[meta.type]).toBe('typed_array');
141
expect(date[meta.type]).toBe('date');
src/backend/renderer.js
+1
-1
@@ -2168,7 +2168,7 @@ export function attach(
2168
((mostRecentlyInspectedElement: any): InspectedElement),
2169
path
2170
),
2171
- mergeInspectedPaths,
2171
+ currentlyInspectedPaths,
2172
path
2173
),
2174
};
src/devtools/views/Components/ExpandCollapseToggle.js
+2
-6
@@ -1,6 +1,6 @@
1
// @flow
2
3
-import React, { useCallback } from 'react';
3
+import React from 'react';
4
import Button from '../Button';
5
import ButtonIcon from '../ButtonIcon';
6
@@ -15,14 +15,10 @@ export default function ExpandCollapseToggle({
15
isOpen,
16
setIsOpen,
17
}: ExpandCollapseToggleProps) {
18
- const handleClick = useCallback(() => {
19
- setIsOpen(prevIsOpen => !prevIsOpen);
20
- }, [setIsOpen]);
21
-
18
return (
19
<Button
20
className={styles.ExpandCollapseToggle}
25
- onClick={handleClick}
21
+ onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
22
title={`${isOpen ? 'Collapse' : 'Expand'} prop value`}
23
>
24
<ButtonIcon type={isOpen ? 'expanded' : 'collapsed'} />
src/devtools/views/Components/InspectedElementTree.js
+5
-1
@@ -8,10 +8,13 @@ import KeyValue from './KeyValue';
8
import { serializeDataForCopy } from '../utils';
9
import styles from './InspectedElementTree.css';
10
11
+import type { InspectPath } from './SelectedElement';
12
+
13
type OverrideValueFn = (path: Array<string | number>, value: any) => void;
14
15
type Props = {|
16
data: Object | null,
17
+ inspectPath?: InspectPath,
18
label: string,
19
overrideValueFn?: ?OverrideValueFn,
20
showWhenEmpty?: boolean,
@@ -19,6 +22,7 @@ type Props = {|
22
23
export default function InspectedElementTree({
24
data,
25
+ inspectPath,
26
label,
27
overrideValueFn,
28
showWhenEmpty = false,
@@ -32,7 +36,6 @@ export default function InspectedElementTree({
36
if (isEmpty && !showWhenEmpty) {
37
return null;
38
} else {
35
- // TODO Add click and key handlers for toggling element open/close state.
39
return (
40
<div className={styles.InspectedElementTree}>
41
<div className={styles.HeaderRow}>
@@ -49,6 +52,7 @@ export default function InspectedElementTree({
52
<KeyValue
53
key={name}
54
depth={1}
55
+ inspectPath={inspectPath}
56
name={name}
57
overrideValueFn={overrideValueFn}
58
path={[name]}
src/devtools/views/Components/KeyValue.js
+38
-9
@@ -1,6 +1,6 @@
1
// @flow
2
3
-import React, { useCallback, useState } from 'react';
3
+import React, { useEffect, useRef, useState } from 'react';
4
import type { Element } from 'react';
5
import EditableValue from './EditableValue';
6
import ExpandCollapseToggle from './ExpandCollapseToggle';
@@ -8,11 +8,14 @@ import { getMetaValueLabel } from '../utils';
8
import { meta } from '../../../hydration';
9
import styles from './KeyValue.css';
10
11
+import type { InspectPath } from './SelectedElement';
12
+
13
type OverrideValueFn = (path: Array<string | number>, value: any) => void;
14
15
type KeyValueProps = {|
16
depth: number,
17
hidden?: boolean,
18
+ inspectPath?: InspectPath,
19
name: string,
20
overrideValueFn?: ?OverrideValueFn,
21
path?: Array<any>,
@@ -24,6 +27,7 @@ type KeyValueProps = {|
27
28
export default function KeyValue({
29
depth,
30
+ inspectPath,
31
hidden,
32
name,
33
overrideValueFn,
@@ -31,11 +35,27 @@ export default function KeyValue({
35
value,
36
}: KeyValueProps) {
37
const [isOpen, setIsOpen] = useState<boolean>(false);
38
+ const prevIsOpenRef = useRef(isOpen);
39
+
40
+ const isInspectable =
41
+ value !== null &&
42
+ typeof value === 'object' &&
43
+ value[meta.inspectable] &&
44
+ value[meta.size] !== 0;
45
+
46
+ useEffect(() => {
47
+ if (
48
+ isInspectable &&
49
+ isOpen &&
50
+ !prevIsOpenRef.current &&
51
+ typeof inspectPath === 'function'
52
+ ) {
53
+ inspectPath(path);
54
+ }
55
+ prevIsOpenRef.current = isOpen;
56
+ }, [inspectPath, isInspectable, isOpen, path]);
57
35
- const toggleIsOpen = useCallback(
36
- () => setIsOpen(prevIsOpen => !prevIsOpen),
37
- []
38
- );
58
+ const toggleIsOpen = () => setIsOpen(prevIsOpen => !prevIsOpen);
59
60
const dataType = typeof value;
61
const isSimpleType =
@@ -81,12 +101,19 @@ export default function KeyValue({
101
</div>
102
);
103
} else if (value.hasOwnProperty(meta.type)) {
84
- // TODO (hydration) show UI to load its data?
85
- // TODO Is this type even necessary? Can we just drop it?
104
children = (
105
<div key="root" className={styles.Item} hidden={hidden} style={style}>
88
- <div className={styles.ExpandCollapseToggleSpacer} />
89
- <span className={styles.Name}>{name}</span>
106
+ {isInspectable ? (
107
+ <ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
108
+ ) : (
109
+ <div className={styles.ExpandCollapseToggleSpacer} />
110
+ )}
111
+ <span
112
+ className={styles.Name}
113
+ onClick={isInspectable ? toggleIsOpen : undefined}
114
+ >
115
+ {name}
116
+ </span>
117
<span className={styles.Value}>{getMetaValueLabel(value)}</span>
118
</div>
119
);
@@ -98,6 +125,7 @@ export default function KeyValue({
125
<KeyValue
126
key={index}
127
depth={depth + 1}
128
+ inspectPath={inspectPath}
129
hidden={hidden || !isOpen}
130
name={index}
131
overrideValueFn={overrideValueFn}
@@ -133,6 +161,7 @@ export default function KeyValue({
161
<KeyValue
162
key={name}
163
depth={depth + 1}
164
+ inspectPath={inspectPath}
165
hidden={hidden || !isOpen}
166
name={name}
167
overrideValueFn={overrideValueFn}
src/devtools/views/Components/SelectedElement.js
+32
-1
@@ -25,6 +25,7 @@ import {
25
26
import styles from './SelectedElement.css';
27
28
+import type { GetPath } from './InspectedElementContext';
29
import type { Element, InspectedElement } from './types';
30
import type { ElementType } from 'src/types';
31
@@ -38,7 +39,7 @@ export default function SelectedElement(_: Props) {
39
const store = useContext(StoreContext);
40
const { dispatch: modalDialogDispatch } = useContext(ModalDialogContext);
41
41
- const { read } = useContext(InspectedElementContext);
42
+ const { getPath, read } = useContext(InspectedElementContext);
43
44
const element =
45
inspectedElementID !== null
@@ -200,7 +201,11 @@ export default function SelectedElement(_: Props) {
201
202
{inspectedElement !== null && (
203
<InspectedElementView
204
+ key={
205
+ inspectedElementID /* Ensure state resets between seleted Elements */
206
+ }
207
element={element}
208
+ getPath={getPath}
209
inspectedElement={inspectedElement}
210
/>
211
)}
@@ -208,8 +213,11 @@ export default function SelectedElement(_: Props) {
213
);
214
}
215
216
+export type InspectPath = (path: Array<string | number>) => void;
217
+
218
type InspectedElementViewProps = {|
219
element: Element,
220
+ getPath: GetPath,
221
inspectedElement: InspectedElement,
222
|};
223
@@ -217,6 +225,7 @@ const IS_SUSPENDED = 'Suspended';
225
226
function InspectedElementView({
227
element,
228
+ getPath,
229
inspectedElement,
230
}: InspectedElementViewProps) {
231
const { id, type } = element;
@@ -236,6 +245,25 @@ function InspectedElementView({
245
const bridge = useContext(BridgeContext);
246
const store = useContext(StoreContext);
247
248
+ const inspectContextPath = useCallback(
249
+ (path: Array<string | number>) => {
250
+ getPath(id, ['context', ...path]);
251
+ },
252
+ [getPath, id]
253
+ );
254
+ const inspectPropsPath = useCallback(
255
+ (path: Array<string | number>) => {
256
+ getPath(id, ['props', ...path]);
257
+ },
258
+ [getPath, id]
259
+ );
260
+ const inspectStatePath = useCallback(
261
+ (path: Array<string | number>) => {
262
+ getPath(id, ['state', ...path]);
263
+ },
264
+ [getPath, id]
265
+ );
266
+
267
let overrideContextFn = null;
268
let overridePropsFn = null;
269
let overrideStateFn = null;
@@ -279,6 +307,7 @@ function InspectedElementView({
307
<InspectedElementTree
308
label="props"
309
data={props}
310
+ inspectPath={inspectPropsPath}
311
overrideValueFn={overridePropsFn}
312
showWhenEmpty
313
/>
@@ -294,6 +323,7 @@ function InspectedElementView({
323
<InspectedElementTree
324
label="state"
325
data={state}
326
+ inspectPath={inspectStatePath}
327
overrideValueFn={overrideStateFn}
328
/>
329
)}
@@ -301,6 +331,7 @@ function InspectedElementView({
331
<InspectedElementTree
332
label="context"
333
data={context}
334
+ inspectPath={inspectContextPath}
335
overrideValueFn={overrideContextFn}
336
/>
337
{events !== null && events.length > 0 && <EventsTree events={events} />}
src/devtools/views/utils.js
+1
-1
@@ -91,7 +91,7 @@ export function getMetaValueLabel(data: Object): string | null {
91
case 'data_view':
92
case 'array':
93
case 'typed_array':
94
- return `${name}[${data[meta.meta].length}]`;
94
+ return `${name}[${data[meta.size]}]`;
95
default:
96
return null;
97
}
src/hydration.js
+58
-41
@@ -19,13 +19,22 @@ import {
19
import { getDisplayName, getInObject, setInObject } from './utils';
20
21
export const meta = {
22
+ inspectable: Symbol('inspectable'),
23
+ inspected: Symbol('inspected'),
24
name: Symbol('name'),
25
+ readonly: Symbol('readonly'),
26
+ size: Symbol('size'),
27
type: Symbol('type'),
24
- inspected: Symbol('inspected'),
25
- meta: Symbol('meta'),
26
- proto: Symbol('proto'),
28
};
29
30
+type Dehydrated = {|
31
+ inspectable: boolean,
32
+ name: string | null,
33
+ readonly?: boolean,
34
+ size?: number,
35
+ type: string,
36
+|};
37
+
38
// This threshold determines the depth at which the bridge "dehydrates" nested data.
39
// Dehydration means that we don't serialize the data for e.g. postMessage or stringify,
40
// 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 {
91
*/
92
function createDehydrated(
93
type: string,
94
+ inspectable: boolean,
95
data: Object,
96
cleaned: Array<Array<string | number>>,
97
path: Array<string | number>
88
-): Object {
89
- const meta = {};
90
-
91
- if (type === 'array' || type === 'typed_array') {
92
- meta.length = data.length;
93
- }
94
- if (type === 'iterator' || type === 'typed_array') {
95
- meta.readOnly = true;
96
- }
97
-
98
+): Dehydrated {
99
cleaned.push(path);
100
100
- return {
101
+ const dehydrated: Dehydrated = {
102
+ inspectable,
103
type,
102
- meta,
104
name:
105
!data.constructor || data.constructor.name === 'Object'
106
? ''
107
: data.constructor.name,
108
};
109
+
110
+ if (type === 'array' || type === 'typed_array') {
111
+ dehydrated.size = data.length;
112
+ } else if (type === 'object') {
113
+ dehydrated.size = Object.keys(data).length;
114
+ }
115
+
116
+ if (type === 'iterator' || type === 'typed_array') {
117
+ dehydrated.readonly = true;
118
+ }
119
+
120
+ return dehydrated;
121
}
122
123
function isInspectedPath(
@@ -145,13 +158,14 @@ export function dehydrate(
158
path: Array<string | number>,
159
inspectedPaths: Object,
160
level?: number = 0
148
-): string | Object {
161
+): string | Dehydrated | { [key: string]: string | Dehydrated } {
162
const type = getPropType(data);
163
164
switch (type) {
165
case 'html_element':
166
cleaned.push(path);
167
return {
168
+ inspectable: false,
169
name: data.tagName,
170
type: 'html_element',
171
};
@@ -159,6 +173,7 @@ export function dehydrate(
173
case 'function':
174
cleaned.push(path);
175
return {
176
+ inspectable: false,
177
name: data.name,
178
type: 'function',
179
};
@@ -171,8 +186,9 @@ export function dehydrate(
186
case 'symbol':
187
cleaned.push(path);
188
return {
174
- type: 'symbol',
189
+ inspectable: false,
190
name: data.toString(),
191
+ type: 'symbol',
192
};
193
194
// React Elements aren't very inspector-friendly,
@@ -180,6 +196,7 @@ export function dehydrate(
196
case 'react_element':
197
cleaned.push(path);
198
return {
199
+ inspectable: false,
200
name: getDisplayNameForReactElement(data),
201
type: 'react_element',
202
};
@@ -189,18 +206,16 @@ export function dehydrate(
206
case 'data_view':
207
cleaned.push(path);
208
return {
192
- type,
209
+ inspectable: false,
210
name: type === 'data_view' ? 'DataView' : 'ArrayBuffer',
194
- meta: {
195
- length: data.byteLength,
196
- uninspectable: true,
197
- },
211
+ size: data.byteLength,
212
+ type,
213
};
214
215
case 'array':
216
const arrayPathCheck = isInspectedPath(path, inspectedPaths);
217
if (level >= LEVEL_THRESHOLD && !arrayPathCheck) {
203
- return createDehydrated(type, data, cleaned, path);
218
+ return createDehydrated(type, true, data, cleaned, path);
219
}
220
return data.map((item, i) =>
221
dehydrate(
@@ -214,24 +229,24 @@ export function dehydrate(
229
230
case 'typed_array':
231
case 'iterator':
217
- return createDehydrated(type, data, cleaned, path);
232
+ return createDehydrated(type, false, data, cleaned, path);
233
+
234
case 'date':
235
cleaned.push(path);
236
return {
237
+ inspectable: false,
238
name: data.toString(),
239
type: 'date',
223
- meta: {
224
- uninspectable: true,
225
- },
240
};
241
+
242
case 'object':
243
const objectPathCheck = isInspectedPath(path, inspectedPaths);
244
if (level >= LEVEL_THRESHOLD && !objectPathCheck) {
230
- return createDehydrated(type, data, cleaned, path);
245
+ return createDehydrated(type, true, data, cleaned, path);
246
} else {
232
- const res = {};
247
+ const object = {};
248
for (let name in data) {
234
- res[name] = dehydrate(
249
+ object[name] = dehydrate(
250
data[name],
251
cleaned,
252
path.concat([name]),
@@ -239,7 +254,7 @@ export function dehydrate(
254
objectPathCheck ? 1 : level + 1
255
);
256
}
242
- return res;
257
+ return object;
258
}
259
260
default:
@@ -252,16 +267,16 @@ export function fillInPath(
267
path: Array<string | number>,
268
value: any
269
) {
255
- const length = path.length;
256
- const parent = getInObject(object, path.slice(0, length - 1));
257
- if (object != null) {
258
- delete parent[meta.name];
259
- delete parent[meta.type];
260
- delete parent[meta.meta];
261
- delete parent[meta.inspected];
262
-
263
- setInObject(object, path, value);
270
+ const target = getInObject(object, path);
271
+ if (target != null) {
272
+ delete target[meta.inspectable];
273
+ delete target[meta.inspected];
274
+ delete target[meta.name];
275
+ delete target[meta.readonly];
276
+ delete target[meta.size];
277
+ delete target[meta.type];
278
}
279
+ setInObject(object, path, value);
280
}
281
282
export function hydrate(
@@ -280,9 +295,11 @@ export function hydrate(
295
296
// Replace the string keys with Symbols so they're non-enumerable.
297
const replaced: { [key: Symbol]: boolean | string } = {};
298
+ replaced[meta.inspectable] = !!value.inspectable;
299
replaced[meta.inspected] = false;
284
- replaced[meta.meta] = value.meta;
300
replaced[meta.name] = value.name;
301
+ replaced[meta.size] = value.size;
302
+ replaced[meta.readonly] = !!value.readonly;
303
replaced[meta.type] = value.type;
304
305
parent[last] = replaced;