header.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839
  1. import * as React from 'react';
  2. import styled from '@emotion/styled';
  3. import OpsBreakdown from 'app/components/events/opsBreakdown';
  4. import {
  5. DividerSpacer,
  6. ScrollbarContainer,
  7. VirtualScrollbar,
  8. VirtualScrollbarGrip,
  9. } from 'app/components/performance/waterfall/miniHeader';
  10. import {
  11. getHumanDuration,
  12. pickBarColor,
  13. rectOfContent,
  14. toPercent,
  15. } from 'app/components/performance/waterfall/utils';
  16. import ConfigStore from 'app/stores/configStore';
  17. import space from 'app/styles/space';
  18. import {Organization} from 'app/types';
  19. import {EventTransaction} from 'app/types/event';
  20. import theme from 'app/utils/theme';
  21. import {
  22. MINIMAP_CONTAINER_HEIGHT,
  23. MINIMAP_HEIGHT,
  24. TIME_AXIS_HEIGHT,
  25. VIEW_HANDLE_HEIGHT,
  26. } from './constants';
  27. import * as CursorGuideHandler from './cursorGuideHandler';
  28. import * as DividerHandlerManager from './dividerHandlerManager';
  29. import {DragManagerChildrenProps} from './dragManager';
  30. import {ActiveOperationFilter} from './filter';
  31. import MeasurementsPanel from './measurementsPanel';
  32. import * as ScrollbarManager from './scrollbarManager';
  33. import {
  34. EnhancedProcessedSpanType,
  35. ParsedTraceType,
  36. RawSpanType,
  37. TickAlignment,
  38. } from './types';
  39. import {
  40. boundsGenerator,
  41. getSpanOperation,
  42. SpanBoundsType,
  43. SpanGeneratedBoundsType,
  44. } from './utils';
  45. type PropType = {
  46. organization: Organization;
  47. minimapInteractiveRef: React.RefObject<HTMLDivElement>;
  48. virtualScrollBarContainerRef: React.RefObject<HTMLDivElement>;
  49. dragProps: DragManagerChildrenProps;
  50. trace: ParsedTraceType;
  51. event: EventTransaction;
  52. operationNameFilters: ActiveOperationFilter;
  53. rootSpan: RawSpanType;
  54. spans: EnhancedProcessedSpanType[];
  55. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
  56. };
  57. type State = {
  58. minimapWidth: number | undefined;
  59. };
  60. class TraceViewHeader extends React.Component<PropType, State> {
  61. state: State = {
  62. minimapWidth: undefined,
  63. };
  64. componentDidMount() {
  65. this.fetchMinimapWidth();
  66. }
  67. componentDidUpdate() {
  68. this.fetchMinimapWidth();
  69. }
  70. fetchMinimapWidth() {
  71. const {minimapInteractiveRef} = this.props;
  72. if (minimapInteractiveRef.current) {
  73. const minimapWidth = minimapInteractiveRef.current.getBoundingClientRect().width;
  74. if (minimapWidth !== this.state.minimapWidth) {
  75. // eslint-disable-next-line react/no-did-update-set-state
  76. this.setState({
  77. minimapWidth,
  78. });
  79. }
  80. }
  81. }
  82. renderCursorGuide({
  83. cursorGuideHeight,
  84. showCursorGuide,
  85. mouseLeft,
  86. }: {
  87. cursorGuideHeight: number;
  88. showCursorGuide: boolean;
  89. mouseLeft: number | undefined;
  90. }) {
  91. if (!showCursorGuide || !mouseLeft) {
  92. return null;
  93. }
  94. return (
  95. <CursorGuide
  96. style={{
  97. left: toPercent(mouseLeft),
  98. height: `${cursorGuideHeight}px`,
  99. }}
  100. />
  101. );
  102. }
  103. renderViewHandles({
  104. isDragging,
  105. onLeftHandleDragStart,
  106. leftHandlePosition,
  107. onRightHandleDragStart,
  108. rightHandlePosition,
  109. viewWindowStart,
  110. viewWindowEnd,
  111. }: DragManagerChildrenProps) {
  112. const leftHandleGhost = isDragging ? (
  113. <Handle
  114. left={viewWindowStart}
  115. onMouseDown={() => {
  116. // do nothing
  117. }}
  118. isDragging={false}
  119. />
  120. ) : null;
  121. const leftHandle = (
  122. <Handle
  123. left={leftHandlePosition}
  124. onMouseDown={onLeftHandleDragStart}
  125. isDragging={isDragging}
  126. />
  127. );
  128. const rightHandle = (
  129. <Handle
  130. left={rightHandlePosition}
  131. onMouseDown={onRightHandleDragStart}
  132. isDragging={isDragging}
  133. />
  134. );
  135. const rightHandleGhost = isDragging ? (
  136. <Handle
  137. left={viewWindowEnd}
  138. onMouseDown={() => {
  139. // do nothing
  140. }}
  141. isDragging={false}
  142. />
  143. ) : null;
  144. return (
  145. <React.Fragment>
  146. {leftHandleGhost}
  147. {rightHandleGhost}
  148. {leftHandle}
  149. {rightHandle}
  150. </React.Fragment>
  151. );
  152. }
  153. renderFog(dragProps: DragManagerChildrenProps) {
  154. return (
  155. <React.Fragment>
  156. <Fog style={{height: '100%', width: toPercent(dragProps.viewWindowStart)}} />
  157. <Fog
  158. style={{
  159. height: '100%',
  160. width: toPercent(1 - dragProps.viewWindowEnd),
  161. left: toPercent(dragProps.viewWindowEnd),
  162. }}
  163. />
  164. </React.Fragment>
  165. );
  166. }
  167. renderDurationGuide({
  168. showCursorGuide,
  169. mouseLeft,
  170. }: {
  171. showCursorGuide: boolean;
  172. mouseLeft: number | undefined;
  173. }) {
  174. if (!showCursorGuide || !mouseLeft) {
  175. return null;
  176. }
  177. const interactiveLayer = this.props.minimapInteractiveRef.current;
  178. if (!interactiveLayer) {
  179. return null;
  180. }
  181. const rect = rectOfContent(interactiveLayer);
  182. const {trace} = this.props;
  183. const duration =
  184. mouseLeft * Math.abs(trace.traceEndTimestamp - trace.traceStartTimestamp);
  185. const style = {top: 0, left: `calc(${mouseLeft * 100}% + 4px)`};
  186. const alignLeft = (1 - mouseLeft) * rect.width <= 100;
  187. return (
  188. <DurationGuideBox style={style} alignLeft={alignLeft}>
  189. <span>{getHumanDuration(duration)}</span>
  190. </DurationGuideBox>
  191. );
  192. }
  193. renderTicks() {
  194. const {trace} = this.props;
  195. const {minimapWidth} = this.state;
  196. const duration = Math.abs(trace.traceEndTimestamp - trace.traceStartTimestamp);
  197. let numberOfParts = 5;
  198. if (minimapWidth) {
  199. if (minimapWidth <= 350) {
  200. numberOfParts = 4;
  201. }
  202. if (minimapWidth <= 280) {
  203. numberOfParts = 3;
  204. }
  205. if (minimapWidth <= 160) {
  206. numberOfParts = 2;
  207. }
  208. if (minimapWidth <= 130) {
  209. numberOfParts = 1;
  210. }
  211. }
  212. if (numberOfParts === 1) {
  213. return (
  214. <TickLabel
  215. key="1"
  216. duration={duration * 0.5}
  217. style={{
  218. left: toPercent(0.5),
  219. }}
  220. />
  221. );
  222. }
  223. const segment = 1 / (numberOfParts - 1);
  224. const ticks: React.ReactNode[] = [];
  225. for (let currentPart = 0; currentPart < numberOfParts; currentPart++) {
  226. if (currentPart === 0) {
  227. ticks.push(
  228. <TickLabel
  229. key="first"
  230. align={TickAlignment.Left}
  231. hideTickMarker
  232. duration={0}
  233. style={{
  234. left: space(1),
  235. }}
  236. />
  237. );
  238. continue;
  239. }
  240. if (currentPart === numberOfParts - 1) {
  241. ticks.push(
  242. <TickLabel
  243. key="last"
  244. duration={duration}
  245. align={TickAlignment.Right}
  246. hideTickMarker
  247. style={{
  248. right: space(1),
  249. }}
  250. />
  251. );
  252. continue;
  253. }
  254. const progress = segment * currentPart;
  255. ticks.push(
  256. <TickLabel
  257. key={String(currentPart)}
  258. duration={duration * progress}
  259. style={{
  260. left: toPercent(progress),
  261. }}
  262. />
  263. );
  264. }
  265. return ticks;
  266. }
  267. renderTimeAxis({
  268. showCursorGuide,
  269. mouseLeft,
  270. }: {
  271. showCursorGuide: boolean;
  272. mouseLeft: number | undefined;
  273. }) {
  274. return (
  275. <TimeAxis>
  276. {this.renderTicks()}
  277. {this.renderCursorGuide({
  278. showCursorGuide,
  279. mouseLeft,
  280. cursorGuideHeight: TIME_AXIS_HEIGHT,
  281. })}
  282. {this.renderDurationGuide({
  283. showCursorGuide,
  284. mouseLeft,
  285. })}
  286. </TimeAxis>
  287. );
  288. }
  289. renderWindowSelection(dragProps: DragManagerChildrenProps) {
  290. if (!dragProps.isWindowSelectionDragging) {
  291. return null;
  292. }
  293. const left = Math.min(
  294. dragProps.windowSelectionInitial,
  295. dragProps.windowSelectionCurrent
  296. );
  297. return (
  298. <WindowSelection
  299. style={{
  300. left: toPercent(left),
  301. width: toPercent(dragProps.windowSelectionSize),
  302. }}
  303. />
  304. );
  305. }
  306. generateBounds() {
  307. const {dragProps, trace} = this.props;
  308. return boundsGenerator({
  309. traceStartTimestamp: trace.traceStartTimestamp,
  310. traceEndTimestamp: trace.traceEndTimestamp,
  311. viewStart: dragProps.viewWindowStart,
  312. viewEnd: dragProps.viewWindowEnd,
  313. });
  314. }
  315. renderSecondaryHeader() {
  316. const {event} = this.props;
  317. const hasMeasurements = Object.keys(event.measurements ?? {}).length > 0;
  318. return (
  319. <DividerHandlerManager.Consumer>
  320. {dividerHandlerChildrenProps => {
  321. const {dividerPosition} = dividerHandlerChildrenProps;
  322. return (
  323. <SecondaryHeader>
  324. <ScrollbarManager.Consumer>
  325. {({virtualScrollbarRef, scrollBarAreaRef, onDragStart, onScroll}) => {
  326. return (
  327. <ScrollbarContainer
  328. ref={this.props.virtualScrollBarContainerRef}
  329. style={{
  330. // the width of this component is shrunk to compensate for half of the width of the divider line
  331. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  332. }}
  333. onScroll={onScroll}
  334. >
  335. <div
  336. style={{
  337. width: 0,
  338. height: '1px',
  339. }}
  340. ref={scrollBarAreaRef}
  341. />
  342. <VirtualScrollbar
  343. data-type="virtual-scrollbar"
  344. ref={virtualScrollbarRef}
  345. onMouseDown={onDragStart}
  346. >
  347. <VirtualScrollbarGrip />
  348. </VirtualScrollbar>
  349. </ScrollbarContainer>
  350. );
  351. }}
  352. </ScrollbarManager.Consumer>
  353. <DividerSpacer />
  354. {hasMeasurements ? (
  355. <MeasurementsPanel
  356. event={event}
  357. generateBounds={this.generateBounds()}
  358. dividerPosition={dividerPosition}
  359. />
  360. ) : null}
  361. </SecondaryHeader>
  362. );
  363. }}
  364. </DividerHandlerManager.Consumer>
  365. );
  366. }
  367. render() {
  368. return (
  369. <HeaderContainer>
  370. <DividerHandlerManager.Consumer>
  371. {dividerHandlerChildrenProps => {
  372. const {dividerPosition} = dividerHandlerChildrenProps;
  373. return (
  374. <React.Fragment>
  375. <OperationsBreakdown
  376. style={{
  377. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  378. }}
  379. >
  380. {this.props.event && (
  381. <OpsBreakdown
  382. operationNameFilters={this.props.operationNameFilters}
  383. event={this.props.event}
  384. topN={3}
  385. hideHeader
  386. />
  387. )}
  388. </OperationsBreakdown>
  389. <DividerSpacer
  390. style={{
  391. position: 'absolute',
  392. top: 0,
  393. left: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  394. height: `${MINIMAP_HEIGHT + TIME_AXIS_HEIGHT}px`,
  395. }}
  396. />
  397. <ActualMinimap
  398. spans={this.props.spans}
  399. generateBounds={this.props.generateBounds}
  400. dividerPosition={dividerPosition}
  401. rootSpan={this.props.rootSpan}
  402. />
  403. <CursorGuideHandler.Consumer>
  404. {({
  405. displayCursorGuide,
  406. hideCursorGuide,
  407. mouseLeft,
  408. showCursorGuide,
  409. }) => (
  410. <RightSidePane
  411. ref={this.props.minimapInteractiveRef}
  412. style={{
  413. width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
  414. left: `calc(${toPercent(dividerPosition)} + 0.5px)`,
  415. }}
  416. onMouseEnter={event => {
  417. displayCursorGuide(event.pageX);
  418. }}
  419. onMouseLeave={() => {
  420. hideCursorGuide();
  421. }}
  422. onMouseMove={event => {
  423. displayCursorGuide(event.pageX);
  424. }}
  425. onMouseDown={event => {
  426. const target = event.target;
  427. if (
  428. target instanceof Element &&
  429. target.getAttribute &&
  430. target.getAttribute('data-ignore')
  431. ) {
  432. // ignore this event if we need to
  433. return;
  434. }
  435. this.props.dragProps.onWindowSelectionDragStart(event);
  436. }}
  437. >
  438. <MinimapContainer>
  439. {this.renderFog(this.props.dragProps)}
  440. {this.renderCursorGuide({
  441. showCursorGuide,
  442. mouseLeft,
  443. cursorGuideHeight: MINIMAP_HEIGHT,
  444. })}
  445. {this.renderViewHandles(this.props.dragProps)}
  446. {this.renderWindowSelection(this.props.dragProps)}
  447. </MinimapContainer>
  448. {this.renderTimeAxis({
  449. showCursorGuide,
  450. mouseLeft,
  451. })}
  452. </RightSidePane>
  453. )}
  454. </CursorGuideHandler.Consumer>
  455. {this.renderSecondaryHeader()}
  456. </React.Fragment>
  457. );
  458. }}
  459. </DividerHandlerManager.Consumer>
  460. </HeaderContainer>
  461. );
  462. }
  463. }
  464. class ActualMinimap extends React.PureComponent<{
  465. spans: EnhancedProcessedSpanType[];
  466. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
  467. dividerPosition: number;
  468. rootSpan: RawSpanType;
  469. }> {
  470. renderRootSpan(): React.ReactNode {
  471. const {spans, generateBounds} = this.props;
  472. return spans.map(payload => {
  473. switch (payload.type) {
  474. case 'root_span':
  475. case 'span':
  476. case 'span_group_chain': {
  477. const {span} = payload;
  478. const spanBarColor: string = pickBarColor(getSpanOperation(span));
  479. const bounds = generateBounds({
  480. startTimestamp: span.start_timestamp,
  481. endTimestamp: span.timestamp,
  482. });
  483. const {left: spanLeft, width: spanWidth} = this.getBounds(bounds);
  484. return (
  485. <MinimapSpanBar
  486. style={{
  487. backgroundColor:
  488. payload.type === 'span_group_chain' ? theme.blue300 : spanBarColor,
  489. left: spanLeft,
  490. width: spanWidth,
  491. }}
  492. />
  493. );
  494. }
  495. default: {
  496. return null;
  497. }
  498. }
  499. });
  500. }
  501. getBounds(bounds: SpanGeneratedBoundsType): {
  502. left: string;
  503. width: string;
  504. } {
  505. switch (bounds.type) {
  506. case 'TRACE_TIMESTAMPS_EQUAL':
  507. case 'INVALID_VIEW_WINDOW': {
  508. return {
  509. left: toPercent(0),
  510. width: '0px',
  511. };
  512. }
  513. case 'TIMESTAMPS_EQUAL': {
  514. return {
  515. left: toPercent(bounds.start),
  516. width: `${bounds.width}px`,
  517. };
  518. }
  519. case 'TIMESTAMPS_REVERSED':
  520. case 'TIMESTAMPS_STABLE': {
  521. return {
  522. left: toPercent(bounds.start),
  523. width: toPercent(bounds.end - bounds.start),
  524. };
  525. }
  526. default: {
  527. const _exhaustiveCheck: never = bounds;
  528. return _exhaustiveCheck;
  529. }
  530. }
  531. }
  532. render() {
  533. const {dividerPosition} = this.props;
  534. return (
  535. <MinimapBackground
  536. style={{
  537. // the width of this component is shrunk to compensate for half of the width of the divider line
  538. width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
  539. left: `calc(${toPercent(dividerPosition)} + 0.5px)`,
  540. }}
  541. >
  542. <BackgroundSlider id="minimap-background-slider">
  543. {this.renderRootSpan()}
  544. </BackgroundSlider>
  545. </MinimapBackground>
  546. );
  547. }
  548. }
  549. const TimeAxis = styled('div')`
  550. width: 100%;
  551. position: absolute;
  552. left: 0;
  553. top: ${MINIMAP_HEIGHT}px;
  554. border-top: 1px solid ${p => p.theme.border};
  555. height: ${TIME_AXIS_HEIGHT}px;
  556. background-color: ${p => p.theme.background};
  557. color: ${p => p.theme.gray300};
  558. font-size: 10px;
  559. font-weight: 500;
  560. overflow: hidden;
  561. `;
  562. const TickLabelContainer = styled('div')`
  563. height: ${TIME_AXIS_HEIGHT}px;
  564. position: absolute;
  565. top: 0;
  566. display: flex;
  567. align-items: center;
  568. user-select: none;
  569. `;
  570. const TickText = styled('span')<{align: TickAlignment}>`
  571. position: absolute;
  572. line-height: 1;
  573. white-space: nowrap;
  574. ${({align}) => {
  575. switch (align) {
  576. case TickAlignment.Center: {
  577. return 'transform: translateX(-50%)';
  578. }
  579. case TickAlignment.Left: {
  580. return null;
  581. }
  582. case TickAlignment.Right: {
  583. return 'transform: translateX(-100%)';
  584. }
  585. default: {
  586. throw Error(`Invalid tick alignment: ${align}`);
  587. }
  588. }
  589. }};
  590. `;
  591. const TickMarker = styled('div')`
  592. width: 1px;
  593. height: 4px;
  594. background-color: ${p => p.theme.gray200};
  595. position: absolute;
  596. top: 0;
  597. left: 0;
  598. transform: translateX(-50%);
  599. `;
  600. const TickLabel = (props: {
  601. style: React.CSSProperties;
  602. hideTickMarker?: boolean;
  603. align?: TickAlignment;
  604. duration: number;
  605. }) => {
  606. const {style, duration, hideTickMarker = false, align = TickAlignment.Center} = props;
  607. return (
  608. <TickLabelContainer style={style}>
  609. {hideTickMarker ? null : <TickMarker />}
  610. <TickText align={align}>{getHumanDuration(duration)}</TickText>
  611. </TickLabelContainer>
  612. );
  613. };
  614. const DurationGuideBox = styled('div')<{alignLeft: boolean}>`
  615. position: absolute;
  616. background-color: ${p => p.theme.background};
  617. padding: 4px;
  618. height: 100%;
  619. border-radius: 3px;
  620. border: 1px solid rgba(0, 0, 0, 0.1);
  621. line-height: 1;
  622. white-space: nowrap;
  623. ${({alignLeft}) => {
  624. if (!alignLeft) {
  625. return null;
  626. }
  627. return 'transform: translateX(-100%) translateX(-8px);';
  628. }};
  629. `;
  630. const HeaderContainer = styled('div')`
  631. width: 100%;
  632. position: sticky;
  633. left: 0;
  634. top: ${p => (ConfigStore.get('demoMode') ? p.theme.demo.headerSize : 0)};
  635. z-index: ${p => p.theme.zIndex.traceView.minimapContainer};
  636. background-color: ${p => p.theme.background};
  637. border-bottom: 1px solid ${p => p.theme.border};
  638. height: ${MINIMAP_CONTAINER_HEIGHT}px;
  639. border-top-left-radius: ${p => p.theme.borderRadius};
  640. border-top-right-radius: ${p => p.theme.borderRadius};
  641. `;
  642. const MinimapBackground = styled('div')`
  643. height: ${MINIMAP_HEIGHT}px;
  644. max-height: ${MINIMAP_HEIGHT}px;
  645. overflow: hidden;
  646. position: absolute;
  647. top: 0;
  648. `;
  649. const MinimapContainer = styled('div')`
  650. height: ${MINIMAP_HEIGHT}px;
  651. width: 100%;
  652. position: relative;
  653. left: 0;
  654. `;
  655. const ViewHandleContainer = styled('div')`
  656. position: absolute;
  657. top: 0;
  658. height: ${MINIMAP_HEIGHT}px;
  659. `;
  660. const ViewHandleLine = styled('div')`
  661. height: ${MINIMAP_HEIGHT - VIEW_HANDLE_HEIGHT}px;
  662. width: 2px;
  663. background-color: ${p => p.theme.textColor};
  664. `;
  665. const ViewHandle = styled('div')<{isDragging: boolean}>`
  666. position: absolute;
  667. background-color: ${p => p.theme.textColor};
  668. cursor: col-resize;
  669. width: 8px;
  670. height: ${VIEW_HANDLE_HEIGHT}px;
  671. bottom: 0;
  672. left: -3px;
  673. `;
  674. const Fog = styled('div')`
  675. background-color: ${p => p.theme.textColor};
  676. opacity: 0.1;
  677. position: absolute;
  678. top: 0;
  679. `;
  680. const MinimapSpanBar = styled('div')`
  681. position: relative;
  682. height: 2px;
  683. min-height: 2px;
  684. max-height: 2px;
  685. margin: 2px 0;
  686. min-width: 1px;
  687. border-radius: 1px;
  688. box-sizing: border-box;
  689. `;
  690. const BackgroundSlider = styled('div')`
  691. position: relative;
  692. `;
  693. const CursorGuide = styled('div')`
  694. position: absolute;
  695. top: 0;
  696. width: 1px;
  697. background-color: ${p => p.theme.red300};
  698. transform: translateX(-50%);
  699. `;
  700. const Handle = ({
  701. left,
  702. onMouseDown,
  703. isDragging,
  704. }: {
  705. left: number;
  706. onMouseDown: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
  707. isDragging: boolean;
  708. }) => (
  709. <ViewHandleContainer
  710. style={{
  711. left: toPercent(left),
  712. }}
  713. >
  714. <ViewHandleLine />
  715. <ViewHandle
  716. data-ignore="true"
  717. onMouseDown={onMouseDown}
  718. isDragging={isDragging}
  719. style={{
  720. height: `${VIEW_HANDLE_HEIGHT}px`,
  721. }}
  722. />
  723. </ViewHandleContainer>
  724. );
  725. const WindowSelection = styled('div')`
  726. position: absolute;
  727. top: 0;
  728. height: ${MINIMAP_HEIGHT}px;
  729. background-color: ${p => p.theme.textColor};
  730. opacity: 0.1;
  731. `;
  732. export const SecondaryHeader = styled('div')`
  733. position: absolute;
  734. top: ${MINIMAP_HEIGHT + TIME_AXIS_HEIGHT}px;
  735. left: 0;
  736. height: ${TIME_AXIS_HEIGHT}px;
  737. width: 100%;
  738. background-color: ${p => p.theme.backgroundSecondary};
  739. display: flex;
  740. border-top: 1px solid ${p => p.theme.border};
  741. overflow: hidden;
  742. `;
  743. const OperationsBreakdown = styled('div')`
  744. height: ${MINIMAP_HEIGHT + TIME_AXIS_HEIGHT}px;
  745. position: absolute;
  746. left: 0;
  747. top: 0;
  748. overflow: hidden;
  749. `;
  750. const RightSidePane = styled('div')`
  751. height: ${MINIMAP_HEIGHT + TIME_AXIS_HEIGHT}px;
  752. position: absolute;
  753. top: 0;
  754. `;
  755. export default TraceViewHeader;