@samitouri / QOS-React / commits / 04ec50efa9

[DevTools] Add Filtering of Environment Names (#30850)

Stacked on #30842. This adds a filter to be able to exclude Components from a certain environment. Default to Client or Server. The available options are computed into a dropdown based on the names that are currently used on the page (or an option that were previously used). In addition to the hardcoded "Client". Meaning that if you have Server Components on the page you see "Server" or "Client" as possible options but it can be anything if there are multiple RSC environments on the page. "Client" in this case means Function and Class Components in Fiber - excluding built-ins. If a Server Component has two environments (primary and secondary) then both have to be filtered to exclude it. We don't show the option at all if there are no Server Components used in the page to avoid confusing existing users that are just using Client Components and wouldn't know the difference between Server vs Client. <img width="815" alt="Screenshot 2024-08-30 at 12 56 42 AM" src="https://github.com/user-attachments/assets/e06b225a-e85d-4cdc-8707-d4630fede19e">

Sebastian Markbåge committed Sep 3, 2024 at 12:29 UTC 04ec50efa941a7f07e8231a87e72d6d851948b8c
10 files changed +253 -26
packages/react-devtools-shared/src/__tests__/utils.js
+13
@@ -284,6 +284,19 @@ export function createHOCFilter(isEnabled: boolean = true) {
284 };
285 }
286
287 +export function createEnvironmentNameFilter(
288 + env: string,
289 + isEnabled: boolean = true,
290 +) {
291 + const Types = require('react-devtools-shared/src/frontend/types');
292 + return {
293 + type: Types.ComponentFilterEnvironmentName,
294 + isEnabled,
295 + isValid: true,
296 + value: env,
297 + };
298 +}
299 +
300 export function createElementTypeFilter(
301 elementType: ElementType,
302 isEnabled: boolean = true,
packages/react-devtools-shared/src/backend/agent.js
+19
@@ -220,6 +220,7 @@ export default class Agent extends EventEmitter<{
220 this.updateConsolePatchSettings,
221 );
222 bridge.addListener('updateComponentFilters', this.updateComponentFilters);
223 + bridge.addListener('getEnvironmentNames', this.getEnvironmentNames);
224
225 // Temporarily support older standalone front-ends sending commands to newer embedded backends.
226 // We do this because React Native embeds the React DevTools backend,
@@ -814,6 +815,24 @@ export default class Agent extends EventEmitter<{
815 }
816 };
817
818 + getEnvironmentNames: () => void = () => {
819 + let accumulatedNames = null;
820 + for (const rendererID in this._rendererInterfaces) {
821 + const renderer = this._rendererInterfaces[+rendererID];
822 + const names = renderer.getEnvironmentNames();
823 + if (accumulatedNames === null) {
824 + accumulatedNames = names;
825 + } else {
826 + for (let i = 0; i < names.length; i++) {
827 + if (accumulatedNames.indexOf(names[i]) === -1) {
828 + accumulatedNames.push(names[i]);
829 + }
830 + }
831 + }
832 + }
833 + this._bridge.send('environmentNames', accumulatedNames || []);
834 + };
835 +
836 onTraceUpdates: (nodes: Set<HostInstance>) => void = nodes => {
837 this.emit('traceUpdates', nodes);
838 };
packages/react-devtools-shared/src/backend/fiber/renderer.js
+68 -12
@@ -14,6 +14,7 @@ import {
14 ComponentFilterElementType,
15 ComponentFilterHOC,
16 ComponentFilterLocation,
17 + ComponentFilterEnvironmentName,
18 ElementTypeClass,
19 ElementTypeContext,
20 ElementTypeFunction,
@@ -721,6 +722,11 @@ export function getInternalReactConstants(version: string): {
722 };
723 }
724
725 +// All environment names we've seen so far. This lets us create a list of filters to apply.
726 +// This should ideally include env of filtered Components too so that you can add those as
727 +// filters at the same time as removing some other filter.
728 +const knownEnvironmentNames: Set<string> = new Set();
729 +
730 // Map of one or more Fibers in a pair to their unique id number.
731 // We track both Fibers to support Fast Refresh,
732 // which may forcefully replace one of the pair as part of hot reloading.
@@ -1099,6 +1105,7 @@ export function attach(
1105 const hideElementsWithDisplayNames: Set<RegExp> = new Set();
1106 const hideElementsWithPaths: Set<RegExp> = new Set();
1107 const hideElementsWithTypes: Set<ElementType> = new Set();
1108 + const hideElementsWithEnvs: Set<string> = new Set();
1109
1110 // Highlight updates
1111 let traceUpdatesEnabled: boolean = false;
@@ -1108,6 +1115,7 @@ export function attach(
1115 hideElementsWithTypes.clear();
1116 hideElementsWithDisplayNames.clear();
1117 hideElementsWithPaths.clear();
1118 + hideElementsWithEnvs.clear();
1119
1120 componentFilters.forEach(componentFilter => {
1121 if (!componentFilter.isEnabled) {
@@ -1133,6 +1141,9 @@ export function attach(
1141 case ComponentFilterHOC:
1142 hideElementsWithDisplayNames.add(new RegExp('\\('));
1143 break;
1144 + case ComponentFilterEnvironmentName:
1145 + hideElementsWithEnvs.add(componentFilter.value);
1146 + break;
1147 default:
1148 console.warn(
1149 `Invalid component filter type "${componentFilter.type}"`,
@@ -1215,7 +1226,14 @@ export function attach(
1226 flushPendingEvents();
1227 }
1228
1218 - function shouldFilterVirtual(data: ReactComponentInfo): boolean {
1229 + function getEnvironmentNames(): Array<string> {
1230 + return Array.from(knownEnvironmentNames);
1231 + }
1232 +
1233 + function shouldFilterVirtual(
1234 + data: ReactComponentInfo,
1235 + secondaryEnv: null | string,
1236 + ): boolean {
1237 // For purposes of filtering Server Components are always Function Components.
1238 // Environment will be used to filter Server vs Client.
1239 // Technically they can be forwardRef and memo too but those filters will go away
@@ -1236,6 +1254,14 @@ export function attach(
1254 }
1255 }
1256
1257 + if (
1258 + (data.env == null || hideElementsWithEnvs.has(data.env)) &&
1259 + (secondaryEnv === null || hideElementsWithEnvs.has(secondaryEnv))
1260 + ) {
1261 + // If a Component has two environments, you have to filter both for it not to appear.
1262 + return true;
1263 + }
1264 +
1265 return false;
1266 }
1267
@@ -1294,6 +1320,26 @@ export function attach(
1320 }
1321 }
1322
1323 + if (hideElementsWithEnvs.has('Client')) {
1324 + // If we're filtering out the Client environment we should filter out all
1325 + // "Client Components". Technically that also includes the built-ins but
1326 + // since that doesn't actually include any additional code loading it's
1327 + // useful to not filter out the built-ins. Those can be filtered separately.
1328 + // There's no other way to filter out just Function components on the Client.
1329 + // Therefore, this only filters Class and Function components.
1330 + switch (tag) {
1331 + case ClassComponent:
1332 + case IncompleteClassComponent:
1333 + case IncompleteFunctionComponent:
1334 + case FunctionComponent:
1335 + case IndeterminateComponent:
1336 + case ForwardRef:
1337 + case MemoComponent:
1338 + case SimpleMemoComponent:
1339 + return true;
1340 + }
1341 + }
1342 +
1343 /* DISABLED: https://github.com/facebook/react/pull/28417
1344 if (hideElementsWithPaths.size > 0) {
1345 const source = getSourceForFiber(fiber);
@@ -2489,7 +2535,14 @@ export function attach(
2535 }
2536 // Scan up until the next Component to see if this component changed environment.
2537 const componentInfo: ReactComponentInfo = (debugEntry: any);
2492 - if (shouldFilterVirtual(componentInfo)) {
2538 + const secondaryEnv = getSecondaryEnvironmentName(fiber._debugInfo, i);
2539 + if (componentInfo.env != null) {
2540 + knownEnvironmentNames.add(componentInfo.env);
2541 + }
2542 + if (secondaryEnv !== null) {
2543 + knownEnvironmentNames.add(secondaryEnv);
2544 + }
2545 + if (shouldFilterVirtual(componentInfo, secondaryEnv)) {
2546 // Skip.
2547 continue;
2548 }
@@ -2511,10 +2564,6 @@ export function attach(
2564 );
2565 }
2566 previousVirtualInstance = createVirtualInstance(componentInfo);
2514 - const secondaryEnv = getSecondaryEnvironmentName(
2515 - fiber._debugInfo,
2516 - i,
2517 - );
2567 recordVirtualMount(
2568 previousVirtualInstance,
2569 reconcilingParent,
@@ -2919,7 +2968,17 @@ export function attach(
2968 continue;
2969 }
2970 const componentInfo: ReactComponentInfo = (debugEntry: any);
2922 - if (shouldFilterVirtual(componentInfo)) {
2971 + const secondaryEnv = getSecondaryEnvironmentName(
2972 + nextChild._debugInfo,
2973 + i,
2974 + );
2975 + if (componentInfo.env != null) {
2976 + knownEnvironmentNames.add(componentInfo.env);
2977 + }
2978 + if (secondaryEnv !== null) {
2979 + knownEnvironmentNames.add(secondaryEnv);
2980 + }
2981 + if (shouldFilterVirtual(componentInfo, secondaryEnv)) {
2982 continue;
2983 }
2984 if (level === virtualLevel) {
@@ -2983,10 +3042,6 @@ export function attach(
3042 } else {
3043 // Otherwise we create a new instance.
3044 const newVirtualInstance = createVirtualInstance(componentInfo);
2986 - const secondaryEnv = getSecondaryEnvironmentName(
2987 - nextChild._debugInfo,
2988 - i,
2989 - );
3045 recordVirtualMount(
3046 newVirtualInstance,
3047 reconcilingParent,
@@ -3925,7 +3980,7 @@ export function attach(
3980 owner = ownerFiber._debugOwner;
3981 } else {
3982 const ownerInfo: ReactComponentInfo = (owner: any); // Refined
3928 - if (!shouldFilterVirtual(ownerInfo)) {
3983 + if (!shouldFilterVirtual(ownerInfo, null)) {
3984 return ownerInfo;
3985 }
3986 owner = ownerInfo.owner;
@@ -5750,5 +5805,6 @@ export function attach(
5805 storeAsGlobal,
5806 unpatchConsoleForStrictMode,
5807 updateComponentFilters,
5808 + getEnvironmentNames,
5809 };
5810 }
packages/react-devtools-shared/src/backend/legacy/renderer.js
+6
@@ -1078,6 +1078,11 @@ export function attach(
1078 // Not implemented.
1079 }
1080
1081 + function getEnvironmentNames(): Array<string> {
1082 + // No RSC support.
1083 + return [];
1084 + }
1085 +
1086 function setTraceUpdatesEnabled(enabled: boolean) {
1087 // Not implemented.
1088 }
@@ -1152,5 +1157,6 @@ export function attach(
1157 storeAsGlobal,
1158 unpatchConsoleForStrictMode,
1159 updateComponentFilters,
1160 + getEnvironmentNames,
1161 };
1162 }
packages/react-devtools-shared/src/backend/types.js
+1
@@ -416,6 +416,7 @@ export type RendererInterface = {
416 ) => void,
417 unpatchConsoleForStrictMode: () => void,
418 updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void,
419 + getEnvironmentNames: () => Array<string>,
420
421 // Timeline profiler interface
422
packages/react-devtools-shared/src/bridge.js
+2
@@ -189,6 +189,7 @@ export type BackendEvents = {
189 operations: [Array<number>],
190 ownersList: [OwnersList],
191 overrideComponentFilters: [Array<ComponentFilter>],
192 + environmentNames: [Array<string>],
193 profilingData: [ProfilingDataBackend],
194 profilingStatus: [boolean],
195 reloadAppForProfiling: [],
@@ -237,6 +238,7 @@ type FrontendEvents = {
238 stopProfiling: [],
239 storeAsGlobal: [StoreAsGlobalParams],
240 updateComponentFilters: [Array<ComponentFilter>],
241 + getEnvironmentNames: [],
242 updateConsolePatchSettings: [ConsolePatchSettings],
243 viewAttributeSource: [ViewAttributeSourceParams],
244 viewElementSource: [ElementAndRendererID],
packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js
+91 -2
@@ -15,6 +15,7 @@ import {
15 useMemo,
16 useRef,
17 useState,
18 + use,
19 } from 'react';
20 import {
21 LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
@@ -31,6 +32,7 @@ import {
32 ComponentFilterElementType,
33 ComponentFilterHOC,
34 ComponentFilterLocation,
35 + ComponentFilterEnvironmentName,
36 ElementTypeClass,
37 ElementTypeContext,
38 ElementTypeFunction,
@@ -52,11 +54,16 @@ import type {
54 ElementType,
55 ElementTypeComponentFilter,
56 RegExpComponentFilter,
57 + EnvironmentNameComponentFilter,
58 } from 'react-devtools-shared/src/frontend/types';
59
60 const vscodeFilepath = 'vscode://file/{path}:{line}';
61
59 -export default function ComponentsSettings(_: {}): React.Node {
62 +export default function ComponentsSettings({
63 + environmentNames,
64 +}: {
65 + environmentNames: Promise<Array<string>>,
66 +}): React.Node {
67 const store = useContext(StoreContext);
68 const {parseHookNames, setParseHookNames} = useContext(SettingsContext);
69
@@ -101,6 +108,30 @@ export default function ComponentsSettings(_: {}): React.Node {
108 Array<ComponentFilter>,
109 >(() => [...store.componentFilters]);
110
111 + const usedEnvironmentNames = use(environmentNames);
112 +
113 + const resolvedEnvironmentNames = useMemo(() => {
114 + const set = new Set(usedEnvironmentNames);
115 + // If there are other filters already specified but are not currently
116 + // on the page, we still allow them as options.
117 + for (let i = 0; i < componentFilters.length; i++) {
118 + const filter = componentFilters[i];
119 + if (filter.type === ComponentFilterEnvironmentName) {
120 + set.add(filter.value);
121 + }
122 + }
123 + // Client is special and is always available as a default.
124 + if (set.size > 0) {
125 + // Only show any options at all if there's any other option already
126 + // used by a filter or if any environments are used by the page.
127 + // Note that "Client" can have been added above which would mean
128 + // that we should show it as an option regardless if it's the only
129 + // option.
130 + set.add('Client');
131 + }
132 + return Array.from(set).sort();
133 + }, [usedEnvironmentNames, componentFilters]);
134 +
135 const addFilter = useCallback(() => {
136 setComponentFilters(prevComponentFilters => {
137 return [
@@ -146,6 +177,13 @@ export default function ComponentsSettings(_: {}): React.Node {
177 isEnabled: componentFilter.isEnabled,
178 isValid: true,
179 };
180 + } else if (type === ComponentFilterEnvironmentName) {
181 + cloned[index] = {
182 + type: ComponentFilterEnvironmentName,
183 + isEnabled: componentFilter.isEnabled,
184 + isValid: true,
185 + value: 'Client',
186 + };
187 }
188 }
189 return cloned;
@@ -210,6 +248,29 @@ export default function ComponentsSettings(_: {}): React.Node {
248 [],
249 );
250
251 + const updateFilterValueEnvironmentName = useCallback(
252 + (componentFilter: ComponentFilter, value: string) => {
253 + if (componentFilter.type !== ComponentFilterEnvironmentName) {
254 + throw Error('Invalid value for environment name filter');
255 + }
256 +
257 + setComponentFilters(prevComponentFilters => {
258 + const cloned: Array<ComponentFilter> = [...prevComponentFilters];
259 + if (componentFilter.type === ComponentFilterEnvironmentName) {
260 + const index = prevComponentFilters.indexOf(componentFilter);
261 + if (index >= 0) {
262 + cloned[index] = {
263 + ...componentFilter,
264 + value,
265 + };
266 + }
267 + }
268 + return cloned;
269 + });
270 + },
271 + [],
272 + );
273 +
274 const removeFilter = useCallback((index: number) => {
275 setComponentFilters(prevComponentFilters => {
276 const cloned: Array<ComponentFilter> = [...prevComponentFilters];
@@ -246,6 +307,11 @@ export default function ComponentsSettings(_: {}): React.Node {
307 ...((cloned[index]: any): BooleanComponentFilter),
308 isEnabled,
309 };
310 + } else if (componentFilter.type === ComponentFilterEnvironmentName) {
311 + cloned[index] = {
312 + ...((cloned[index]: any): EnvironmentNameComponentFilter),
313 + isEnabled,
314 + };
315 }
316 }
317 return cloned;
@@ -380,10 +446,16 @@ export default function ComponentsSettings(_: {}): React.Node {
446 <option value={ComponentFilterDisplayName}>name</option>
447 <option value={ComponentFilterElementType}>type</option>
448 <option value={ComponentFilterHOC}>hoc</option>
449 + {resolvedEnvironmentNames.length > 0 && (
450 + <option value={ComponentFilterEnvironmentName}>
451 + environment
452 + </option>
453 + )}
454 </select>
455 </td>
456 <td className={styles.TableCell}>
386 - {componentFilter.type === ComponentFilterElementType &&
457 + {(componentFilter.type === ComponentFilterElementType ||
458 + componentFilter.type === ComponentFilterEnvironmentName) &&
459 'equals'}
460 {(componentFilter.type === ComponentFilterLocation ||
461 componentFilter.type === ComponentFilterDisplayName) &&
@@ -428,6 +500,23 @@ export default function ComponentsSettings(_: {}): React.Node {
500 value={componentFilter.value}
501 />
502 )}
503 + {componentFilter.type === ComponentFilterEnvironmentName && (
504 + <select
505 + className={styles.Select}
506 + value={componentFilter.value}
507 + onChange={({currentTarget}) =>
508 + updateFilterValueEnvironmentName(
509 + componentFilter,
510 + currentTarget.value,
511 + )
512 + }>
513 + {resolvedEnvironmentNames.map(name => (
514 + <option key={name} value={name}>
515 + {name}
516 + </option>
517 + ))}
518 + </select>
519 + )}
520 </td>
521 <td className={styles.TableCell}>
522 <Button
packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js
+3 -2
@@ -58,7 +58,8 @@ export default function SettingsModal(_: {}): React.Node {
58 }
59
60 function SettingsModalImpl(_: {}) {
61 - const {setIsModalShowing} = useContext(SettingsModalContext);
61 + const {setIsModalShowing, environmentNames} =
62 + useContext(SettingsModalContext);
63 const dismissModal = useCallback(
64 () => setIsModalShowing(false),
65 [setIsModalShowing],
@@ -81,7 +82,7 @@ function SettingsModalImpl(_: {}) {
82 let view = null;
83 switch (selectedTabID) {
84 case 'components':
84 - view = <ComponentsSettings />;
85 + view = <ComponentsSettings environmentNames={environmentNames} />;
86 break;
87 // $FlowFixMe[incompatible-type] is this missing in TabID?
88 case 'debugging':
packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js
+39 -8
@@ -10,7 +10,16 @@
10 import type {ReactContext} from 'shared/ReactTypes';
11
12 import * as React from 'react';
13 -import {createContext, useMemo, useState} from 'react';
13 +import {
14 + createContext,
15 + useContext,
16 + useCallback,
17 + useState,
18 + startTransition,
19 +} from 'react';
20 +
21 +import {BridgeContext} from '../context';
22 +import type {FrontendBridge} from '../../../bridge';
23
24 export type DisplayDensity = 'comfortable' | 'compact';
25 export type Theme = 'auto' | 'light' | 'dark';
@@ -18,7 +27,7 @@ export type Theme = 'auto' | 'light' | 'dark';
27 type Context = {
28 isModalShowing: boolean,
29 setIsModalShowing: (value: boolean) => void,
21 - ...
30 + environmentNames: null | Promise<Array<string>>,
31 };
32
33 const SettingsModalContext: ReactContext<Context> = createContext<Context>(
@@ -26,20 +35,42 @@ const SettingsModalContext: ReactContext<Context> = createContext<Context>(
35 );
36 SettingsModalContext.displayName = 'SettingsModalContext';
37
38 +function fetchEnvironmentNames(bridge: FrontendBridge): Promise<Array<string>> {
39 + return new Promise(resolve => {
40 + function onEnvironmentNames(names: Array<string>) {
41 + bridge.removeListener('environmentNames', onEnvironmentNames);
42 + resolve(names);
43 + }
44 + bridge.addListener('environmentNames', onEnvironmentNames);
45 + bridge.send('getEnvironmentNames');
46 + });
47 +}
48 +
49 function SettingsModalContextController({
50 children,
51 }: {
52 children: React$Node,
53 }): React.Node {
34 - const [isModalShowing, setIsModalShowing] = useState<boolean>(false);
54 + const bridge = useContext(BridgeContext);
55
36 - const value = useMemo(
37 - () => ({isModalShowing, setIsModalShowing}),
38 - [isModalShowing, setIsModalShowing],
39 - );
56 + const setIsModalShowing: boolean => void = useCallback((value: boolean) => {
57 + startTransition(() => {
58 + setContext({
59 + isModalShowing: value,
60 + setIsModalShowing,
61 + environmentNames: value ? fetchEnvironmentNames(bridge) : null,
62 + });
63 + });
64 + });
65 +
66 + const [currentContext, setContext] = useState<Context>({
67 + isModalShowing: false,
68 + setIsModalShowing,
69 + environmentNames: null,
70 + });
71
72 return (
42 - <SettingsModalContext.Provider value={value}>
73 + <SettingsModalContext.Provider value={currentContext}>
74 {children}
75 </SettingsModalContext.Provider>
76 );
packages/react-devtools-shared/src/frontend/types.js
+11 -2
@@ -76,8 +76,9 @@ export const ComponentFilterElementType = 1;
76 export const ComponentFilterDisplayName = 2;
77 export const ComponentFilterLocation = 3;
78 export const ComponentFilterHOC = 4;
79 +export const ComponentFilterEnvironmentName = 5;
80
80 -export type ComponentFilterType = 1 | 2 | 3 | 4;
81 +export type ComponentFilterType = 1 | 2 | 3 | 4 | 5;
82
83 // Hide all elements of types in this Set.
84 // We hide host components only by default.
@@ -102,10 +103,18 @@ export type BooleanComponentFilter = {
103 type: 4,
104 };
105
106 +export type EnvironmentNameComponentFilter = {
107 + isEnabled: boolean,
108 + isValid: boolean,
109 + type: 5,
110 + value: string,
111 +};
112 +
113 export type ComponentFilter =
114 | BooleanComponentFilter
115 | ElementTypeComponentFilter
108 - | RegExpComponentFilter;
116 + | RegExpComponentFilter
117 + | EnvironmentNameComponentFilter;
118
119 export type HookName = string | null;
120 // Map of hook source ("<filename>:<line-number>:<column-number>") to name.