mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
[DevTools] Assign a different color and label based on environment (#34893)
Stacked on #34892. In the timeline scrubber each timeline entry gets a label and color assigned based on the environment computed for that step. In the rects, we find the timeline step that this boundary is part of and use that environment to assign a color. This is slightly different than picking from the boundary itself since it takes into account parent boundaries. In the "suspended by" section we color each entry individually based on the environment that spawned the I/O. <img width="790" height="813" alt="Screenshot 2025-10-17 at 12 18 56 AM" src="https://github.com/user-attachments/assets/c902b1fb-0992-4e24-8e94-a97ca8507551" />
This commit is contained in:
parent
a083344699
commit
3a669170e9
|
|
@ -154,8 +154,8 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
|
|||
'--color-warning-text-color': '#ffffff',
|
||||
'--color-warning-text-color-inverted': '#fd4d69',
|
||||
|
||||
'--color-suspense': '#0088fa',
|
||||
'--color-transition': '#6a51b2',
|
||||
'--color-suspense-default': '#0088fa',
|
||||
'--color-transition-default': '#6a51b2',
|
||||
'--color-suspense-server': '#62bc6a',
|
||||
'--color-transition-server': '#3f7844',
|
||||
'--color-suspense-other': '#f3ce49',
|
||||
|
|
@ -315,8 +315,8 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
|
|||
'--color-warning-text-color': '#ffffff',
|
||||
'--color-warning-text-color-inverted': '#ee1638',
|
||||
|
||||
'--color-suspense': '#61dafb',
|
||||
'--color-transition': '#6a51b2',
|
||||
'--color-suspense-default': '#61dafb',
|
||||
'--color-transition-default': '#6a51b2',
|
||||
'--color-suspense-server': '#62bc6a',
|
||||
'--color-transition-server': '#3f7844',
|
||||
'--color-suspense-other': '#f3ce49',
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import OwnerView from './OwnerView';
|
|||
import {meta} from '../../../hydration';
|
||||
import useInferredName from '../useInferredName';
|
||||
|
||||
import {getClassNameForEnvironment} from '../SuspenseTab/SuspenseEnvironmentColors.js';
|
||||
|
||||
import type {
|
||||
InspectedElement,
|
||||
SerializedAsyncInfo,
|
||||
|
|
@ -181,7 +183,12 @@ function SuspendedByRow({
|
|||
</>
|
||||
)}
|
||||
<div className={styles.CollapsableHeaderFiller} />
|
||||
<div className={styles.TimeBarContainer}>
|
||||
<div
|
||||
className={
|
||||
styles.TimeBarContainer +
|
||||
' ' +
|
||||
getClassNameForEnvironment(ioInfo.env)
|
||||
}>
|
||||
<div
|
||||
className={
|
||||
!isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
|
||||
|
|
@ -341,6 +348,7 @@ type GroupProps = {
|
|||
inspectedElement: InspectedElement,
|
||||
store: Store,
|
||||
name: string,
|
||||
environment: null | string,
|
||||
suspendedBy: Array<{
|
||||
index: number,
|
||||
value: SerializedAsyncInfo,
|
||||
|
|
@ -355,6 +363,7 @@ function SuspendedByGroup({
|
|||
inspectedElement,
|
||||
store,
|
||||
name,
|
||||
environment,
|
||||
suspendedBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
|
|
@ -407,7 +416,12 @@ function SuspendedByGroup({
|
|||
<span className={styles.CollapsableHeaderTitle}>{pluralizedName}</span>
|
||||
<div className={styles.CollapsableHeaderFiller} />
|
||||
{isOpen ? null : (
|
||||
<div className={styles.TimeBarContainer}>
|
||||
<div
|
||||
className={
|
||||
styles.TimeBarContainer +
|
||||
' ' +
|
||||
getClassNameForEnvironment(environment)
|
||||
}>
|
||||
<div
|
||||
className={
|
||||
!isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
|
||||
|
|
@ -502,17 +516,21 @@ export default function InspectedElementSuspendedBy({
|
|||
const groups = [];
|
||||
let currentGroup = null;
|
||||
let currentGroupName = null;
|
||||
let currentGroupEnv = null;
|
||||
for (let i = 0; i < sortedSuspendedBy.length; i++) {
|
||||
const entry = sortedSuspendedBy[i];
|
||||
const name = entry.value.awaited.name;
|
||||
const env = entry.value.awaited.env;
|
||||
if (
|
||||
currentGroupName !== name ||
|
||||
currentGroupEnv !== env ||
|
||||
!name ||
|
||||
name === 'Promise' ||
|
||||
currentGroup === null
|
||||
) {
|
||||
// Create a new group.
|
||||
currentGroupName = name;
|
||||
currentGroupEnv = env;
|
||||
currentGroup = [];
|
||||
groups.push(currentGroup);
|
||||
}
|
||||
|
|
@ -591,6 +609,7 @@ export default function InspectedElementSuspendedBy({
|
|||
<SuspendedByGroup
|
||||
key={entries[0].index}
|
||||
name={entries[0].value.awaited.name}
|
||||
environment={entries[0].value.awaited.env}
|
||||
suspendedBy={entries}
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
.SuspenseEnvironmentDefault {
|
||||
--color-suspense: var(--color-suspense-default);
|
||||
--color-transition: var(--color-transition-default);
|
||||
}
|
||||
|
||||
.SuspenseEnvironmentServer {
|
||||
--color-suspense: var(--color-suspense-server);
|
||||
--color-transition: var(--color-transition-server);
|
||||
}
|
||||
|
||||
.SuspenseEnvironmentOther {
|
||||
--color-suspense: var(--color-suspense-other);
|
||||
--color-transition: var(--color-transition-other);
|
||||
}
|
||||
20
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseEnvironmentColors.js
vendored
Normal file
20
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseEnvironmentColors.js
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import styles from './SuspenseEnvironmentColors.css';
|
||||
|
||||
export function getClassNameForEnvironment(environment: null | string): string {
|
||||
if (environment === null) {
|
||||
return styles.SuspenseEnvironmentDefault;
|
||||
}
|
||||
if (environment === 'Server') {
|
||||
return styles.SuspenseEnvironmentServer;
|
||||
}
|
||||
return styles.SuspenseEnvironmentOther;
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ import {
|
|||
SuspenseTreeStateContext,
|
||||
SuspenseTreeDispatcherContext,
|
||||
} from './SuspenseTreeContext';
|
||||
import {getClassNameForEnvironment} from './SuspenseEnvironmentColors.js';
|
||||
|
||||
function ScaledRect({
|
||||
className,
|
||||
|
|
@ -157,12 +158,25 @@ function SuspenseRects({
|
|||
hoveredTimelineIndex > -1 &&
|
||||
timeline[hoveredTimelineIndex].id === suspenseID;
|
||||
|
||||
let environment: null | string = null;
|
||||
for (let i = 0; i < timeline.length; i++) {
|
||||
const timelineStep = timeline[i];
|
||||
if (timelineStep.id === suspenseID) {
|
||||
environment = timelineStep.environment;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const boundingBox = getBoundingBox(suspense.rects);
|
||||
|
||||
return (
|
||||
<ScaledRect
|
||||
rect={boundingBox}
|
||||
className={styles.SuspenseRectsBoundary}
|
||||
className={
|
||||
styles.SuspenseRectsBoundary +
|
||||
' ' +
|
||||
getClassNameForEnvironment(environment)
|
||||
}
|
||||
visible={visible}
|
||||
selected={selected}
|
||||
suspended={suspense.isSuspended}
|
||||
|
|
@ -327,9 +341,8 @@ function SuspenseRectsContainer(): React$Node {
|
|||
const treeDispatch = useContext(TreeDispatcherContext);
|
||||
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
||||
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
|
||||
const {roots, hoveredTimelineIndex, uniqueSuspendersOnly} = useContext(
|
||||
SuspenseTreeStateContext,
|
||||
);
|
||||
const {roots, timeline, hoveredTimelineIndex, uniqueSuspendersOnly} =
|
||||
useContext(SuspenseTreeStateContext);
|
||||
|
||||
// TODO: bbox does not consider uniqueSuspendersOnly filter
|
||||
const boundingBox = getDocumentBoundingRect(store, roots);
|
||||
|
|
@ -389,11 +402,16 @@ function SuspenseRectsContainer(): React$Node {
|
|||
}
|
||||
}
|
||||
|
||||
const rootEnvironment =
|
||||
timeline.length === 0 ? null : timeline[0].environment;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
styles.SuspenseRectsContainer +
|
||||
(hasRootSuspenders ? ' ' + styles.SuspenseRectsRoot : '')
|
||||
(hasRootSuspenders ? ' ' + styles.SuspenseRectsRoot : '') +
|
||||
' ' +
|
||||
getClassNameForEnvironment(rootEnvironment)
|
||||
}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import type {SuspenseTimelineStep} from 'react-devtools-shared/src/frontend/types';
|
||||
|
||||
import typeof {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
|
||||
|
||||
import * as React from 'react';
|
||||
|
|
@ -14,11 +16,14 @@ import {useRef} from 'react';
|
|||
|
||||
import styles from './SuspenseScrubber.css';
|
||||
|
||||
import {getClassNameForEnvironment} from './SuspenseEnvironmentColors.js';
|
||||
|
||||
import Tooltip from '../Components/reach-ui/tooltip';
|
||||
|
||||
export default function SuspenseScrubber({
|
||||
min,
|
||||
max,
|
||||
timeline,
|
||||
value,
|
||||
highlight,
|
||||
onBlur,
|
||||
|
|
@ -29,6 +34,7 @@ export default function SuspenseScrubber({
|
|||
}: {
|
||||
min: number,
|
||||
max: number,
|
||||
timeline: $ReadOnlyArray<SuspenseTimelineStep>,
|
||||
value: number,
|
||||
highlight: number,
|
||||
onBlur?: () => void,
|
||||
|
|
@ -54,17 +60,18 @@ export default function SuspenseScrubber({
|
|||
}
|
||||
const steps = [];
|
||||
for (let index = min; index <= max; index++) {
|
||||
const environment = timeline[index].environment;
|
||||
const label =
|
||||
index === min
|
||||
? // The first step in the timeline is always a Transition (Initial Paint).
|
||||
'Initial Paint' +
|
||||
(environment === null ? '' : ' (' + environment + ')')
|
||||
: // TODO: Consider adding the name of this specific boundary if this step has only one.
|
||||
environment === null
|
||||
? 'Suspense'
|
||||
: environment;
|
||||
steps.push(
|
||||
<Tooltip
|
||||
key={index}
|
||||
label={
|
||||
index === min
|
||||
? // The first step in the timeline is always a Transition (Initial Paint).
|
||||
// TODO: Support multiple environments.
|
||||
'Initial Paint'
|
||||
: // TODO: Consider adding the name of this specific boundary if this step has only one.
|
||||
'Suspense'
|
||||
}>
|
||||
<Tooltip key={index} label={label}>
|
||||
<div
|
||||
className={
|
||||
styles.SuspenseScrubberStep +
|
||||
|
|
@ -79,9 +86,10 @@ export default function SuspenseScrubber({
|
|||
styles.SuspenseScrubberBead +
|
||||
(index === min
|
||||
? // The first step in the timeline is always a Transition (Initial Paint).
|
||||
// TODO: Support multiple environments.
|
||||
' ' + styles.SuspenseScrubberBeadTransition
|
||||
: '') +
|
||||
' ' +
|
||||
getClassNameForEnvironment(environment) +
|
||||
(index <= value ? ' ' + styles.SuspenseScrubberBeadSelected : '')
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ function SuspenseTimelineInput() {
|
|||
<SuspenseScrubber
|
||||
min={min}
|
||||
max={max}
|
||||
timeline={timeline}
|
||||
value={timelineIndex}
|
||||
highlight={hoveredTimelineIndex}
|
||||
onChange={handleChange}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user