Repository: facebook/react
Pull request: [DevTools] Redesign Timeline Profiler Sidebar

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



Changed files:
filename: packages/react-devtools-shared/src/devtools/views/Button.css
patch: @@ -5,6 +5,7 @@
   padding: 0;
   border-radius: 0.25rem;
   flex: 0 0 auto;
+  cursor: pointer;
 }
 .ButtonContent {
   display: inline-flex;

filename: packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.css
patch: @@ -20,51 +20,47 @@
 }
 
 .ListItem {
-  margin: 0;
+  flex: 1 1;
+  margin: 0 0 0.5rem;
 }
 
 .Label {
-  display: flex;
-  justify-content: space-between;
-
+  overflow: hidden;
+  text-overflow: ellipsis;
   font-weight: bold;
-}
-
-[data-source="true"]:hover .Label > .Button {
-  background-color: var(--color-background-hover);
+  flex: 1 1;
 }
 
 .Value {
   font-family: var(--font-family-monospace);
   font-size: var(--font-size-monospace-normal);
 }
 
-.NothingSelected {
-  display: flex;
+.Row {  
+  display: flex;  
+  flex-direction: row;  
   align-items: center;
-  justify-content: center;
-  height: 100%;
-  color: var(--color-dim);
-}
-
-.Button {
-  display: flex;
-  flex: 1;
+  border-top: 1px solid var(--color-border);
+} 
 
-  max-width: 95%;
+.UnclickableSource,
+.ClickableSource {
+  width: 100%;
   overflow: hidden;
   text-overflow: ellipsis;
+  font-family: var(--font-family-sans);
+  font-size: var(--font-size-sans-normal);
 }
 
-[data-source="true"] .Button {
-  cursor: pointer;
+.UnclickableSource {
+  color: var(--color-dim);
 }
 
-
-.Button > span {
-  display: block;
-  text-align: left;
+.ClickableSource {
+  color: var(--color-text);
 }
 
-.Source {
+.ClickableSource:focus,
+.ClickableSource:hover {
+  background-color: var(--color-background-hover);
 }

filename: packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js
patch: @@ -15,18 +15,25 @@ import ButtonIcon from '../ButtonIcon';
 import ViewSourceContext from '../Components/ViewSourceContext';
 import {useContext} from 'react';
 import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
+import {
+  formatTimestamp,
+  getSchedulingEventLabel,
+} from 'react-devtools-timeline/src/utils/formatting';
 import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
+import {copy} from 'clipboard-js';
 
 import styles from './SidebarEventInfo.css';
 
 export type Props = {||};
 
-function SchedulingEventInfo({eventInfo}: {eventInfo: SchedulingEvent}) {
-  const {viewUrlSourceFunction} = useContext(ViewSourceContext);
+type SchedulingEventProps = {|
+  eventInfo: SchedulingEvent,
+|};
 
-  const componentStack = eventInfo.componentStack
-    ? stackToComponentSources(eventInfo.componentStack)
-    : null;
+function SchedulingEventInfo({eventInfo}: SchedulingEventProps) {
+  const {viewUrlSourceFunction} = useContext(ViewSourceContext);
+  const {componentName, timestamp} = eventInfo;
+  const componentStack = eventInfo.componentStack || null;
 
   const viewSource = source => {
     if (viewUrlSourceFunction != null && source != null) {
@@ -35,45 +42,60 @@ function SchedulingEventInfo({eventInfo}: {eventInfo: SchedulingEvent}) {
   };
 
   return (
-    <div className={styles.Content} tabIndex={0}>
-      {componentStack ? (
-        <ol className={styles.List}>
-          {componentStack.map(([displayName, source], index) => {
-            const hasSource = source != null;
-
-            return (
-              <li
-                key={index}
-                className={styles.ListItem}
-                data-source={hasSource}>
-                <label className={styles.Label}>
-                  <Button
-                    className={styles.Button}
-                    onClick={() => viewSource(source)}>
-                    {displayName}
-                  </Button>
-                  {hasSource && (
-                    <ButtonIcon className={styles.Source} type="view-source" />
-                  )}
-                </label>
-              </li>
-            );
-          })}
-        </ol>
-      ) : null}
-    </div>
+    <>
+      <div className={styles.Toolbar}>
+        {componentName} {getSchedulingEventLabel(eventInfo)}
+      </div>
+      <div className={styles.Content} tabIndex={0}>
+        <ul className={styles.List}>
+          <li className={styles.ListItem}>
+            <label className={styles.Label}>Timestamp</label>:{' '}
+            <span className={styles.Value}>{formatTimestamp(timestamp)}</span>
+          </li>
+          {componentStack && (
+            <li className={styles.ListItem}>
+              <div className={styles.Row}>
+                <label className={styles.Label}>Rendered by</label>
+                <Button
+                  onClick={() => copy(componentStack)}
+                  title="Copy component stack to clipboard">
+                  <ButtonIcon type="copy" />
+                </Button>
+              </div>
+              <ul className={styles.List}>
+                {stackToComponentSources(componentStack).map(
+                  ([displayName, source], index) => {
+                    return (
+                      <li key={index}>
+                        <Button
+                          className={
+                            source
+                              ? styles.ClickableSource
+                              : styles.UnclickableSource
+                          }
+                          disabled={!source}
+                          onClick={() => viewSource(source)}>
+                          {displayName}
+                        </Button>
+                      </li>
+                    );
+                  },
+                )}
+              </ul>
+            </li>
+          )}
+        </ul>
+      </div>
+    </>
   );
 }
 
 export default function SidebarEventInfo(_: Props) {
   const {selectedEvent} = useContext(TimelineContext);
   // (TODO) Refactor in next PR so this supports multiple types of events
-  return selectedEvent ? (
-    <>
-      <div className={styles.Toolbar}>Event Component Tree</div>
-      {selectedEvent.schedulingEvent ? (
-        <SchedulingEventInfo eventInfo={selectedEvent.schedulingEvent} />
-      ) : null}
-    </>
-  ) : null;
+  if (selectedEvent && selectedEvent.schedulingEvent) {
+    return <SchedulingEventInfo eventInfo={selectedEvent.schedulingEvent} />;
+  }
+
+  return null;
 }

filename: packages/react-devtools-timeline/src/EventTooltip.js
patch: @@ -24,7 +24,12 @@ import type {
 } from './types';
 
 import * as React from 'react';
-import {formatDuration, formatTimestamp, trimString} from './utils/formatting';
+import {
+  formatDuration,
+  formatTimestamp,
+  trimString,
+  getSchedulingEventLabel,
+} from './utils/formatting';
 import {getBatchRange} from './utils/getBatchRange';
 import useSmartTooltip from './utils/useSmartTooltip';
 import styles from './EventTooltip.css';
@@ -40,19 +45,6 @@ type Props = {|
   width: number,
 |};
 
-function getSchedulingEventLabel(event: SchedulingEvent): string | null {
-  switch (event.type) {
-    case 'schedule-render':
-      return 'render scheduled';
-    case 'schedule-state-update':
-      return 'state update scheduled';
-    case 'schedule-force-update':
-      return 'force update scheduled';
-    default:
-      return null;
-  }
-}
-
 function getReactMeasureLabel(type): string | null {
   switch (type) {
     case 'commit':

filename: packages/react-devtools-timeline/src/utils/formatting.js
patch: @@ -7,6 +7,8 @@
  * @flow
  */
 
+import type {SchedulingEvent} from '../types';
+
 import prettyMilliseconds from 'pretty-ms';
 
 export function formatTimestamp(ms: number) {
@@ -28,3 +30,16 @@ export function trimString(string: string, length: number): string {
   }
   return string;
 }
+
+export function getSchedulingEventLabel(event: SchedulingEvent): string | null {
+  switch (event.type) {
+    case 'schedule-render':
+      return 'render scheduled';
+    case 'schedule-state-update':
+      return 'state update scheduled';
+    case 'schedule-force-update':
+      return 'force update scheduled';
+    default:
+      return null;
+  }
+}