Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,6 +44,7 @@ type RowProps = {
index: number,
minTime: number,
maxTime: number,
skipName?: boolean,
};

function getShortDescription(name: string, description: string): string {
Expand Down Expand Up @@ -99,6 +100,7 @@ function SuspendedByRow({
index,
minTime,
maxTime,
skipName,
}: RowProps) {
const [isOpen, setIsOpen] = useState(false);
const [openIsPending, startOpenTransition] = useTransition();
Expand Down Expand Up @@ -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}>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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>
);
Expand Down
36 changes: 36 additions & 0 deletions packages/react-devtools-shared/src/devtools/views/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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