[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
+}