@samitouri / QOS-React-1 / commits / 52912a1453

[DevTools] Add ignore-listed stack frame disclosure (#36828)

Jiwon Choi committed Jun 25, 2026 at 15:57 UTC 52912a14531dad54d7844c83bc0e35f2c6a74c11
5 files changed +415 -133
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
+5
@@ -143,3 +143,8 @@
143 padding-left: 1.25rem;
144 margin-top: 0.25rem;
145 }
146 +
147 +.SuspendedBySkeleton {
148 + padding: 0.25rem;
149 + padding-left: 1.25rem;
150 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
+198 -97
@@ -9,7 +9,7 @@
9
10 import {copy} from 'clipboard-js';
11 import * as React from 'react';
12 -import {useState, useTransition} from 'react';
12 +import {use, useContext, useState, useTransition} from 'react';
13 import Button from '../Button';
14 import ButtonIcon from '../ButtonIcon';
15 import KeyValue from './KeyValue';
@@ -17,10 +17,13 @@ import {serializeDataForCopy, pluralize} from '../utils';
17 import Store from '../../store';
18 import styles from './InspectedElementSharedStyles.css';
19 import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
20 -import StackTraceView from './StackTraceView';
20 +import FetchFileWithCachingContext from './FetchFileWithCachingContext';
21 +import StackTraceView, {IgnoreListToggleButton} from './StackTraceView';
22 import OwnerView from './OwnerView';
23 import {meta} from '../../../hydration';
24 +import Skeleton from './Skeleton';
25 import useInferredName from '../useInferredName';
26 +import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
27
28 import {getClassNameForEnvironment} from '../SuspenseTab/SuspenseEnvironmentColors.js';
29
@@ -29,6 +32,8 @@ import type {
32 SerializedAsyncInfo,
33 } from 'react-devtools-shared/src/frontend/types';
34 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
35 +import type {ReactStackTrace} from 'shared/ReactTypes';
36 +import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
37
38 import {
39 UNKNOWN_SUSPENDERS_NONE,
@@ -94,6 +99,78 @@ function formatBytes(bytes: number) {
99 return (bytes / 1_000_000_000).toFixed(1) + ' gB';
100 }
101
102 +type StackTraceGroupProps = {
103 + children: (showIgnoreList: boolean) => React.Node,
104 + ioStack: null | ReactStackTrace,
105 + asyncInfoStack: null | ReactStackTrace,
106 +};
107 +
108 +function StackTraceGroup({
109 + children,
110 + ioStack,
111 + asyncInfoStack,
112 +}: StackTraceGroupProps): React.Node {
113 + const [showIgnoreList, setShowIgnoreList] = useState(false);
114 + const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
115 +
116 + const ioStackHasIgnoredFrames =
117 + ioStack !== null &&
118 + ioStack.some(callSite => {
119 + const [, virtualURL, virtualLine, virtualColumn] = callSite;
120 +
121 + // symbolicated output is cached
122 + const symbolicatedCallSite: null | SourceMappedLocation =
123 + fetchFileWithCaching !== null
124 + ? use(
125 + symbolicateSourceWithCache(
126 + fetchFileWithCaching,
127 + virtualURL,
128 + virtualLine,
129 + virtualColumn,
130 + ),
131 + )
132 + : null;
133 +
134 + return symbolicatedCallSite !== null && symbolicatedCallSite.ignored;
135 + });
136 +
137 + const asyncInfoStackHasIgnoredFrames =
138 + asyncInfoStack !== null &&
139 + asyncInfoStack.some(callSite => {
140 + const [, virtualURL, virtualLine, virtualColumn] = callSite;
141 +
142 + // symbolicated output is cached
143 + const symbolicatedCallSite: null | SourceMappedLocation =
144 + fetchFileWithCaching !== null
145 + ? use(
146 + symbolicateSourceWithCache(
147 + fetchFileWithCaching,
148 + virtualURL,
149 + virtualLine,
150 + virtualColumn,
151 + ),
152 + )
153 + : null;
154 +
155 + return symbolicatedCallSite !== null && symbolicatedCallSite.ignored;
156 + });
157 +
158 + const hasIgnoredFrames =
159 + ioStackHasIgnoredFrames || asyncInfoStackHasIgnoredFrames;
160 +
161 + return (
162 + <>
163 + {children(showIgnoreList)}
164 + {hasIgnoredFrames && (
165 + <IgnoreListToggleButton
166 + onClick={() => setShowIgnoreList(prev => !prev)}
167 + showIgnoreList={showIgnoreList}
168 + />
169 + )}
170 + </>
171 + );
172 +}
173 +
174 function SuspendedByRow({
175 bridge,
176 element,
@@ -203,102 +280,126 @@ function SuspendedByRow({
280 </Button>
281 {isOpen && (
282 <div className={styles.CollapsableContent}>
206 - {showIOStack && (
207 - <StackTraceView
208 - stack={ioInfo.stack}
209 - environmentName={
210 - ioOwner !== null && ioOwner.env === ioInfo.env
211 - ? null
212 - : ioInfo.env
213 - }
214 - />
215 - )}
216 - {ioOwner !== null &&
217 - ioOwner.id !== inspectedElement.id &&
218 - (showIOStack ||
219 - !showAwaitStack ||
220 - asyncOwner === null ||
221 - ioOwner.id !== asyncOwner.id) ? (
222 - <OwnerView
223 - key={ioOwner.id}
224 - displayName={ioOwner.displayName || 'Anonymous'}
225 - environmentName={
226 - ioOwner.env === inspectedElement.env &&
227 - ioOwner.env === ioInfo.env
228 - ? null
229 - : ioOwner.env
230 - }
231 - hocDisplayNames={ioOwner.hocDisplayNames}
232 - compiledWithForget={ioOwner.compiledWithForget}
233 - id={ioOwner.id}
234 - isInStore={store.containsElement(ioOwner.id)}
235 - type={ioOwner.type}
236 - />
237 - ) : null}
238 - {showAwaitStack ? (
239 - <>
240 - <div className={styles.SmallHeader}>awaited at:</div>
241 - {asyncInfo.stack !== null && asyncInfo.stack.length > 0 && (
242 - <StackTraceView
243 - stack={asyncInfo.stack}
244 - environmentName={
245 - asyncOwner !== null && asyncOwner.env === asyncInfo.env
246 - ? null
247 - : asyncInfo.env
248 - }
249 - />
283 + <React.Suspense
284 + fallback={
285 + <div className={styles.SuspendedBySkeleton}>
286 + <Skeleton height={16} width="40%" />
287 + </div>
288 + }>
289 + <StackTraceGroup
290 + ioStack={showIOStack ? ioInfo.stack : null}
291 + asyncInfoStack={showAwaitStack ? asyncInfo.stack : null}>
292 + {(showIgnoreList: boolean) => (
293 + <>
294 + {showIOStack && (
295 + <StackTraceView
296 + stack={ioInfo.stack}
297 + environmentName={
298 + ioOwner !== null && ioOwner.env === ioInfo.env
299 + ? null
300 + : ioInfo.env
301 + }
302 + showIgnoreList={showIgnoreList}
303 + />
304 + )}
305 + {ioOwner !== null &&
306 + ioOwner.id !== inspectedElement.id &&
307 + (showIOStack ||
308 + !showAwaitStack ||
309 + asyncOwner === null ||
310 + ioOwner.id !== asyncOwner.id) ? (
311 + <OwnerView
312 + key={ioOwner.id}
313 + displayName={ioOwner.displayName || 'Anonymous'}
314 + environmentName={
315 + ioOwner.env === inspectedElement.env &&
316 + ioOwner.env === ioInfo.env
317 + ? null
318 + : ioOwner.env
319 + }
320 + hocDisplayNames={ioOwner.hocDisplayNames}
321 + compiledWithForget={ioOwner.compiledWithForget}
322 + id={ioOwner.id}
323 + isInStore={store.containsElement(ioOwner.id)}
324 + type={ioOwner.type}
325 + />
326 + ) : null}
327 + {showAwaitStack ? (
328 + <>
329 + <div className={styles.SmallHeader}>awaited at:</div>
330 + {asyncInfo.stack !== null &&
331 + asyncInfo.stack.length > 0 && (
332 + <StackTraceView
333 + stack={asyncInfo.stack}
334 + environmentName={
335 + asyncOwner !== null &&
336 + asyncOwner.env === asyncInfo.env
337 + ? null
338 + : asyncInfo.env
339 + }
340 + showIgnoreList={showIgnoreList}
341 + />
342 + )}
343 + {asyncOwner !== null &&
344 + asyncOwner.id !== inspectedElement.id ? (
345 + <OwnerView
346 + key={asyncOwner.id}
347 + displayName={asyncOwner.displayName || 'Anonymous'}
348 + environmentName={
349 + asyncOwner.env === inspectedElement.env &&
350 + asyncOwner.env === asyncInfo.env
351 + ? null
352 + : asyncOwner.env
353 + }
354 + hocDisplayNames={asyncOwner.hocDisplayNames}
355 + compiledWithForget={asyncOwner.compiledWithForget}
356 + id={asyncOwner.id}
357 + isInStore={store.containsElement(asyncOwner.id)}
358 + type={asyncOwner.type}
359 + />
360 + ) : null}
361 + </>
362 + ) : null}
363 + <div className={styles.PreviewContainer}>
364 + <KeyValue
365 + alphaSort={true}
366 + bridge={bridge}
367 + canDeletePaths={false}
368 + canEditValues={false}
369 + canRenamePaths={false}
370 + depth={1}
371 + element={element}
372 + hidden={false}
373 + inspectedElement={inspectedElement}
374 + name={
375 + isFulfilled
376 + ? 'awaited value'
377 + : isRejected
378 + ? 'rejected with'
379 + : 'pending value'
380 + }
381 + path={
382 + isFulfilled
383 + ? [index, 'awaited', 'value', 'value']
384 + : isRejected
385 + ? [index, 'awaited', 'value', 'reason']
386 + : [index, 'awaited', 'value']
387 + }
388 + pathRoot="suspendedBy"
389 + store={store}
390 + value={
391 + isFulfilled
392 + ? value.value
393 + : isRejected
394 + ? value.reason
395 + : value
396 + }
397 + />
398 + </div>
399 + </>
400 )}
251 - {asyncOwner !== null && asyncOwner.id !== inspectedElement.id ? (
252 - <OwnerView
253 - key={asyncOwner.id}
254 - displayName={asyncOwner.displayName || 'Anonymous'}
255 - environmentName={
256 - asyncOwner.env === inspectedElement.env &&
257 - asyncOwner.env === asyncInfo.env
258 - ? null
259 - : asyncOwner.env
260 - }
261 - hocDisplayNames={asyncOwner.hocDisplayNames}
262 - compiledWithForget={asyncOwner.compiledWithForget}
263 - id={asyncOwner.id}
264 - isInStore={store.containsElement(asyncOwner.id)}
265 - type={asyncOwner.type}
266 - />
267 - ) : null}
268 - </>
269 - ) : null}
270 - <div className={styles.PreviewContainer}>
271 - <KeyValue
272 - alphaSort={true}
273 - bridge={bridge}
274 - canDeletePaths={false}
275 - canEditValues={false}
276 - canRenamePaths={false}
277 - depth={1}
278 - element={element}
279 - hidden={false}
280 - inspectedElement={inspectedElement}
281 - name={
282 - isFulfilled
283 - ? 'awaited value'
284 - : isRejected
285 - ? 'rejected with'
286 - : 'pending value'
287 - }
288 - path={
289 - isFulfilled
290 - ? [index, 'awaited', 'value', 'value']
291 - : isRejected
292 - ? [index, 'awaited', 'value', 'reason']
293 - : [index, 'awaited', 'value']
294 - }
295 - pathRoot="suspendedBy"
296 - store={store}
297 - value={
298 - isFulfilled ? value.value : isRejected ? value.reason : value
299 - }
300 - />
301 - </div>
401 + </StackTraceGroup>
402 + </React.Suspense>
403 </div>
404 )}
405 </div>
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+125 -28
@@ -8,7 +8,7 @@
8 */
9
10 import * as React from 'react';
11 -import {Fragment, useContext} from 'react';
11 +import {Fragment, useContext, useState, use} from 'react';
12 import {BridgeContext, StoreContext} from '../context';
13 import InspectedElementBadges from './InspectedElementBadges';
14 import InspectedElementContextTree from './InspectedElementContextTree';
@@ -19,15 +19,17 @@ import InspectedElementStateTree from './InspectedElementStateTree';
19 import InspectedElementStyleXPlugin from './InspectedElementStyleXPlugin';
20 import InspectedElementSuspendedBy from './InspectedElementSuspendedBy';
21 import NativeStyleEditor from './NativeStyleEditor';
22 +import FetchFileWithCachingContext from './FetchFileWithCachingContext';
23 import {enableStyleXFeatures} from 'react-devtools-feature-flags';
24 import InspectedElementSourcePanel from './InspectedElementSourcePanel';
24 -import StackTraceView from './StackTraceView';
25 +import StackTraceView, {IgnoreListToggleButton} from './StackTraceView';
26 import OwnerView from './OwnerView';
27 import Skeleton from './Skeleton';
28 import {
29 ElementTypeSuspense,
30 ElementTypeActivity,
31 } from 'react-devtools-shared/src/frontend/types';
32 +import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
33
34 import styles from './InspectedElementView.css';
35
@@ -39,6 +41,83 @@ import type {HookNames} from 'react-devtools-shared/src/frontend/types';
41 import type {ToggleParseHookNames} from './InspectedElementContext';
42 import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
43
44 +type StackTraceGroupProps = {
45 + children: (showIgnoreList: boolean) => React.Node,
46 + componentStack: InspectedElement['stack'],
47 + owners: InspectedElement['owners'],
48 +};
49 +
50 +function StackTraceGroup({
51 + children,
52 + componentStack,
53 + owners,
54 +}: StackTraceGroupProps): React.Node {
55 + const [showIgnoreList, setShowIgnoreList] = useState(false);
56 + const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
57 +
58 + const componentStackHasIgnoredFrames =
59 + componentStack !== null &&
60 + componentStack.some(callSite => {
61 + const [, virtualURL, virtualLine, virtualColumn] = callSite;
62 +
63 + // symbolicated output is cached
64 + const symbolicatedCallSite: null | SourceMappedLocation =
65 + fetchFileWithCaching !== null
66 + ? use(
67 + symbolicateSourceWithCache(
68 + fetchFileWithCaching,
69 + virtualURL,
70 + virtualLine,
71 + virtualColumn,
72 + ),
73 + )
74 + : null;
75 +
76 + return symbolicatedCallSite !== null && symbolicatedCallSite.ignored;
77 + });
78 +
79 + const ownerStacksHaveIgnoredFrames =
80 + owners !== null &&
81 + owners.some(owner => {
82 + return (
83 + owner.stack !== null &&
84 + owner.stack.some(callSite => {
85 + const [, virtualURL, virtualLine, virtualColumn] = callSite;
86 +
87 + // symbolicated output is cached
88 + const symbolicatedCallSite: null | SourceMappedLocation =
89 + fetchFileWithCaching !== null
90 + ? use(
91 + symbolicateSourceWithCache(
92 + fetchFileWithCaching,
93 + virtualURL,
94 + virtualLine,
95 + virtualColumn,
96 + ),
97 + )
98 + : null;
99 +
100 + return symbolicatedCallSite !== null && symbolicatedCallSite.ignored;
101 + })
102 + );
103 + });
104 +
105 + const hasIgnoredFrames =
106 + componentStackHasIgnoredFrames || ownerStacksHaveIgnoredFrames;
107 +
108 + return (
109 + <>
110 + {children(showIgnoreList)}
111 + {hasIgnoredFrames && (
112 + <IgnoreListToggleButton
113 + onClick={() => setShowIgnoreList(prev => !prev)}
114 + showIgnoreList={showIgnoreList}
115 + />
116 + )}
117 + </>
118 + );
119 +}
120 +
121 type Props = {
122 element: Element,
123 hookNames: HookNames | null,
@@ -189,33 +268,51 @@ export default function InspectedElementView({
268 <Skeleton height={16} width="40%" />
269 </div>
270 }>
192 - {showStack ? <StackTraceView stack={stack} /> : null}
193 - {showOwnersList &&
194 - owners?.map(owner => (
195 - <Fragment key={owner.id}>
196 - <OwnerView
197 - displayName={owner.displayName || 'Anonymous'}
198 - hocDisplayNames={owner.hocDisplayNames}
199 - environmentName={
200 - inspectedElement.env === owner.env ? null : owner.env
201 - }
202 - compiledWithForget={owner.compiledWithForget}
203 - id={owner.id}
204 - isInStore={store.containsElement(owner.id)}
205 - type={owner.type}
206 - />
207 - {owner.stack != null && owner.stack.length > 0 ? (
208 - <StackTraceView stack={owner.stack} />
271 + <StackTraceGroup componentStack={stack} owners={owners}>
272 + {(showIgnoreList: boolean) => (
273 + <>
274 + {showStack ? (
275 + <StackTraceView
276 + stack={stack}
277 + showIgnoreList={showIgnoreList}
278 + />
279 ) : null}
210 - </Fragment>
211 - ))}
212 -
213 - {rootType !== null && (
214 - <div className={styles.OwnersMetaField}>{rootType}</div>
215 - )}
216 - {rendererLabel !== null && (
217 - <div className={styles.OwnersMetaField}>{rendererLabel}</div>
218 - )}
280 + {showOwnersList &&
281 + owners?.map(owner => (
282 + <Fragment key={owner.id}>
283 + <OwnerView
284 + displayName={owner.displayName || 'Anonymous'}
285 + hocDisplayNames={owner.hocDisplayNames}
286 + environmentName={
287 + inspectedElement.env === owner.env
288 + ? null
289 + : owner.env
290 + }
291 + compiledWithForget={owner.compiledWithForget}
292 + id={owner.id}
293 + isInStore={store.containsElement(owner.id)}
294 + type={owner.type}
295 + />
296 + {owner.stack != null && owner.stack.length > 0 ? (
297 + <StackTraceView
298 + stack={owner.stack}
299 + showIgnoreList={showIgnoreList}
300 + />
301 + ) : null}
302 + </Fragment>
303 + ))}
304 +
305 + {rootType !== null && (
306 + <div className={styles.OwnersMetaField}>{rootType}</div>
307 + )}
308 + {rendererLabel !== null && (
309 + <div className={styles.OwnersMetaField}>
310 + {rendererLabel}
311 + </div>
312 + )}
313 + </>
314 + )}
315 + </StackTraceGroup>
316 </React.Suspense>
317 </div>
318 )}
packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.css
+19
@@ -8,10 +8,18 @@
8 white-space-collapse: preserve;
9 }
10
11 +.IgnoredCallSite {
12 + opacity: 0.5;
13 +}
14 +
15 .IgnoredCallSite, .BuiltInCallSite {
16 display: none;
17 }
18
19 +.IgnoredCallSite.CallSite {
20 + display: flex;
21 +}
22 +
23 .CallSite + .BuiltInCallSite {
24 display: block;
25 }
@@ -33,3 +41,14 @@
41 margin-left: 0.25rem;
42 }
43
44 +.IgnoreListToggleContainer {
45 + padding-left: 1rem;
46 +}
47 +
48 +.IgnoreListToggleButton {
49 + background: none;
50 + color: var(--color-link);
51 + font: inherit;
52 + font-style: italic;
53 + text-decoration: underline;
54 +}
packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js
+68 -8
@@ -10,6 +10,7 @@
10 import * as React from 'react';
11 import {use, useContext} from 'react';
12
13 +import Button from '../Button';
14 import useOpenResource from '../useOpenResource';
15
16 import ElementBadges from './ElementBadges';
@@ -29,11 +30,13 @@ import formatLocationForDisplay from './formatLocationForDisplay';
30 type CallSiteViewProps = {
31 callSite: ReactCallSite,
32 environmentName: null | string,
33 + showIgnoreList: boolean,
34 };
35
36 export function CallSiteView({
37 callSite,
38 environmentName,
39 + showIgnoreList,
40 }: CallSiteViewProps): React.Node {
41 const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
42
@@ -60,16 +63,14 @@ export function CallSiteView({
63 symbolicatedCallSite !== null ? symbolicatedCallSite.location : callSite;
64 const ignored =
65 symbolicatedCallSite !== null ? symbolicatedCallSite.ignored : false;
63 - // TODO: Make an option to be able to toggle the display of ignore listed rows.
64 - // Ideally this UI should be higher than a single Stack Trace so that there's not
65 - // multiple buttons in a single inspection taking up space.
66
67 const isBuiltIn = url === '' || url.startsWith('<anonymous>'); // This looks like a fake anonymous through eval.
68 return (
69 <div
70 className={
71 ignored
72 - ? styles.IgnoredCallSite
72 + ? styles.IgnoredCallSite +
73 + (showIgnoreList ? ' ' + styles.CallSite : '')
74 : isBuiltIn
75 ? styles.BuiltInCallSite
76 : styles.CallSite
@@ -94,15 +95,75 @@ export function CallSiteView({
95 );
96 }
97
98 +type IgnoreListToggleButtonProps = {
99 + onClick: () => void,
100 + showIgnoreList: boolean,
101 +};
102 +
103 +export function IgnoreListToggleButton({
104 + onClick,
105 + showIgnoreList,
106 +}: IgnoreListToggleButtonProps): React.Node {
107 + const label = showIgnoreList
108 + ? 'Hide ignore-listed frames'
109 + : 'Show ignore-listed frames';
110 +
111 + return (
112 + <div className={styles.IgnoreListToggleContainer}>
113 + <Button
114 + className={styles.IgnoreListToggleButton}
115 + onClick={onClick}
116 + title={label}>
117 + {label}
118 + </Button>
119 + </div>
120 + );
121 +}
122 +
123 type Props = {
124 stack: ReactStackTrace,
125 environmentName: null | string,
126 + showIgnoreList: boolean,
127 };
128
129 export default function StackTraceView({
130 stack,
131 environmentName,
132 + showIgnoreList,
133 }: Props): React.Node {
134 + const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
135 +
136 + let lastMeaningfulFrameIndex = -1;
137 + // Reverse loop to find the last non-ignored, non-built-in index
138 + for (let index = stack.length - 1; index >= 0; index--) {
139 + const callSite = stack[index];
140 + const [, virtualURL, virtualLine, virtualColumn] = callSite;
141 +
142 + // symbolicated output is cached
143 + const symbolicatedCallSite: null | SourceMappedLocation =
144 + fetchFileWithCaching !== null
145 + ? use(
146 + symbolicateSourceWithCache(
147 + fetchFileWithCaching,
148 + virtualURL,
149 + virtualLine,
150 + virtualColumn,
151 + ),
152 + )
153 + : null;
154 + const [, url] =
155 + symbolicatedCallSite !== null ? symbolicatedCallSite.location : callSite;
156 + const ignored =
157 + symbolicatedCallSite !== null ? symbolicatedCallSite.ignored : false;
158 + // This looks like a fake anonymous through eval.
159 + const isBuiltIn = url === '' || url.startsWith('<anonymous>');
160 +
161 + if (!ignored && !isBuiltIn) {
162 + lastMeaningfulFrameIndex = index;
163 + break;
164 + }
165 + }
166 +
167 return (
168 <div className={styles.StackTraceView}>
169 {stack.map((callSite, index) => (
@@ -110,11 +171,10 @@ export default function StackTraceView({
171 key={index}
172 callSite={callSite}
173 environmentName={
113 - // Badge last row
114 - // TODO: If we start ignore listing the last row, we should badge the last
115 - // non-ignored row.
116 - index === stack.length - 1 ? environmentName : null
174 + // Badge last meaningful frame (non-ignored, non-built-in)
175 + index === lastMeaningfulFrameIndex ? environmentName : null
176 }
177 + showIgnoreList={showIgnoreList}
178 />
179 ))}
180 </div>