mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[DevTools] Group consecutive suspended by rows by the same name (#34830)
Stacked on #34829. This lets you get an overview more easily when there's lots of things like scripts downloading. Pluralized the name. E.g. `script` -> `scripts` or `fetch` -> `fetches`. This only groups them consecutively when they'd have the same place in the list anyway because otherwise it might cover up some kind of waterfall effects. <img width="404" height="225" alt="Screenshot 2025-10-13 at 12 06 51 AM" src="https://github.com/user-attachments/assets/da204a8e-d5f7-4eb0-8c51-4cc5bfd184c4" /> Expanded: <img width="407" height="360" alt="Screenshot 2025-10-13 at 12 07 00 AM" src="https://github.com/user-attachments/assets/de3c3de9-f314-4c87-b606-31bc49eb4aba" />
This commit is contained in:
parent
026abeaa5f
commit
83ea655a0b
|
|
@ -13,7 +13,7 @@ import {useState, useTransition} from 'react';
|
|||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import KeyValue from './KeyValue';
|
||||
import {serializeDataForCopy} from '../utils';
|
||||
import {serializeDataForCopy, pluralize} from '../utils';
|
||||
import Store from '../../store';
|
||||
import styles from './InspectedElementSharedStyles.css';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
|
|
@ -44,6 +44,7 @@ type RowProps = {
|
|||
index: number,
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
skipName?: boolean,
|
||||
};
|
||||
|
||||
function getShortDescription(name: string, description: string): string {
|
||||
|
|
@ -99,6 +100,7 @@ function SuspendedByRow({
|
|||
index,
|
||||
minTime,
|
||||
maxTime,
|
||||
skipName,
|
||||
}: RowProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [openIsPending, startOpenTransition] = useTransition();
|
||||
|
|
@ -166,8 +168,10 @@ function SuspendedByRow({
|
|||
className={styles.CollapsableHeaderIcon}
|
||||
type={isOpen ? 'expanded' : 'collapsed'}
|
||||
/>
|
||||
<span className={styles.CollapsableHeaderTitle}>{name}</span>
|
||||
{shortDescription === '' ? null : (
|
||||
<span className={styles.CollapsableHeaderTitle}>
|
||||
{skipName ? shortDescription : name}
|
||||
</span>
|
||||
{skipName || shortDescription === '' ? null : (
|
||||
<>
|
||||
<span className={styles.CollapsableHeaderSeparator}>{' ('}</span>
|
||||
<span className={styles.CollapsableHeaderTitle}>
|
||||
|
|
@ -331,6 +335,110 @@ function compareTime(
|
|||
return ioA.start - ioB.start;
|
||||
}
|
||||
|
||||
type GroupProps = {
|
||||
bridge: FrontendBridge,
|
||||
element: Element,
|
||||
inspectedElement: InspectedElement,
|
||||
store: Store,
|
||||
name: string,
|
||||
suspendedBy: Array<{
|
||||
index: number,
|
||||
value: SerializedAsyncInfo,
|
||||
}>,
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
};
|
||||
|
||||
function SuspendedByGroup({
|
||||
bridge,
|
||||
element,
|
||||
inspectedElement,
|
||||
store,
|
||||
name,
|
||||
suspendedBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
}: GroupProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
let start = Infinity;
|
||||
let end = -Infinity;
|
||||
let isRejected = false;
|
||||
for (let i = 0; i < suspendedBy.length; i++) {
|
||||
const asyncInfo: SerializedAsyncInfo = suspendedBy[i].value;
|
||||
const ioInfo = asyncInfo.awaited;
|
||||
if (ioInfo.start < start) {
|
||||
start = ioInfo.start;
|
||||
}
|
||||
if (ioInfo.end > end) {
|
||||
end = ioInfo.end;
|
||||
}
|
||||
const value: any = ioInfo.value;
|
||||
if (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
value[meta.name] === 'rejected Thenable'
|
||||
) {
|
||||
isRejected = true;
|
||||
}
|
||||
}
|
||||
const timeScale = 100 / (maxTime - minTime);
|
||||
let left = (start - minTime) * timeScale;
|
||||
let width = (end - start) * timeScale;
|
||||
if (width < 5) {
|
||||
// Use at least a 5% width to avoid showing too small indicators.
|
||||
width = 5;
|
||||
if (left > 95) {
|
||||
left = 95;
|
||||
}
|
||||
}
|
||||
const pluralizedName = pluralize(name);
|
||||
return (
|
||||
<div className={styles.CollapsableRow}>
|
||||
<Button
|
||||
className={styles.CollapsableHeader}
|
||||
onClick={() => {
|
||||
setIsOpen(prevIsOpen => !prevIsOpen);
|
||||
}}
|
||||
title={pluralizedName}>
|
||||
<ButtonIcon
|
||||
className={styles.CollapsableHeaderIcon}
|
||||
type={isOpen ? 'expanded' : 'collapsed'}
|
||||
/>
|
||||
<span className={styles.CollapsableHeaderTitle}>{pluralizedName}</span>
|
||||
<div className={styles.CollapsableHeaderFiller} />
|
||||
{isOpen ? null : (
|
||||
<div className={styles.TimeBarContainer}>
|
||||
<div
|
||||
className={
|
||||
!isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
|
||||
}
|
||||
style={{
|
||||
left: left.toFixed(2) + '%',
|
||||
width: width.toFixed(2) + '%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
{isOpen &&
|
||||
suspendedBy.map(({value, index}) => (
|
||||
<SuspendedByRow
|
||||
key={index}
|
||||
index={index}
|
||||
asyncInfo={value}
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
skipName={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InspectedElementSuspendedBy({
|
||||
bridge,
|
||||
element,
|
||||
|
|
@ -390,6 +498,27 @@ export default function InspectedElementSuspendedBy({
|
|||
suspendedBy === null ? [] : suspendedBy.map(withIndex);
|
||||
sortedSuspendedBy.sort(compareTime);
|
||||
|
||||
// Organize into groups of consecutive entries with the same name.
|
||||
const groups = [];
|
||||
let currentGroup = null;
|
||||
let currentGroupName = null;
|
||||
for (let i = 0; i < sortedSuspendedBy.length; i++) {
|
||||
const entry = sortedSuspendedBy[i];
|
||||
const name = entry.value.awaited.name;
|
||||
if (
|
||||
currentGroupName !== name ||
|
||||
!name ||
|
||||
name === 'Promise' ||
|
||||
currentGroup === null
|
||||
) {
|
||||
// Create a new group.
|
||||
currentGroupName = name;
|
||||
currentGroup = [];
|
||||
groups.push(currentGroup);
|
||||
}
|
||||
currentGroup.push(entry);
|
||||
}
|
||||
|
||||
let unknownSuspenders = null;
|
||||
switch (inspectedElement.unknownSuspenders) {
|
||||
case UNKNOWN_SUSPENDERS_REASON_PRODUCTION:
|
||||
|
|
@ -430,19 +559,48 @@ export default function InspectedElementSuspendedBy({
|
|||
<ButtonIcon type="copy" />
|
||||
</Button>
|
||||
</div>
|
||||
{sortedSuspendedBy.map(({value, index}) => (
|
||||
<SuspendedByRow
|
||||
key={index}
|
||||
index={index}
|
||||
asyncInfo={value}
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
))}
|
||||
{groups.length === 1
|
||||
? // If it's only one type of suspender we can flatten it.
|
||||
groups[0].map(entry => (
|
||||
<SuspendedByRow
|
||||
key={entry.index}
|
||||
index={entry.index}
|
||||
asyncInfo={entry.value}
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
))
|
||||
: groups.map((entries, index) =>
|
||||
entries.length === 1 ? (
|
||||
<SuspendedByRow
|
||||
key={entries[0].index}
|
||||
index={entries[0].index}
|
||||
asyncInfo={entries[0].value}
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
) : (
|
||||
<SuspendedByGroup
|
||||
key={entries[0].index}
|
||||
name={entries[0].value.awaited.name}
|
||||
suspendedBy={entries}
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{unknownSuspenders}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -198,3 +198,39 @@ export function truncateText(text: string, maxLength: number): string {
|
|||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
export function pluralize(word: string): string {
|
||||
if (!/^[a-z]+$/i.test(word)) {
|
||||
// If it's not a single a-z word, give up.
|
||||
return word;
|
||||
}
|
||||
|
||||
switch (word) {
|
||||
case 'man':
|
||||
return 'men';
|
||||
case 'woman':
|
||||
return 'women';
|
||||
case 'child':
|
||||
return 'children';
|
||||
case 'foot':
|
||||
return 'feet';
|
||||
case 'tooth':
|
||||
return 'teeth';
|
||||
case 'mouse':
|
||||
return 'mice';
|
||||
case 'person':
|
||||
return 'people';
|
||||
}
|
||||
|
||||
// Words ending in s, x, z, ch, sh → add "es"
|
||||
if (/(s|x|z|ch|sh)$/i.test(word)) return word + 'es';
|
||||
|
||||
// Words ending in consonant + y → replace y with "ies"
|
||||
if (/[bcdfghjklmnpqrstvwxz]y$/i.test(word)) return word.slice(0, -1) + 'ies';
|
||||
|
||||
// Words ending in f or fe → replace with "ves"
|
||||
if (/(?:f|fe)$/i.test(word)) return word.replace(/(?:f|fe)$/i, 'ves');
|
||||
|
||||
// Default: just add "s"
|
||||
return word + 's';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user