@samitouri / QOS-React-2 / commits / 7a94ad4e8a

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;