@samitouri / QOS-React-2 / commits / 4e1fcfa771

[DevTools] Resign Timeline Profiler Sidebar (#24816)

This PR: * Redesigned the sidebar to resemble the flamegraph profiler sidebar and added title and timestamp to the sidebar * Added ability to copy the component stack (for places where you're unable to link to source) https://user-images.githubusercontent.com/2735514/176564897-5301d6d4-429a-4ea3-86cd-74427cff4ce6.mov

Luna Ruan committed Jun 29, 2022 at 20:54 UTC 4e1fcfa77175765a7d51d70a2e519569012593d8
5 files changed +106 -80
packages/react-devtools-shared/src/devtools/views/Button.css
+1
@@ -5,6 +5,7 @@
5 padding: 0;
6 border-radius: 0.25rem;
7 flex: 0 0 auto;
8 + cursor: pointer;
9 }
10 .ButtonContent {
11 display: inline-flex;
packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.css
+22 -26
@@ -20,18 +20,15 @@
20 }
21
22 .ListItem {
23 - margin: 0;
23 + flex: 1 1;
24 + margin: 0 0 0.5rem;
25 }
26
27 .Label {
27 - display: flex;
28 - justify-content: space-between;
29 -
28 + overflow: hidden;
29 + text-overflow: ellipsis;
30 font-weight: bold;
31 -}
32 -
33 -[data-source="true"]:hover .Label > .Button {
34 - background-color: var(--color-background-hover);
31 + flex: 1 1;
32 }
33
34 .Value {
@@ -39,32 +36,31 @@
36 font-size: var(--font-size-monospace-normal);
37 }
38
42 -.NothingSelected {
43 - display: flex;
39 +.Row {
40 + display: flex;
41 + flex-direction: row;
42 align-items: center;
45 - justify-content: center;
46 - height: 100%;
47 - color: var(--color-dim);
48 -}
49 -
50 -.Button {
51 - display: flex;
52 - flex: 1;
43 + border-top: 1px solid var(--color-border);
44 +}
45
54 - max-width: 95%;
46 +.UnclickableSource,
47 +.ClickableSource {
48 + width: 100%;
49 overflow: hidden;
50 text-overflow: ellipsis;
51 + font-family: var(--font-family-sans);
52 + font-size: var(--font-size-sans-normal);
53 }
54
59 -[data-source="true"] .Button {
60 - cursor: pointer;
55 +.UnclickableSource {
56 + color: var(--color-dim);
57 }
58
63 -
64 -.Button > span {
65 - display: block;
66 - text-align: left;
59 +.ClickableSource {
60 + color: var(--color-text);
61 }
62
69 -.Source {
63 +.ClickableSource:focus,
64 +.ClickableSource:hover {
65 + background-color: var(--color-background-hover);
66 }
packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js
+62 -40
@@ -15,18 +15,25 @@ import ButtonIcon from '../ButtonIcon';
15 import ViewSourceContext from '../Components/ViewSourceContext';
16 import {useContext} from 'react';
17 import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
18 +import {
19 + formatTimestamp,
20 + getSchedulingEventLabel,
21 +} from 'react-devtools-timeline/src/utils/formatting';
22 import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
23 +import {copy} from 'clipboard-js';
24
25 import styles from './SidebarEventInfo.css';
26
27 export type Props = {||};
28
24 -function SchedulingEventInfo({eventInfo}: {eventInfo: SchedulingEvent}) {
25 - const {viewUrlSourceFunction} = useContext(ViewSourceContext);
29 +type SchedulingEventProps = {|
30 + eventInfo: SchedulingEvent,
31 +|};
32
27 - const componentStack = eventInfo.componentStack
28 - ? stackToComponentSources(eventInfo.componentStack)
29 - : null;
33 +function SchedulingEventInfo({eventInfo}: SchedulingEventProps) {
34 + const {viewUrlSourceFunction} = useContext(ViewSourceContext);
35 + const {componentName, timestamp} = eventInfo;
36 + const componentStack = eventInfo.componentStack || null;
37
38 const viewSource = source => {
39 if (viewUrlSourceFunction != null && source != null) {
@@ -35,45 +42,60 @@ function SchedulingEventInfo({eventInfo}: {eventInfo: SchedulingEvent}) {
42 };
43
44 return (
38 - <div className={styles.Content} tabIndex={0}>
39 - {componentStack ? (
40 - <ol className={styles.List}>
41 - {componentStack.map(([displayName, source], index) => {
42 - const hasSource = source != null;
43 -
44 - return (
45 - <li
46 - key={index}
47 - className={styles.ListItem}
48 - data-source={hasSource}>
49 - <label className={styles.Label}>
50 - <Button
51 - className={styles.Button}
52 - onClick={() => viewSource(source)}>
53 - {displayName}
54 - </Button>
55 - {hasSource && (
56 - <ButtonIcon className={styles.Source} type="view-source" />
57 - )}
58 - </label>
59 - </li>
60 - );
61 - })}
62 - </ol>
63 - ) : null}
64 - </div>
45 + <>
46 + <div className={styles.Toolbar}>
47 + {componentName} {getSchedulingEventLabel(eventInfo)}
48 + </div>
49 + <div className={styles.Content} tabIndex={0}>
50 + <ul className={styles.List}>
51 + <li className={styles.ListItem}>
52 + <label className={styles.Label}>Timestamp</label>:{' '}
53 + <span className={styles.Value}>{formatTimestamp(timestamp)}</span>
54 + </li>
55 + {componentStack && (
56 + <li className={styles.ListItem}>
57 + <div className={styles.Row}>
58 + <label className={styles.Label}>Rendered by</label>
59 + <Button
60 + onClick={() => copy(componentStack)}
61 + title="Copy component stack to clipboard">
62 + <ButtonIcon type="copy" />
63 + </Button>
64 + </div>
65 + <ul className={styles.List}>
66 + {stackToComponentSources(componentStack).map(
67 + ([displayName, source], index) => {
68 + return (
69 + <li key={index}>
70 + <Button
71 + className={
72 + source
73 + ? styles.ClickableSource
74 + : styles.UnclickableSource
75 + }
76 + disabled={!source}
77 + onClick={() => viewSource(source)}>
78 + {displayName}
79 + </Button>
80 + </li>
81 + );
82 + },
83 + )}
84 + </ul>
85 + </li>
86 + )}
87 + </ul>
88 + </div>
89 + </>
90 );
91 }
92
93 export default function SidebarEventInfo(_: Props) {
94 const {selectedEvent} = useContext(TimelineContext);
95 // (TODO) Refactor in next PR so this supports multiple types of events
71 - return selectedEvent ? (
72 - <>
73 - <div className={styles.Toolbar}>Event Component Tree</div>
74 - {selectedEvent.schedulingEvent ? (
75 - <SchedulingEventInfo eventInfo={selectedEvent.schedulingEvent} />
76 - ) : null}
77 - </>
78 - ) : null;
96 + if (selectedEvent && selectedEvent.schedulingEvent) {
97 + return <SchedulingEventInfo eventInfo={selectedEvent.schedulingEvent} />;
98 + }
99 +
100 + return null;
101 }
packages/react-devtools-timeline/src/EventTooltip.js
+6 -14
@@ -24,7 +24,12 @@ import type {
24 } from './types';
25
26 import * as React from 'react';
27 -import {formatDuration, formatTimestamp, trimString} from './utils/formatting';
27 +import {
28 + formatDuration,
29 + formatTimestamp,
30 + trimString,
31 + getSchedulingEventLabel,
32 +} from './utils/formatting';
33 import {getBatchRange} from './utils/getBatchRange';
34 import useSmartTooltip from './utils/useSmartTooltip';
35 import styles from './EventTooltip.css';
@@ -40,19 +45,6 @@ type Props = {|
45 width: number,
46 |};
47
43 -function getSchedulingEventLabel(event: SchedulingEvent): string | null {
44 - switch (event.type) {
45 - case 'schedule-render':
46 - return 'render scheduled';
47 - case 'schedule-state-update':
48 - return 'state update scheduled';
49 - case 'schedule-force-update':
50 - return 'force update scheduled';
51 - default:
52 - return null;
53 - }
54 -}
55 -
48 function getReactMeasureLabel(type): string | null {
49 switch (type) {
50 case 'commit':
packages/react-devtools-timeline/src/utils/formatting.js
+15
@@ -7,6 +7,8 @@
7 * @flow
8 */
9
10 +import type {SchedulingEvent} from '../types';
11 +
12 import prettyMilliseconds from 'pretty-ms';
13
14 export function formatTimestamp(ms: number) {
@@ -28,3 +30,16 @@ export function trimString(string: string, length: number): string {
30 }
31 return string;
32 }
33 +
34 +export function getSchedulingEventLabel(event: SchedulingEvent): string | null {
35 + switch (event.type) {
36 + case 'schedule-render':
37 + return 'render scheduled';
38 + case 'schedule-state-update':
39 + return 'state update scheduled';
40 + case 'schedule-force-update':
41 + return 'force update scheduled';
42 + default:
43 + return null;
44 + }
45 +}