diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index 984408500d..2b35e82363 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -22,6 +22,8 @@ import { addObjectToProperties, } from 'shared/ReactPerformanceTrackProperties'; +import {getIODescription} from 'shared/ReactIODescription'; + const supportsUserTiming = enableProfilerTimer && typeof console !== 'undefined' && @@ -300,70 +302,6 @@ function getIOColor( } } -function getIODescription(value: any): string { - if (!__DEV__) { - return ''; - } - try { - switch (typeof value) { - case 'object': - // Test the object for a bunch of common property names that are useful identifiers. - // While we only have the return value here, it should ideally be a name that - // describes the arguments requested. - if (value === null) { - return ''; - } else if (value instanceof Error) { - // eslint-disable-next-line react-internal/safe-string-coercion - return String(value.message); - } else if (typeof value.url === 'string') { - return value.url; - } else if (typeof value.command === 'string') { - return value.command; - } else if ( - typeof value.request === 'object' && - typeof value.request.url === 'string' - ) { - return value.request.url; - } else if ( - typeof value.response === 'object' && - typeof value.response.url === 'string' - ) { - return value.response.url; - } else if ( - typeof value.id === 'string' || - typeof value.id === 'number' || - typeof value.id === 'bigint' - ) { - // eslint-disable-next-line react-internal/safe-string-coercion - return String(value.id); - } else if (typeof value.name === 'string') { - return value.name; - } else { - const str = value.toString(); - if (str.startWith('[object ') || str.length < 5 || str.length > 500) { - // This is probably not a useful description. - return ''; - } - return str; - } - case 'string': - if (value.length < 5 || value.length > 500) { - return ''; - } - return value; - case 'number': - case 'bigint': - // eslint-disable-next-line react-internal/safe-string-coercion - return String(value); - default: - // Not useful descriptors. - return ''; - } - } catch (x) { - return ''; - } -} - function getIOLongName( ioInfo: ReactIOInfo, description: string, diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 31db8a7433..b5b7fe9658 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -105,6 +105,8 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent import is from 'shared/objectIs'; import hasOwnProperty from 'shared/hasOwnProperty'; +import {getIODescription} from 'shared/ReactIODescription'; + import { getStackByFiberInDevAndProd, getOwnerStackByFiberInDev, @@ -4116,9 +4118,26 @@ export function attach( parentInstance, asyncInfo.owner, ); + const value: any = ioInfo.value; + let resolvedValue = undefined; + if ( + typeof value === 'object' && + value !== null && + typeof value.then === 'function' + ) { + switch (value.status) { + case 'fulfilled': + resolvedValue = value.value; + break; + case 'rejected': + resolvedValue = value.reason; + break; + } + } return { awaited: { name: ioInfo.name, + description: getIODescription(resolvedValue), start: ioInfo.start, end: ioInfo.end, value: ioInfo.value == null ? null : ioInfo.value, diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 979baa3e0a..6324e63da1 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -235,6 +235,7 @@ export type PathMatch = { // Serialized version of ReactIOInfo export type SerializedIOInfo = { name: string, + description: string, start: number, end: number, value: null | Promise, diff --git a/packages/react-devtools-shared/src/backendAPI.js b/packages/react-devtools-shared/src/backendAPI.js index 9f96215026..d6aa18cd31 100644 --- a/packages/react-devtools-shared/src/backendAPI.js +++ b/packages/react-devtools-shared/src/backendAPI.js @@ -218,6 +218,7 @@ function backendToFrontendSerializedAsyncInfo( return { awaited: { name: ioInfo.name, + description: ioInfo.description, start: ioInfo.start, end: ioInfo.end, value: ioInfo.value, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css index c0d3c95bec..41e510b7c1 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css @@ -75,11 +75,25 @@ color: var(--color-expand-collapse-toggle); } -.CollapsableHeaderTitle { - flex: 1 1 auto; +.CollapsableHeaderTitle, .CollapsableHeaderDescription, .CollapsableHeaderSeparator, .CollapsableHeaderFiller { font-family: var(--font-family-monospace); font-size: var(--font-size-monospace-normal); text-align: left; + white-space: nowrap; +} +.CollapsableHeaderTitle { + flex: 0 1 auto; + overflow: hidden; + text-overflow: ellipsis; +} + +.CollapsableHeaderSeparator { + flex: 0 0 auto; + white-space: pre; +} + +.CollapsableHeaderFiller { + flex: 1 0 0; } .CollapsableContent { @@ -108,4 +122,4 @@ .TimeBarSpanErrored { background-color: var(--color-timespan-background-errored); -} \ No newline at end of file +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js index 93f4078a0e..9deddef14b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js @@ -38,6 +38,37 @@ type RowProps = { maxTime: number, }; +function getShortDescription(name: string, description: string): string { + const descMaxLength = 30 - name.length; + if (descMaxLength > 1) { + const l = description.length; + if (l > 0 && l <= descMaxLength) { + // We can fit the full description + return description; + } else if ( + description.startsWith('http://') || + description.startsWith('https://') || + description.startsWith('/') + ) { + // Looks like a URL. Let's see if we can extract something shorter. + // We don't have to do a full parse so let's try something cheaper. + let queryIdx = description.indexOf('?'); + if (queryIdx === -1) { + queryIdx = description.length; + } + if (description.charCodeAt(queryIdx - 1) === 47 /* "/" */) { + // Ends with slash. Look before that. + queryIdx--; + } + const slashIdx = description.lastIndexOf('/', queryIdx - 1); + // This may now be either the file name or the host. + // Include the slash to make it more obvious what we trimmed. + return '…' + description.slice(slashIdx, queryIdx); + } + } + return ''; +} + function SuspendedByRow({ bridge, element, @@ -50,6 +81,9 @@ function SuspendedByRow({ }: RowProps) { const [isOpen, setIsOpen] = useState(false); const name = asyncInfo.awaited.name; + const description = asyncInfo.awaited.description; + const longName = description === '' ? name : name + ' (' + description + ')'; + const shortDescription = getShortDescription(name, description); let stack; let owner; if (asyncInfo.stack === null || asyncInfo.stack.length === 0) { @@ -83,12 +117,22 @@ function SuspendedByRow({