@@ -13,7 +13,7 @@ import {useState, useTransition} from 'react';
1313import Button from '../Button' ;
1414import ButtonIcon from '../ButtonIcon' ;
1515import KeyValue from './KeyValue' ;
16- import { serializeDataForCopy } from '../utils' ;
16+ import { serializeDataForCopy , pluralize } from '../utils' ;
1717import Store from '../../store' ;
1818import styles from './InspectedElementSharedStyles.css' ;
1919import { withPermissionsCheck } from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck' ;
@@ -44,6 +44,7 @@ type RowProps = {
4444 index : number ,
4545 minTime : number ,
4646 maxTime : number ,
47+ skipName ?: boolean ,
4748} ;
4849
4950function getShortDescription ( name : string , description : string ) : string {
@@ -99,6 +100,7 @@ function SuspendedByRow({
99100 index,
100101 minTime,
101102 maxTime,
103+ skipName,
102104} : RowProps ) {
103105 const [ isOpen , setIsOpen ] = useState ( false ) ;
104106 const [ openIsPending , startOpenTransition ] = useTransition ( ) ;
@@ -166,8 +168,10 @@ function SuspendedByRow({
166168 className = { styles . CollapsableHeaderIcon }
167169 type = { isOpen ? 'expanded' : 'collapsed' }
168170 />
169- < span className = { styles . CollapsableHeaderTitle } > { name } </ span >
170- { shortDescription === '' ? null : (
171+ < span className = { styles . CollapsableHeaderTitle } >
172+ { skipName ? shortDescription : name }
173+ </ span >
174+ { skipName || shortDescription === '' ? null : (
171175 < >
172176 < span className = { styles . CollapsableHeaderSeparator } > { ' (' } </ span >
173177 < span className = { styles . CollapsableHeaderTitle } >
@@ -331,6 +335,110 @@ function compareTime(
331335 return ioA . start - ioB . start ;
332336}
333337
338+ type GroupProps = {
339+ bridge : FrontendBridge ,
340+ element : Element ,
341+ inspectedElement : InspectedElement ,
342+ store : Store ,
343+ name : string ,
344+ suspendedBy : Array < {
345+ index : number ,
346+ value : SerializedAsyncInfo ,
347+ } > ,
348+ minTime : number ,
349+ maxTime : number ,
350+ } ;
351+
352+ function SuspendedByGroup ( {
353+ bridge,
354+ element,
355+ inspectedElement,
356+ store,
357+ name,
358+ suspendedBy,
359+ minTime,
360+ maxTime,
361+ } : GroupProps ) {
362+ const [ isOpen , setIsOpen ] = useState ( false ) ;
363+ let start = Infinity ;
364+ let end = - Infinity ;
365+ let isRejected = false ;
366+ for ( let i = 0 ; i < suspendedBy . length ; i ++ ) {
367+ const asyncInfo : SerializedAsyncInfo = suspendedBy [ i ] . value ;
368+ const ioInfo = asyncInfo . awaited ;
369+ if ( ioInfo . start < start ) {
370+ start = ioInfo . start ;
371+ }
372+ if ( ioInfo . end > end ) {
373+ end = ioInfo . end ;
374+ }
375+ const value : any = ioInfo . value ;
376+ if (
377+ value !== null &&
378+ typeof value === 'object' &&
379+ value [ meta . name ] === 'rejected Thenable'
380+ ) {
381+ isRejected = true ;
382+ }
383+ }
384+ const timeScale = 100 / ( maxTime - minTime ) ;
385+ let left = ( start - minTime ) * timeScale ;
386+ let width = ( end - start ) * timeScale ;
387+ if ( width < 5 ) {
388+ // Use at least a 5% width to avoid showing too small indicators.
389+ width = 5 ;
390+ if ( left > 95 ) {
391+ left = 95 ;
392+ }
393+ }
394+ const pluralizedName = pluralize ( name ) ;
395+ return (
396+ < div className = { styles . CollapsableRow } >
397+ < Button
398+ className = { styles . CollapsableHeader }
399+ onClick = { ( ) => {
400+ setIsOpen ( prevIsOpen => ! prevIsOpen ) ;
401+ } }
402+ title = { pluralizedName } >
403+ < ButtonIcon
404+ className = { styles . CollapsableHeaderIcon }
405+ type = { isOpen ? 'expanded' : 'collapsed' }
406+ />
407+ < span className = { styles . CollapsableHeaderTitle } > { pluralizedName } </ span >
408+ < div className = { styles . CollapsableHeaderFiller } />
409+ { isOpen ? null : (
410+ < div className = { styles . TimeBarContainer } >
411+ < div
412+ className = {
413+ ! isRejected ? styles . TimeBarSpan : styles . TimeBarSpanErrored
414+ }
415+ style = { {
416+ left : left . toFixed ( 2 ) + '%' ,
417+ width : width . toFixed ( 2 ) + '%' ,
418+ } }
419+ />
420+ </ div >
421+ ) }
422+ </ Button >
423+ { isOpen &&
424+ suspendedBy . map ( ( { value, index} ) => (
425+ < SuspendedByRow
426+ key = { index }
427+ index = { index }
428+ asyncInfo = { value }
429+ bridge = { bridge }
430+ element = { element }
431+ inspectedElement = { inspectedElement }
432+ store = { store }
433+ minTime = { minTime }
434+ maxTime = { maxTime }
435+ skipName = { true }
436+ />
437+ ) ) }
438+ </ div >
439+ ) ;
440+ }
441+
334442export default function InspectedElementSuspendedBy ( {
335443 bridge,
336444 element,
@@ -390,6 +498,27 @@ export default function InspectedElementSuspendedBy({
390498 suspendedBy === null ? [ ] : suspendedBy . map ( withIndex ) ;
391499 sortedSuspendedBy . sort ( compareTime ) ;
392500
501+ // Organize into groups of consecutive entries with the same name.
502+ const groups = [ ] ;
503+ let currentGroup = null ;
504+ let currentGroupName = null ;
505+ for ( let i = 0 ; i < sortedSuspendedBy . length ; i ++ ) {
506+ const entry = sortedSuspendedBy [ i ] ;
507+ const name = entry . value . awaited . name ;
508+ if (
509+ currentGroupName !== name ||
510+ ! name ||
511+ name === 'Promise' ||
512+ currentGroup === null
513+ ) {
514+ // Create a new group.
515+ currentGroupName = name ;
516+ currentGroup = [ ] ;
517+ groups . push ( currentGroup ) ;
518+ }
519+ currentGroup . push ( entry ) ;
520+ }
521+
393522 let unknownSuspenders = null ;
394523 switch ( inspectedElement . unknownSuspenders ) {
395524 case UNKNOWN_SUSPENDERS_REASON_PRODUCTION :
@@ -430,19 +559,48 @@ export default function InspectedElementSuspendedBy({
430559 < ButtonIcon type = "copy" />
431560 </ Button >
432561 </ div >
433- { sortedSuspendedBy . map ( ( { value, index} ) => (
434- < SuspendedByRow
435- key = { index }
436- index = { index }
437- asyncInfo = { value }
438- bridge = { bridge }
439- element = { element }
440- inspectedElement = { inspectedElement }
441- store = { store }
442- minTime = { minTime }
443- maxTime = { maxTime }
444- />
445- ) ) }
562+ { groups . length === 1
563+ ? // If it's only one type of suspender we can flatten it.
564+ groups [ 0 ] . map ( entry => (
565+ < SuspendedByRow
566+ key = { entry . index }
567+ index = { entry . index }
568+ asyncInfo = { entry . value }
569+ bridge = { bridge }
570+ element = { element }
571+ inspectedElement = { inspectedElement }
572+ store = { store }
573+ minTime = { minTime }
574+ maxTime = { maxTime }
575+ />
576+ ) )
577+ : groups . map ( ( entries , index ) =>
578+ entries . length === 1 ? (
579+ < SuspendedByRow
580+ key = { entries [ 0 ] . index }
581+ index = { entries [ 0 ] . index }
582+ asyncInfo = { entries [ 0 ] . value }
583+ bridge = { bridge }
584+ element = { element }
585+ inspectedElement = { inspectedElement }
586+ store = { store }
587+ minTime = { minTime }
588+ maxTime = { maxTime }
589+ />
590+ ) : (
591+ < SuspendedByGroup
592+ key = { entries [ 0 ] . index }
593+ name = { entries [ 0 ] . value . awaited . name }
594+ suspendedBy = { entries }
595+ bridge = { bridge }
596+ element = { element }
597+ inspectedElement = { inspectedElement }
598+ store = { store }
599+ minTime = { minTime }
600+ maxTime = { maxTime }
601+ />
602+ ) ,
603+ ) }
446604 { unknownSuspenders }
447605 </ div >
448606 ) ;
0 commit comments