@samitouri / QOS-React-2 / commits / 564a223368

Fetch owners list from renderer (using suspense)

Owners in the list may have been filtered out of the Store, but in the owners list view- it's important to still show them. The frontend cannot do this on its own, so this list needs to come from the renderer interface.

Brian Vaughn committed May 9, 2019 at 11:47 UTC 564a2233685d52fceccfd2f4a515a654ec6f9879
13 files changed +404 -182
src/backend/agent.js
+17 -5
@@ -16,6 +16,7 @@ import type {
16 RendererID,
17 RendererInterface,
18 } from './types';
19 +import type { OwnersList } from 'src/devtools/views/Components/types';
20 import type { Bridge, ComponentFilter } from '../types';
21
22 const debug = (methodName, ...args) => {
@@ -29,7 +30,7 @@ const debug = (methodName, ...args) => {
30 }
31 };
32
32 -type InspectSelectParams = {|
33 +type ElementAndRendererID = {|
34 id: number,
35 rendererID: number,
36 |};
@@ -99,6 +100,7 @@ export default class Agent extends EventEmitter {
100 bridge.addListener('getProfilingStatus', this.getProfilingStatus);
101 bridge.addListener('getProfilingSummary', this.getProfilingSummary);
102 bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
103 + bridge.addListener('getOwnersList', this.getOwnersList);
104 bridge.addListener('inspectElement', this.inspectElement);
105 bridge.addListener('logElementToConsole', this.logElementToConsole);
106 bridge.addListener('overrideContext', this.overrideContext);
@@ -303,7 +305,17 @@ export default class Agent extends EventEmitter {
305 }
306 };
307
306 - inspectElement = ({ id, rendererID }: InspectSelectParams) => {
308 + getOwnersList = ({ id, rendererID }: ElementAndRendererID) => {
309 + const renderer = this._rendererInterfaces[rendererID];
310 + if (renderer == null) {
311 + console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
312 + } else {
313 + const owners = renderer.getOwnersList(id);
314 + this._bridge.send('ownersList', ({ id, owners }: OwnersList));
315 + }
316 + };
317 +
318 + inspectElement = ({ id, rendererID }: ElementAndRendererID) => {
319 const renderer = this._rendererInterfaces[rendererID];
320 if (renderer == null) {
321 console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
@@ -312,7 +324,7 @@ export default class Agent extends EventEmitter {
324 }
325 };
326
315 - logElementToConsole = ({ id, rendererID }: InspectSelectParams) => {
327 + logElementToConsole = ({ id, rendererID }: ElementAndRendererID) => {
328 const renderer = this._rendererInterfaces[rendererID];
329 if (renderer == null) {
330 console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
@@ -340,7 +352,7 @@ export default class Agent extends EventEmitter {
352 this._bridge.send('screenshotCaptured', { commitIndex, dataURL });
353 };
354
343 - selectElement = ({ id, rendererID }: InspectSelectParams) => {
355 + selectElement = ({ id, rendererID }: ElementAndRendererID) => {
356 const renderer = this._rendererInterfaces[rendererID];
357 if (renderer == null) {
358 console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
@@ -506,7 +518,7 @@ export default class Agent extends EventEmitter {
518 }
519 };
520
509 - viewElementSource = ({ id, rendererID }: InspectSelectParams) => {
521 + viewElementSource = ({ id, rendererID }: ElementAndRendererID) => {
522 const renderer = this._rendererInterfaces[rendererID];
523 if (renderer == null) {
524 console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
src/backend/renderer.js
+30 -16
@@ -50,7 +50,10 @@ import type {
50 ReactRenderer,
51 RendererInterface,
52 } from './types';
53 -import type { InspectedElement } from 'src/devtools/views/Components/types';
53 +import type {
54 + InspectedElement,
55 + Owner,
56 +} from 'src/devtools/views/Components/types';
57 import type { ComponentFilter, ElementType } from 'src/types';
58
59 function getInternalReactConstants(version) {
@@ -1685,6 +1688,30 @@ export function attach(
1688 }
1689 }
1690
1691 + function getOwnersList(id: number): Array<Owner> | null {
1692 + let fiber = findCurrentFiberUsingSlowPathById(id);
1693 + if (fiber == null) {
1694 + return null;
1695 + }
1696 +
1697 + const { _debugOwner } = fiber;
1698 +
1699 + let owners = null;
1700 + if (_debugOwner) {
1701 + owners = [];
1702 + let owner = _debugOwner;
1703 + while (owner !== null) {
1704 + owners.push({
1705 + displayName: getDisplayNameForFiber(owner) || 'Unknown',
1706 + id: getFiberID(getPrimaryFiber(owner)),
1707 + });
1708 + owner = owner._debugOwner || null;
1709 + }
1710 + }
1711 +
1712 + return owners;
1713 + }
1714 +
1715 function inspectElementRaw(id: number): InspectedElement | null {
1716 let fiber = findCurrentFiberUsingSlowPathById(id);
1717 if (fiber == null) {
@@ -1692,7 +1719,6 @@ export function attach(
1719 }
1720
1721 const {
1695 - _debugOwner,
1722 _debugSource,
1723 stateNode,
1724 memoizedProps,
@@ -1764,19 +1790,6 @@ export function attach(
1790 context = { value: context };
1791 }
1792
1767 - let owners = null;
1768 - if (_debugOwner) {
1769 - owners = [];
1770 - let owner = _debugOwner;
1771 - while (owner !== null) {
1772 - owners.push({
1773 - displayName: getDisplayNameForFiber(owner) || 'Unknown',
1774 - id: getFiberID(getPrimaryFiber(owner)),
1775 - });
1776 - owner = owner._debugOwner || null;
1777 - }
1778 - }
1779 -
1793 const isTimedOutSuspense =
1794 tag === SuspenseComponent && memoizedState !== null;
1795
@@ -1812,7 +1825,7 @@ export function attach(
1825 state: usesHooks ? null : memoizedState,
1826
1827 // List of owners
1815 - owners,
1828 + owners: getOwnersList(id),
1829
1830 // Location of component in source coude.
1831 source: _debugSource,
@@ -2385,6 +2398,7 @@ export function attach(
2398 getFiberCommits,
2399 getInteractions,
2400 findNativeByFiberID,
2401 + getOwnersList,
2402 getPathForElement,
2403 getProfilingDataForDownload,
2404 getProfilingSummary,
src/backend/types.js
+5 -1
@@ -1,7 +1,10 @@
1 // @flow
2
3 import type { ComponentFilter, ElementType } from 'src/types';
4 -import type { InspectedElement } from 'src/devtools/views/Components/types';
4 +import type {
5 + InspectedElement,
6 + Owner,
7 +} from 'src/devtools/views/Components/types';
8
9 type BundleType =
10 | 0 // PROD
@@ -175,6 +178,7 @@ export type RendererInterface = {
178 ) => number | null,
179 getFiberCommits: (rootID: number, fiberID: number) => FiberCommitsBackend,
180 getInteractions: (rootID: number) => InteractionsBackend,
181 + getOwnersList: (id: number) => Array<Owner> | null,
182 getProfilingDataForDownload: (rootID: number) => Object,
183 getProfilingSummary: (rootID: number) => ProfilingSummaryBackend,
184 getPathForElement: (id: number) => Array<PathFrame> | null,
src/devtools/views/Components/Components.js
+16 -13
@@ -4,6 +4,7 @@ import React, { Suspense } from 'react';
4 import Tree from './Tree';
5 import SelectedElement from './SelectedElement';
6 import { InspectedElementContextController } from './InspectedElementContext';
7 +import { OwnersListContextController } from './OwnersListContext';
8 import portaledContent from '../portaledContent';
9 import { ModalDialog } from '../ModalDialog';
10
@@ -12,19 +13,21 @@ import styles from './Components.css';
13 function Components(_: {||}) {
14 // TODO Flex wrappers below should be user resizable.
15 return (
15 - <div className={styles.Components}>
16 - <div className={styles.TreeWrapper}>
17 - <Tree />
18 - </div>
19 - <div className={styles.SelectedElementWrapper}>
20 - <InspectedElementContextController>
21 - <Suspense fallback={<Loading />}>
22 - <SelectedElement />
23 - </Suspense>
24 - </InspectedElementContextController>
25 - </div>
26 - <ModalDialog />
27 - </div>
16 + <OwnersListContextController>
17 + <InspectedElementContextController>
18 + <div className={styles.Components}>
19 + <div className={styles.TreeWrapper}>
20 + <Tree />
21 + </div>
22 + <div className={styles.SelectedElementWrapper}>
23 + <Suspense fallback={<Loading />}>
24 + <SelectedElement />
25 + </Suspense>
26 + </div>
27 + <ModalDialog />
28 + </div>
29 + </InspectedElementContextController>
30 + </OwnersListContextController>
31 );
32 }
33
src/devtools/views/Components/Element.js
+2 -2
@@ -29,7 +29,7 @@ type Props = {
29
30 export default function ElementView({ data, index, style }: Props) {
31 const store = useContext(StoreContext);
32 - const { ownerFlatTree, ownerStack, selectedElementID } = useContext(
32 + const { ownerFlatTree, ownerID, selectedElementID } = useContext(
33 TreeStateContext
34 );
35 const dispatch = useContext(TreeDispatcherContext);
@@ -168,7 +168,7 @@ export default function ElementView({ data, index, style }: Props) {
168 }}
169 >
170 <span className={styles.ScrollAnchor} ref={scrollAnchorStartRef} />
171 - {ownerStack.length === 0 ? (
171 + {ownerID === null ? (
172 <ExpandCollapseToggle element={element} store={store} />
173 ) : null}
174 <span className={styles.Component}>
src/devtools/views/Components/OwnersListContext.js new
+109
@@ -0,0 +1,109 @@
1 +// @flow
2 +
3 +import React, {
4 + createContext,
5 + useCallback,
6 + useContext,
7 + useEffect,
8 +} from 'react';
9 +import { createResource } from '../../cache';
10 +import { BridgeContext, StoreContext } from '../context';
11 +import { TreeStateContext } from './TreeContext';
12 +
13 +import type {
14 + Element,
15 + Owner,
16 + OwnersList,
17 +} from 'src/devtools/views/Components/types';
18 +import type { Resource, Thenable } from '../../cache';
19 +
20 +type Context = (id: number) => Array<Owner> | null;
21 +
22 +const OwnersListContext = createContext<Context>(((null: any): Context));
23 +OwnersListContext.displayName = 'OwnersListContext';
24 +
25 +type ResolveFn = (ownersList: Array<Owner> | null) => void;
26 +type InProgressRequest = {|
27 + promise: Thenable<Array<Owner>>,
28 + resolveFn: ResolveFn,
29 +|};
30 +
31 +const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap();
32 +const resource: Resource<Element, Element, Array<Owner>> = createResource(
33 + (element: Element) => {
34 + let request = inProgressRequests.get(element);
35 + if (request != null) {
36 + return request.promise;
37 + }
38 +
39 + let resolveFn = ((null: any): ResolveFn);
40 + const promise = new Promise(resolve => {
41 + resolveFn = resolve;
42 + });
43 +
44 + inProgressRequests.set(element, { promise, resolveFn });
45 +
46 + return promise;
47 + },
48 + (element: Element) => element,
49 + { useWeakMap: true }
50 +);
51 +
52 +type Props = {|
53 + children: React$Node,
54 +|};
55 +
56 +function OwnersListContextController({ children }: Props) {
57 + const bridge = useContext(BridgeContext);
58 + const store = useContext(StoreContext);
59 + const { ownerID } = useContext(TreeStateContext);
60 +
61 + const read = useCallback(
62 + (id: number) => {
63 + const element = store.getElementByID(id);
64 + if (element !== null) {
65 + return resource.read(element);
66 + } else {
67 + return null;
68 + }
69 + },
70 + [store]
71 + );
72 +
73 + useEffect(() => {
74 + const onOwnersList = (ownersList: OwnersList) => {
75 + const id = ownersList.id;
76 +
77 + const element = store.getElementByID(id);
78 + if (element !== null) {
79 + const request = inProgressRequests.get(element);
80 + if (request != null) {
81 + inProgressRequests.delete(element);
82 + request.resolveFn(ownersList.owners);
83 + }
84 + }
85 + };
86 +
87 + bridge.addListener('ownersList', onOwnersList);
88 + return () => bridge.removeListener('ownersList', onOwnersList);
89 + }, [bridge, store]);
90 +
91 + // This effect requests an updated owners list any time the selected owner changes
92 + useEffect(() => {
93 + if (ownerID !== null) {
94 + const rendererID = store.getRendererIDForElement(ownerID);
95 +
96 + bridge.send('getOwnersList', { id: ownerID, rendererID });
97 + }
98 +
99 + return () => {};
100 + }, [bridge, ownerID, store]);
101 +
102 + return (
103 + <OwnersListContext.Provider value={read}>
104 + {children}
105 + </OwnersListContext.Provider>
106 + );
107 +}
108 +
109 +export { OwnersListContext, OwnersListContextController };
src/devtools/views/Components/OwnersStack.css
+5
@@ -91,3 +91,8 @@
91 font-family: var(--font-family-monospace);
92 font-size: var(--font-size-monospace-normal);
93 }
94 +
95 +.NotInStore,
96 +.NotInStore:hover {
97 + color: var(--color-dimmest);
98 +}
src/devtools/views/Components/OwnersStack.js
+156 -56
@@ -4,6 +4,7 @@ import React, {
4 useCallback,
5 useContext,
6 useLayoutEffect,
7 + useReducer,
8 useRef,
9 useState,
10 } from 'react';
@@ -12,22 +13,111 @@ import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
13 import Button from '../Button';
14 import ButtonIcon from '../ButtonIcon';
15 import Toggle from '../Toggle';
16 +import { OwnersListContext } from './OwnersListContext';
17 import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
16 -import { StoreContext } from '../context';
18 import { useIsOverflowing } from '../hooks';
19 +import { StoreContext } from '../context';
20
19 -import type { Element } from './types';
21 +import type { Owner } from './types';
22
23 import styles from './OwnersStack.css';
24
25 +type SelectOwner = (owner: Owner | null) => void;
26 +
27 +type ACTION_UPDATE_OWNER_ID = {|
28 + type: 'UPDATE_OWNER_ID',
29 + ownerID: number | null,
30 + owners: Array<Owner>,
31 +|};
32 +type ACTION_UPDATE_SELECTED_INDEX = {|
33 + type: 'UPDATE_SELECTED_INDEX',
34 + selectedIndex: number,
35 +|};
36 +
37 +type Action = ACTION_UPDATE_OWNER_ID | ACTION_UPDATE_SELECTED_INDEX;
38 +
39 +type State = {|
40 + ownerID: number | null,
41 + owners: Array<Owner>,
42 + selectedIndex: number,
43 +|};
44 +
45 +function dialogReducer(state, action) {
46 + switch (action.type) {
47 + case 'UPDATE_OWNER_ID':
48 + const selectedIndex = state.owners.findIndex(
49 + owner => owner.id === action.ownerID
50 + );
51 + return {
52 + ownerID: action.ownerID,
53 + owners: action.owners,
54 + selectedIndex,
55 + };
56 + case 'UPDATE_SELECTED_INDEX':
57 + return {
58 + ...state,
59 + selectedIndex: action.selectedIndex,
60 + };
61 + default:
62 + throw new Error(`Invalid action "${action.type}"`);
63 + }
64 +}
65 +
66 export default function OwnerStack() {
24 - const { ownerStack, ownerStackIndex } = useContext(TreeStateContext);
25 - const dispatch = useContext(TreeDispatcherContext);
67 + const read = useContext(OwnersListContext);
68 + const { ownerID } = useContext(TreeStateContext);
69 + const treeDispatch = useContext(TreeDispatcherContext);
70 +
71 + const [state, dispatch] = useReducer<State, Action>(dialogReducer, {
72 + ownerID: null,
73 + owners: [],
74 + selectedIndex: -1,
75 + });
76 +
77 + // TODO (owners) Explain this and use reducer with ownerID too to avoid inf. loop
78 + if (ownerID === null) {
79 + dispatch({
80 + type: 'UPDATE_OWNER_ID',
81 + ownerID: null,
82 + owners: [],
83 + });
84 + } else if (ownerID !== state.ownerID) {
85 + const isInList = state.owners.findIndex(owner => owner.id === ownerID) >= 0;
86 + dispatch({
87 + type: 'UPDATE_OWNER_ID',
88 + ownerID,
89 + owners: isInList ? state.owners : read(ownerID) || [],
90 + });
91 + }
92 +
93 + const { owners, selectedIndex } = state;
94 +
95 + const selectOwner = useCallback<SelectOwner>(
96 + (owner: Owner | null) => {
97 + if (owner !== null) {
98 + const index = owners.indexOf(owner);
99 + dispatch({
100 + type: 'UPDATE_SELECTED_INDEX',
101 + selectedIndex: index >= 0 ? index : 0,
102 + });
103 + treeDispatch({ type: 'SELECT_OWNER', payload: owner.id });
104 + } else {
105 + dispatch({
106 + type: 'UPDATE_SELECTED_INDEX',
107 + selectedIndex: 0,
108 + });
109 + treeDispatch({ type: 'RESET_OWNER_STACK' });
110 + }
111 + },
112 + [owners, treeDispatch]
113 + );
114
115 const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
116 const elementsBarRef = useRef<HTMLDivElement | null>(null);
117 const isOverflowing = useIsOverflowing(elementsBarRef, elementsTotalWidth);
118
119 + const selectedOwner = owners[selectedIndex];
120 +
121 useLayoutEffect(() => {
122 // If we're already overflowing, then we don't need to re-measure items.
123 // That's because once the owners stack is open, it can only get larger (by driling in).
@@ -37,7 +127,7 @@ export default function OwnerStack() {
127 }
128
129 let elementsTotalWidth = 0;
40 - for (let i = 0; i < ownerStack.length; i++) {
130 + for (let i = 0; i < owners.length; i++) {
131 const element = elementsBarRef.current.children[i];
132 const computedStyle = getComputedStyle(element);
133
@@ -48,7 +138,7 @@ export default function OwnerStack() {
138 }
139
140 setElementsTotalWidth(elementsTotalWidth);
51 - }, [elementsBarRef, isOverflowing, ownerStack.length]);
141 + }, [elementsBarRef, isOverflowing, owners.length]);
142
143 return (
144 <div className={styles.OwnerStack}>
@@ -56,28 +146,38 @@ export default function OwnerStack() {
146 {isOverflowing && (
147 <Fragment>
148 <ElementsDropdown
59 - ownerStack={ownerStack}
60 - ownerStackIndex={ownerStackIndex}
149 + owners={owners}
150 + selectedIndex={selectedIndex}
151 + selectOwner={selectOwner}
152 />
153 <BackToOwnerButton
63 - ownerStack={ownerStack}
64 - ownerStackIndex={ownerStackIndex}
65 - />
66 - <ElementView
67 - id={ownerStack[((ownerStackIndex: any): number)]}
68 - index={ownerStackIndex}
154 + owners={owners}
155 + selectedIndex={selectedIndex}
156 + selectOwner={selectOwner}
157 />
158 + {selectedOwner != null && (
159 + <ElementView
160 + owner={selectedOwner}
161 + isSelected
162 + selectOwner={selectOwner}
163 + />
164 + )}
165 </Fragment>
166 )}
167 {!isOverflowing &&
73 - ownerStack.map((id, index) => (
74 - <ElementView key={id} id={id} index={index} />
168 + owners.map((owner, index) => (
169 + <ElementView
170 + key={index}
171 + owner={owner}
172 + isSelected={index === selectedIndex}
173 + selectOwner={selectOwner}
174 + />
175 ))}
176 </div>
177 <div className={styles.VRule} />
178 <Button
179 className={styles.IconButton}
80 - onClick={() => dispatch({ type: 'RESET_OWNER_STACK' })}
180 + onClick={() => selectOwner(null)}
181 title="Back to tree view"
182 >
183 <ButtonIcon type="close" />
@@ -87,26 +187,28 @@ export default function OwnerStack() {
187 }
188
189 type ElementsDropdownProps = {
90 - ownerStack: Array<number>,
91 - ownerStackIndex: number | null,
190 + owners: Array<Owner>,
191 + selectedIndex: number,
192 + selectOwner: SelectOwner,
193 };
194 function ElementsDropdown({
94 - ownerStack,
95 - ownerStackIndex,
195 + owners,
196 + selectedIndex,
197 + selectOwner,
198 }: ElementsDropdownProps) {
199 const store = useContext(StoreContext);
98 - const dispatch = useContext(TreeDispatcherContext);
200
201 const menuItems = [];
101 - for (let index = ownerStack.length - 1; index >= 0; index--) {
102 - const id = ownerStack[index];
202 + for (let index = owners.length - 1; index >= 0; index--) {
203 + const owner = owners[index];
204 + const isInStore = store.containsElement(owner.id);
205 menuItems.push(
206 <MenuItem
105 - key={id}
106 - className={styles.Component}
107 - onSelect={() => dispatch({ type: 'SELECT_OWNER', payload: id })}
207 + key={owner.id}
208 + className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
209 + onSelect={() => (isInStore ? selectOwner(owner) : null)}
210 >
109 - {((store.getElementByID(id): any): Element).displayName}
211 + {owner.displayName}
212 </MenuItem>
213 );
214 }
@@ -126,28 +228,26 @@ function ElementsDropdown({
228 }
229
230 type ElementViewProps = {
129 - id: number,
130 - index: number | null,
231 + isSelected: boolean,
232 + owner: Owner,
233 + selectOwner: SelectOwner,
234 };
132 -function ElementView({ id, index }: ElementViewProps) {
235 +function ElementView({ isSelected, owner, selectOwner }: ElementViewProps) {
236 const store = useContext(StoreContext);
134 - const { ownerStackIndex } = useContext(TreeStateContext);
135 - const dispatch = useContext(TreeDispatcherContext);
136 -
137 - const { displayName } = ((store.getElementByID(id): any): Element);
237
139 - const isChecked = ownerStackIndex === index;
238 + const { displayName } = owner;
239 + const isInStore = store.containsElement(owner.id);
240
241 const handleChange = useCallback(() => {
142 - if (!isChecked) {
143 - dispatch({ type: 'SELECT_OWNER', payload: id });
242 + if (isInStore) {
243 + selectOwner(owner);
244 }
145 - }, [dispatch, id, isChecked]);
245 + }, [isInStore, selectOwner, owner]);
246
247 return (
248 <Toggle
149 - className={styles.Component}
150 - isChecked={isChecked}
249 + className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
250 + isChecked={isSelected}
251 onChange={handleChange}
252 >
253 {displayName}
@@ -156,32 +256,32 @@ function ElementView({ id, index }: ElementViewProps) {
256 }
257
258 type BackToOwnerButtonProps = {|
159 - ownerStack: Array<number>,
160 - ownerStackIndex: number | null,
259 + owners: Array<Owner>,
260 + selectedIndex: number,
261 + selectOwner: SelectOwner,
262 |};
263 function BackToOwnerButton({
163 - ownerStack,
164 - ownerStackIndex,
264 + owners,
265 + selectedIndex,
266 + selectOwner,
267 }: BackToOwnerButtonProps) {
268 const store = useContext(StoreContext);
167 - const dispatch = useContext(TreeDispatcherContext);
269
169 - if (ownerStackIndex === null || ownerStackIndex === 0) {
270 + if (selectedIndex <= 0) {
271 return null;
272 }
273
173 - const ownerID = ownerStack[ownerStackIndex - 1];
174 - const owner = store.getElementByID(ownerID);
274 + const owner = owners[selectedIndex - 1];
275 + if (owner == null) {
276 + debugger;
277 + }
278 + const isInStore = store.containsElement(owner.id);
279
280 return (
281 <Button
178 - onClick={() =>
179 - dispatch({
180 - type: 'SELECT_OWNER',
181 - payload: ownerID,
182 - })
183 - }
184 - title={`Up to ${(owner !== null && owner.displayName) || 'owner'}`}
282 + className={isInStore ? undefined : styles.NotInStore}
283 + onClick={() => (isInStore ? selectOwner(owner) : null)}
284 + title={`Up to ${owner.displayName || 'owner'}`}
285 >
286 <ButtonIcon type="previous" />
287 </Button>
src/devtools/views/Components/SelectedElement.js
+2 -2
@@ -227,7 +227,7 @@ function InspectedElementView({
227 state,
228 } = inspectedElement;
229
230 - const { ownerStack } = useContext(TreeStateContext);
230 + const { ownerID } = useContext(TreeStateContext);
231 const bridge = useContext(BridgeContext);
232 const store = useContext(StoreContext);
233
@@ -298,7 +298,7 @@ function InspectedElementView({
298 overrideValueFn={overrideContextFn}
299 />
300
301 - {ownerStack.length === 0 && owners !== null && owners.length > 0 && (
301 + {ownerID === null && owners !== null && owners.length > 0 && (
302 <div className={styles.Owners}>
303 <div className={styles.OwnersHeader}>rendered by</div>
304 {owners.map(owner => (
src/devtools/views/Components/Tree.css
+11
@@ -37,3 +37,14 @@
37 margin: 0 0.5rem;
38 background-color: var(--color-border);
39 }
40 +
41 +.Loading {
42 + height: 100%;
43 + padding-left: 0.5rem;
44 + display: flex;
45 + align-items: center;
46 + flex: 1;
47 + justify-content: flex-start;
48 + font-size: var(--font-size-sans-large);
49 + color: var(--color-dim);
50 +}
src/devtools/views/Components/Tree.js
+13 -7
@@ -1,6 +1,7 @@
1 // @flow
2
3 import React, {
4 + Suspense,
5 useState,
6 useCallback,
7 useContext,
@@ -38,7 +39,7 @@ export default function Tree(props: Props) {
39 const dispatch = useContext(TreeDispatcherContext);
40 const {
41 numElements,
41 - ownerStack,
42 + ownerID,
43 searchIndex,
44 searchResults,
45 selectedElementID,
@@ -277,7 +278,9 @@ export default function Tree(props: Props) {
278 <div className={styles.SearchInput}>
279 <InspectHostNodesToggle />
280 <div className={styles.VRule} />
280 - {ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
281 + <Suspense fallback={<Loading />}>
282 + {ownerID !== null ? <OwnersStack /> : <SearchInput />}
283 + </Suspense>
284 <div className={styles.VRule} />
285 <ToggleComponentFiltersModalButton />
286 </div>
@@ -317,7 +320,7 @@ export default function Tree(props: Props) {
320 }
321
322 function InnerElementType({ style, ...rest }) {
320 - const { ownerStack } = useContext(TreeStateContext);
323 + const { ownerID } = useContext(TreeStateContext);
324
325 // The list may need to scroll horizontally due to deeply nested elements.
326 // We don't know the maximum scroll width up front, because we're windowing.
@@ -346,10 +349,9 @@ function InnerElementType({ style, ...rest }) {
349
350 // We shouldn't retain this width across different conceptual trees though,
351 // so when the user opens the "owners tree" view, we should discard the previous width.
349 - const hasOwnerStack = ownerStack.length > 0;
350 - const [prevHasOwnerStack, setPrevHasOwnerStack] = useState(hasOwnerStack);
351 - if (hasOwnerStack !== prevHasOwnerStack) {
352 - setPrevHasOwnerStack(hasOwnerStack);
352 + const [prevOwnerID, setPrevOwnerID] = useState(ownerID);
353 + if (ownerID !== prevOwnerID) {
354 + setPrevOwnerID(ownerID);
355 setMinWidth(null);
356 }
357
@@ -371,3 +373,7 @@ function InnerElementType({ style, ...rest }) {
373 />
374 );
375 }
376 +
377 +function Loading() {
378 + return <div className={styles.Loading}>Loading...</div>;
379 +}
src/devtools/views/Components/TreeContext.js
+33 -80
@@ -50,9 +50,8 @@ type StateContext = {|
50 searchText: string,
51
52 // Owners
53 + ownerID: number | null,
54 ownerFlatTree: Array<Element> | null,
54 - ownerStack: Array<number>,
55 - ownerStackIndex: number | null,
55
56 // Inspection element panel
57 inspectedElementID: number | null,
@@ -142,8 +141,7 @@ type State = {|
141 searchText: string,
142
143 // Owners
145 - ownerStack: Array<number>,
146 - ownerStackIndex: number | null,
144 + ownerID: number | null,
145 ownerFlatTree: Array<Element> | null,
146
147 // Inspection element panel
@@ -151,17 +149,12 @@ type State = {|
149 |};
150
151 function reduceTreeState(store: Store, state: State, action: Action): State {
154 - let {
155 - numElements,
156 - ownerStack,
157 - selectedElementIndex,
158 - selectedElementID,
159 - } = state;
152 + let { numElements, ownerID, selectedElementIndex, selectedElementID } = state;
153
154 let lookupIDForIndex = true;
155
156 // Base tree should ignore selected element changes when the owner's tree is active.
164 - if (ownerStack.length === 0) {
157 + if (ownerID === null) {
158 switch (action.type) {
159 case 'HANDLE_STORE_MUTATION':
160 numElements = store.numElements;
@@ -276,7 +269,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
269
270 function reduceSearchState(store: Store, state: State, action: Action): State {
271 let {
279 - ownerStack,
272 + ownerID,
273 searchIndex,
274 searchResults,
275 searchText,
@@ -295,7 +288,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
288 let didRequestSearch = false;
289
290 // Search isn't supported when the owner's tree is active.
298 - if (ownerStack.length === 0) {
291 + if (ownerID === null) {
292 switch (action.type) {
293 case 'GO_TO_NEXT_SEARCH_RESULT':
294 if (numPrevSearchResults > 0) {
@@ -442,9 +435,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
435 numElements,
436 selectedElementID,
437 selectedElementIndex,
438 + ownerID,
439 ownerFlatTree,
446 - ownerStack,
447 - ownerStackIndex,
440 searchIndex,
441 searchResults,
442 searchText,
@@ -454,25 +446,9 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
446
447 switch (action.type) {
448 case 'HANDLE_STORE_MUTATION':
457 - if (ownerStack.length > 0) {
458 - let indexOfRemovedItem = -1;
459 - for (let i = 0; i < ownerStack.length; i++) {
460 - if (store.getElementByID(ownerStack[i]) === null) {
461 - indexOfRemovedItem = i;
462 - break;
463 - }
464 - }
465 -
466 - if (indexOfRemovedItem >= 0) {
467 - ownerStack = ownerStack.slice(0, indexOfRemovedItem);
468 - if (ownerStack.length === 0) {
469 - ownerFlatTree = null;
470 - ownerStackIndex = null;
471 - } else {
472 - ownerStackIndex = ownerStack.length - 1;
473 - }
474 - }
475 - if (selectedElementID !== null && ownerFlatTree !== null) {
449 + if (ownerID !== null) {
450 + ownerFlatTree = store.getOwnersListForElement(ownerID);
451 + if (selectedElementID !== null) {
452 // Mutation might have caused the index of this ID to shift.
453 selectedElementIndex = ownerFlatTree.findIndex(
454 element => element.id === selectedElementID
@@ -491,13 +467,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
467 }
468 break;
469 case 'RESET_OWNER_STACK':
494 - ownerStack = [];
495 - ownerStackIndex = null;
470 + ownerID = null;
471 + ownerFlatTree = null;
472 selectedElementIndex =
473 selectedElementID !== null
474 ? store.getIndexOfElementID(selectedElementID)
475 : null;
500 - ownerFlatTree = null;
476 break;
477 case 'SELECT_ELEMENT_AT_INDEX':
478 if (ownerFlatTree !== null) {
@@ -533,33 +508,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
508 // If the Store doesn't have any owners metadata, don't drill into an empty stack.
509 // This is a confusing user experience.
510 if (store.hasOwnerMetadata) {
536 - const id = (action: ACTION_SELECT_OWNER).payload;
537 - ownerStackIndex = ownerStack.indexOf(id);
511 + ownerID = (action: ACTION_SELECT_OWNER).payload;
512 + ownerFlatTree = store.getOwnersListForElement(ownerID);
513
514 // Always force reset selection to be the top of the new owner tree.
515 selectedElementIndex = 0;
516 prevSelectedElementIndex = null;
542 -
543 - // If this owner is already in the current stack, just select it.
544 - // Otherwise, create a new stack.
545 - if (ownerStackIndex < 0) {
546 - // Add this new owner, and fill in the owners above it as well.
547 - ownerStack = [];
548 - let currentOwnerID = id;
549 - while (currentOwnerID !== 0) {
550 - ownerStack.unshift(currentOwnerID);
551 - currentOwnerID = ((store.getElementByID(
552 - currentOwnerID
553 - ): any): Element).ownerID;
554 - }
555 - ownerStackIndex = ownerStack.length - 1;
556 -
557 - if (searchText !== '') {
558 - searchIndex = null;
559 - searchResults = [];
560 - searchText = '';
561 - }
562 - }
517 }
518 break;
519 default:
@@ -569,17 +523,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
523
524 // Changes in the selected owner require re-calculating the owners tree.
525 if (
572 - ownerStackIndex !== state.ownerStackIndex ||
573 - ownerStack !== state.ownerStack ||
526 + ownerFlatTree !== state.ownerFlatTree ||
527 action.type === 'HANDLE_STORE_MUTATION'
528 ) {
576 - if (ownerStackIndex === null) {
577 - ownerFlatTree = null;
529 + if (ownerFlatTree === null) {
530 numElements = store.numElements;
531 } else {
580 - ownerFlatTree = store.getOwnersListForElement(
581 - ownerStack[ownerStackIndex]
582 - );
532 numElements = ownerFlatTree.length;
533 }
534 }
@@ -588,9 +537,10 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
537 if (selectedElementIndex !== prevSelectedElementIndex) {
538 if (selectedElementIndex === null) {
539 selectedElementID = null;
591 - } else if (ownerFlatTree !== null) {
592 - selectedElementID =
593 - ownerFlatTree[((selectedElementIndex: any): number)].id;
540 + } else {
541 + if (ownerFlatTree !== null) {
542 + selectedElementID = ownerFlatTree[selectedElementIndex].id;
543 + }
544 }
545 }
546
@@ -605,8 +555,7 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
555 searchResults,
556 searchText,
557
608 - ownerStack,
609 - ownerStackIndex,
558 + ownerID,
559 ownerFlatTree,
560 };
561 }
@@ -619,14 +568,19 @@ function reduceSuspenseState(
568 const { type } = action;
569 switch (type) {
570 case 'UPDATE_INSPECTED_ELEMENT_ID':
622 - return {
623 - ...state,
624 - inspectedElementID: state.selectedElementID,
625 - };
571 + if (state.inspectedElementID !== state.selectedElementID) {
572 + return {
573 + ...state,
574 + inspectedElementID: state.selectedElementID,
575 + };
576 + }
577 + break;
578 default:
627 - // React can bailout of no-op updates.
628 - return state;
579 + break;
580 }
581 +
582 + // React can bailout of no-op updates.
583 + return state;
584 }
585
586 type Props = {| children: React$Node |};
@@ -696,8 +650,7 @@ function TreeContextController({ children }: Props) {
650 searchText: '',
651
652 // Owners
699 - ownerStack: [],
700 - ownerStackIndex: null,
653 + ownerID: null,
654 ownerFlatTree: null,
655
656 // Inspection element panel
src/devtools/views/Components/types.js
+5
@@ -35,6 +35,11 @@ export type Owner = {|
35 id: number,
36 |};
37
38 +export type OwnersList = {|
39 + id: number,
40 + owners: Array<Owner> | null,
41 +|};
42 +
43 export type InspectedElement = {|
44 id: number,
45