traceDrawer.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. import {useCallback, useLayoutEffect, useMemo, useRef, useState} from 'react';
  2. import {type Theme, useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import pick from 'lodash/pick';
  5. import type {Tag} from 'sentry/actionCreators/events';
  6. import {Button} from 'sentry/components/button';
  7. import {IconChevron, IconPanel, IconPin} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import type {EventTransaction} from 'sentry/types/event';
  11. import type EventView from 'sentry/utils/discover/eventView';
  12. import {PERFORMANCE_URL_PARAM} from 'sentry/utils/performance/constants';
  13. import type {
  14. TraceFullDetailed,
  15. TraceSplitResults,
  16. } from 'sentry/utils/performance/quickTrace/types';
  17. import {
  18. cancelAnimationTimeout,
  19. requestAnimationTimeout,
  20. } from 'sentry/utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils';
  21. import type {UseApiQueryResult} from 'sentry/utils/queryClient';
  22. import {useInfiniteApiQuery} from 'sentry/utils/queryClient';
  23. import type RequestError from 'sentry/utils/requestError/requestError';
  24. import {useLocation} from 'sentry/utils/useLocation';
  25. import useOrganization from 'sentry/utils/useOrganization';
  26. import {traceAnalytics} from 'sentry/views/performance/newTraceDetails/traceAnalytics';
  27. import {DrawerContainerRefContext} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/drawerContainerRefContext';
  28. import {TraceProfiles} from 'sentry/views/performance/newTraceDetails/traceDrawer/tabs/traceProfiles';
  29. import {TraceVitals} from 'sentry/views/performance/newTraceDetails/traceDrawer/tabs/traceVitals';
  30. import {
  31. usePassiveResizableDrawer,
  32. type UsePassiveResizableDrawerOptions,
  33. } from 'sentry/views/performance/newTraceDetails/traceDrawer/usePassiveResizeableDrawer';
  34. import type {TraceScheduler} from 'sentry/views/performance/newTraceDetails/traceRenderers/traceScheduler';
  35. import type {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager';
  36. import type {
  37. TraceReducerAction,
  38. TraceReducerState,
  39. } from 'sentry/views/performance/newTraceDetails/traceState';
  40. import {TRACE_DRAWER_DEFAULT_SIZES} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences';
  41. import {
  42. getTraceTabTitle,
  43. type TraceTabsReducerState,
  44. } from 'sentry/views/performance/newTraceDetails/traceState/traceTabs';
  45. import type {ReplayRecord} from 'sentry/views/replays/types';
  46. import {getTraceQueryParams} from '../traceApi/useTrace';
  47. import type {TraceMetaQueryResults} from '../traceApi/useTraceMeta';
  48. import {
  49. makeTraceNodeBarColor,
  50. type TraceTree,
  51. type TraceTreeNode,
  52. } from '../traceModels/traceTree';
  53. import {useTraceState, useTraceStateDispatch} from '../traceState/traceStateProvider';
  54. import type {TraceType} from '../traceType';
  55. import {TraceDetails} from './tabs/trace';
  56. import {TraceTreeNodeDetails} from './tabs/traceTreeNodeDetails';
  57. type TraceDrawerProps = {
  58. manager: VirtualizedViewManager;
  59. metaResults: TraceMetaQueryResults;
  60. onScrollToNode: (node: TraceTreeNode<TraceTree.NodeValue>) => void;
  61. onTabScrollToNode: (node: TraceTreeNode<TraceTree.NodeValue>) => void;
  62. replayRecord: ReplayRecord | null;
  63. rootEventResults: UseApiQueryResult<EventTransaction, RequestError>;
  64. scheduler: TraceScheduler;
  65. trace: TraceTree;
  66. traceEventView: EventView;
  67. traceGridRef: HTMLElement | null;
  68. traceType: TraceType;
  69. traces: TraceSplitResults<TraceFullDetailed> | null;
  70. };
  71. export function TraceDrawer(props: TraceDrawerProps) {
  72. const theme = useTheme();
  73. const location = useLocation();
  74. const organization = useOrganization();
  75. const traceState = useTraceState();
  76. const traceDispatch = useTraceStateDispatch();
  77. const contentContainerRef = useRef<HTMLDivElement>(null);
  78. // The /events-facets/ endpoint used to fetch tags for the trace tab is slow. Therefore,
  79. // we try to prefetch the tags as soon as the drawer loads, hoping that the tags will be loaded
  80. // by the time the user clicks on the trace tab. Also prevents the tags from being refetched.
  81. const urlParams = useMemo(() => {
  82. const {timestamp} = getTraceQueryParams(location.query, undefined);
  83. const params = pick(location.query, [
  84. ...Object.values(PERFORMANCE_URL_PARAM),
  85. 'cursor',
  86. ]);
  87. if (timestamp) {
  88. params.traceTimestamp = timestamp;
  89. }
  90. return params;
  91. // eslint-disable-next-line react-hooks/exhaustive-deps
  92. }, []);
  93. const tagsInfiniteQueryResults = useInfiniteApiQuery<Tag[]>({
  94. queryKey: [
  95. `/organizations/${organization.slug}/events-facets/`,
  96. {
  97. query: {
  98. ...urlParams,
  99. ...props.traceEventView.getFacetsAPIPayload(location),
  100. cursor: undefined,
  101. },
  102. },
  103. ],
  104. });
  105. const traceStateRef = useRef(traceState);
  106. traceStateRef.current = traceState;
  107. const initialSizeRef = useRef<Record<string, number> | null>(null);
  108. if (!initialSizeRef.current) {
  109. initialSizeRef.current = {};
  110. }
  111. const resizeEndRef = useRef<{id: number} | null>(null);
  112. const onResize = useCallback(
  113. (size: number, min: number, user?: boolean, minimized?: boolean) => {
  114. if (!props.traceGridRef) return;
  115. // When we resize the layout in x axis, we need to update the physical space
  116. // of the virtualized view manager to make sure a redrawing is correctly triggered.
  117. // If we dont do this, then the virtualized view manager will only trigger a redraw
  118. // whenver ResizeObserver detects a change. Since resize observers have "debounced"
  119. // callbacks, relying only on them to redraw the screen causes visual jank.
  120. if (
  121. (traceStateRef.current.preferences.layout === 'drawer left' ||
  122. traceStateRef.current.preferences.layout === 'drawer right') &&
  123. props.manager.container
  124. ) {
  125. const {width, height} = props.manager.container.getBoundingClientRect();
  126. props.scheduler.dispatch('set container physical space', [0, 0, width, height]);
  127. }
  128. minimized = minimized ?? traceStateRef.current.preferences.drawer.minimized;
  129. if (traceStateRef.current.preferences.layout === 'drawer bottom' && user) {
  130. if (size <= min && !minimized) {
  131. traceDispatch({
  132. type: 'minimize drawer',
  133. payload: true,
  134. });
  135. } else if (size > min && minimized) {
  136. traceDispatch({
  137. type: 'minimize drawer',
  138. payload: false,
  139. });
  140. }
  141. }
  142. const {width, height} = props.traceGridRef.getBoundingClientRect();
  143. const drawerWidth = size / width;
  144. const drawerHeight = size / height;
  145. if (resizeEndRef.current) cancelAnimationTimeout(resizeEndRef.current);
  146. resizeEndRef.current = requestAnimationTimeout(() => {
  147. if (traceStateRef.current.preferences.drawer.minimized) {
  148. return;
  149. }
  150. const drawer_size =
  151. traceStateRef.current.preferences.layout === 'drawer bottom'
  152. ? drawerHeight
  153. : drawerWidth;
  154. traceDispatch({
  155. type: 'set drawer dimension',
  156. payload: drawer_size,
  157. });
  158. }, 1000);
  159. if (traceStateRef.current.preferences.layout === 'drawer bottom') {
  160. min = minimized ? 27 : size;
  161. } else {
  162. min = minimized ? 0 : size;
  163. }
  164. if (traceStateRef.current.preferences.layout === 'drawer bottom') {
  165. props.traceGridRef.style.gridTemplateColumns = `1fr`;
  166. props.traceGridRef.style.gridTemplateRows = `1fr minmax(${min}px, ${drawerHeight * 100}%)`;
  167. } else if (traceStateRef.current.preferences.layout === 'drawer left') {
  168. props.traceGridRef.style.gridTemplateColumns = `minmax(${min}px, ${drawerWidth * 100}%) 1fr`;
  169. props.traceGridRef.style.gridTemplateRows = '1fr auto';
  170. } else {
  171. props.traceGridRef.style.gridTemplateColumns = `1fr minmax(${min}px, ${drawerWidth * 100}%)`;
  172. props.traceGridRef.style.gridTemplateRows = '1fr auto';
  173. }
  174. },
  175. [props.traceGridRef, props.manager, props.scheduler, traceDispatch]
  176. );
  177. const [drawerRef, setDrawerRef] = useState<HTMLDivElement | null>(null);
  178. const drawerOptions: Pick<UsePassiveResizableDrawerOptions, 'min' | 'initialSize'> =
  179. useMemo(() => {
  180. const initialSizeInPercentage =
  181. traceState.preferences.drawer.sizes[traceState.preferences.layout];
  182. // We have a stored user preference for the drawer size
  183. const {width, height} = props.traceGridRef?.getBoundingClientRect() ?? {
  184. width: 0,
  185. height: 0,
  186. };
  187. const initialSize = traceState.preferences.drawer.minimized
  188. ? 0
  189. : traceState.preferences.layout === 'drawer bottom'
  190. ? height * initialSizeInPercentage
  191. : width * initialSizeInPercentage;
  192. return {
  193. min: traceState.preferences.layout === 'drawer bottom' ? 27 : 350,
  194. initialSize,
  195. ref: drawerRef,
  196. };
  197. // eslint-disable-next-line react-hooks/exhaustive-deps
  198. }, [props.traceGridRef, traceState.preferences.layout, drawerRef]);
  199. const resizableDrawerOptions: UsePassiveResizableDrawerOptions = useMemo(() => {
  200. return {
  201. ...drawerOptions,
  202. onResize,
  203. direction:
  204. traceState.preferences.layout === 'drawer left'
  205. ? 'left'
  206. : traceState.preferences.layout === 'drawer right'
  207. ? 'right'
  208. : 'up',
  209. };
  210. }, [onResize, drawerOptions, traceState.preferences.layout]);
  211. const {onMouseDown, size} = usePassiveResizableDrawer(resizableDrawerOptions);
  212. const onParentClick = useCallback(
  213. (node: TraceTreeNode<TraceTree.NodeValue>) => {
  214. props.onTabScrollToNode(node);
  215. traceDispatch({
  216. type: 'activate tab',
  217. payload: node,
  218. pin_previous: true,
  219. });
  220. },
  221. [props, traceDispatch]
  222. );
  223. const onMinimizeClick = useCallback(() => {
  224. traceAnalytics.trackDrawerMinimize(organization);
  225. traceDispatch({
  226. type: 'minimize drawer',
  227. payload: !traceState.preferences.drawer.minimized,
  228. });
  229. if (!traceState.preferences.drawer.minimized) {
  230. onResize(0, 0, true, true);
  231. size.current = drawerOptions.min;
  232. } else {
  233. if (drawerOptions.initialSize === 0) {
  234. const userPreference =
  235. traceStateRef.current.preferences.drawer.sizes[
  236. traceStateRef.current.preferences.layout
  237. ];
  238. const {width, height} = props.traceGridRef?.getBoundingClientRect() ?? {
  239. width: 0,
  240. height: 0,
  241. };
  242. const containerSize =
  243. traceStateRef.current.preferences.layout === 'drawer bottom' ? height : width;
  244. const drawer_size = containerSize * userPreference;
  245. onResize(drawer_size, drawerOptions.min, true, false);
  246. size.current = userPreference;
  247. return;
  248. }
  249. onResize(drawerOptions.initialSize, drawerOptions.min, true, false);
  250. size.current = drawerOptions.initialSize;
  251. }
  252. }, [
  253. size,
  254. onResize,
  255. traceDispatch,
  256. props.traceGridRef,
  257. traceState.preferences.drawer.minimized,
  258. organization,
  259. drawerOptions,
  260. ]);
  261. const onDoubleClickResetToDefault = useCallback(() => {
  262. if (!traceStateRef.current.preferences.drawer.minimized) {
  263. onMinimizeClick();
  264. return;
  265. }
  266. traceDispatch({type: 'minimize drawer', payload: false});
  267. const initialSize = TRACE_DRAWER_DEFAULT_SIZES[traceState.preferences.layout];
  268. const {width, height} = props.traceGridRef?.getBoundingClientRect() ?? {
  269. width: 0,
  270. height: 0,
  271. };
  272. const containerSize =
  273. traceState.preferences.layout === 'drawer bottom' ? height : width;
  274. const drawer_size = containerSize * initialSize;
  275. onResize(drawer_size, drawerOptions.min, true, false);
  276. size.current = drawer_size;
  277. }, [
  278. size,
  279. onMinimizeClick,
  280. onResize,
  281. drawerOptions.min,
  282. traceState.preferences.layout,
  283. props.traceGridRef,
  284. traceDispatch,
  285. ]);
  286. const initializedRef = useRef(false);
  287. useLayoutEffect(() => {
  288. if (initializedRef.current) return;
  289. if (traceState.preferences.drawer.minimized && props.traceGridRef) {
  290. if (traceStateRef.current.preferences.layout === 'drawer bottom') {
  291. props.traceGridRef.style.gridTemplateColumns = `1fr`;
  292. props.traceGridRef.style.gridTemplateRows = `1fr minmax(${27}px, 0%)`;
  293. size.current = 27;
  294. } else if (traceStateRef.current.preferences.layout === 'drawer left') {
  295. props.traceGridRef.style.gridTemplateColumns = `minmax(${0}px, 0%) 1fr`;
  296. props.traceGridRef.style.gridTemplateRows = '1fr auto';
  297. size.current = 0;
  298. } else {
  299. props.traceGridRef.style.gridTemplateColumns = `1fr minmax(${0}px, 0%)`;
  300. props.traceGridRef.style.gridTemplateRows = '1fr auto';
  301. size.current = 0;
  302. }
  303. initializedRef.current = true;
  304. }
  305. // eslint-disable-next-line react-hooks/exhaustive-deps
  306. }, [props.traceGridRef]);
  307. // Syncs the height of the tabs with the trace indicators
  308. const hasIndicators =
  309. props.trace.indicators.length > 0 &&
  310. traceState.preferences.layout !== 'drawer bottom';
  311. if (
  312. traceState.preferences.drawer.minimized &&
  313. traceState.preferences.layout !== 'drawer bottom'
  314. ) {
  315. return (
  316. <TabsHeightContainer
  317. absolute
  318. layout={traceState.preferences.layout}
  319. hasIndicators={hasIndicators}
  320. >
  321. <TabLayoutControlItem>
  322. <TraceLayoutMinimizeButton onClick={onMinimizeClick} trace_state={traceState} />
  323. </TabLayoutControlItem>
  324. </TabsHeightContainer>
  325. );
  326. }
  327. return (
  328. <PanelWrapper ref={setDrawerRef} layout={traceState.preferences.layout}>
  329. <ResizeableHandle
  330. layout={traceState.preferences.layout}
  331. onMouseDown={onMouseDown}
  332. onDoubleClick={onDoubleClickResetToDefault}
  333. />
  334. <TabsHeightContainer
  335. layout={traceState.preferences.layout}
  336. onDoubleClick={onDoubleClickResetToDefault}
  337. hasIndicators={hasIndicators}
  338. >
  339. <TabsLayout data-test-id="trace-drawer-tabs">
  340. <TabActions>
  341. <TabLayoutControlItem>
  342. <TraceLayoutMinimizeButton
  343. onClick={onMinimizeClick}
  344. trace_state={traceState}
  345. />
  346. <TabSeparator />
  347. </TabLayoutControlItem>
  348. </TabActions>
  349. <TabsContainer
  350. style={{
  351. gridTemplateColumns: `repeat(${traceState.tabs.tabs.length + (traceState.tabs.last_clicked_tab ? 1 : 0)}, minmax(0, min-content))`,
  352. }}
  353. >
  354. {/* Renders all open tabs */}
  355. {traceState.tabs.tabs.map((n, i) => {
  356. return (
  357. <TraceDrawerTab
  358. key={i}
  359. tab={n}
  360. index={i}
  361. theme={theme}
  362. trace={props.trace}
  363. trace_state={traceState}
  364. traceDispatch={traceDispatch}
  365. onTabScrollToNode={props.onTabScrollToNode}
  366. pinned
  367. />
  368. );
  369. })}
  370. {/* Renders the last tab the user clicked on - this one is ephemeral and might change */}
  371. {traceState.tabs.last_clicked_tab ? (
  372. <TraceDrawerTab
  373. pinned={false}
  374. key="last-clicked"
  375. tab={traceState.tabs.last_clicked_tab}
  376. index={traceState.tabs.tabs.length}
  377. theme={theme}
  378. trace_state={traceState}
  379. traceDispatch={traceDispatch}
  380. onTabScrollToNode={props.onTabScrollToNode}
  381. trace={props.trace}
  382. />
  383. ) : null}
  384. </TabsContainer>
  385. {traceState.preferences.drawer.layoutOptions.length > 0 ? (
  386. <TraceLayoutButtons traceDispatch={traceDispatch} trace_state={traceState} />
  387. ) : null}
  388. </TabsLayout>
  389. </TabsHeightContainer>
  390. {traceState.preferences.drawer.minimized ? null : (
  391. <DrawerContainerRefContext.Provider value={contentContainerRef}>
  392. <Content
  393. ref={contentContainerRef}
  394. layout={traceState.preferences.layout}
  395. data-test-id="trace-drawer"
  396. >
  397. <ContentWrapper>
  398. {traceState.tabs.current_tab ? (
  399. traceState.tabs.current_tab.node === 'trace' ? (
  400. <TraceDetails
  401. metaResults={props.metaResults}
  402. traceType={props.traceType}
  403. tree={props.trace}
  404. node={props.trace.root.children[0]}
  405. rootEventResults={props.rootEventResults}
  406. traces={props.traces}
  407. tagsInfiniteQueryResults={tagsInfiniteQueryResults}
  408. traceEventView={props.traceEventView}
  409. />
  410. ) : traceState.tabs.current_tab.node === 'vitals' ? (
  411. <TraceVitals trace={props.trace} />
  412. ) : traceState.tabs.current_tab.node === 'profiles' ? (
  413. <TraceProfiles
  414. tree={props.trace}
  415. onScrollToNode={props.onScrollToNode}
  416. />
  417. ) : (
  418. <TraceTreeNodeDetails
  419. replayRecord={props.replayRecord}
  420. manager={props.manager}
  421. organization={organization}
  422. onParentClick={onParentClick}
  423. node={traceState.tabs.current_tab.node}
  424. onTabScrollToNode={props.onTabScrollToNode}
  425. />
  426. )
  427. ) : null}
  428. </ContentWrapper>
  429. </Content>
  430. </DrawerContainerRefContext.Provider>
  431. )}
  432. </PanelWrapper>
  433. );
  434. }
  435. interface TraceDrawerTabProps {
  436. index: number;
  437. onTabScrollToNode: (node: TraceTreeNode<TraceTree.NodeValue>) => void;
  438. pinned: boolean;
  439. tab: TraceTabsReducerState['tabs'][number];
  440. theme: Theme;
  441. trace: TraceTree;
  442. traceDispatch: React.Dispatch<TraceReducerAction>;
  443. trace_state: TraceReducerState;
  444. }
  445. function TraceDrawerTab(props: TraceDrawerTabProps) {
  446. const organization = useOrganization();
  447. const node = props.tab.node;
  448. if (typeof node === 'string') {
  449. const root = props.trace.root.children[0];
  450. return (
  451. <Tab
  452. data-test-id="trace-drawer-tab"
  453. className={typeof props.tab.node === 'string' ? 'Static' : ''}
  454. aria-selected={
  455. props.tab === props.trace_state.tabs.current_tab ? 'true' : 'false'
  456. }
  457. onClick={() => {
  458. if (props.tab.node !== 'vitals' && props.tab.node !== 'profiles') {
  459. traceAnalytics.trackTabView(node, organization);
  460. props.onTabScrollToNode(root);
  461. }
  462. props.traceDispatch({type: 'activate tab', payload: props.index});
  463. }}
  464. >
  465. {/* A trace is technically an entry in the list, so it has a color */}
  466. {props.tab.node === 'trace' ||
  467. props.tab.node === 'vitals' ||
  468. props.tab.node === 'profiles' ? null : (
  469. <TabButtonIndicator
  470. backgroundColor={makeTraceNodeBarColor(props.theme, root)}
  471. />
  472. )}
  473. <TabButton>{props.tab.label ?? node}</TabButton>
  474. </Tab>
  475. );
  476. }
  477. return (
  478. <Tab
  479. data-test-id="trace-drawer-tab"
  480. aria-selected={props.tab === props.trace_state.tabs.current_tab ? 'true' : 'false'}
  481. onClick={() => {
  482. traceAnalytics.trackTabView('event', organization);
  483. props.onTabScrollToNode(node);
  484. props.traceDispatch({type: 'activate tab', payload: props.index});
  485. }}
  486. >
  487. <TabButtonIndicator backgroundColor={makeTraceNodeBarColor(props.theme, node)} />
  488. <TabButton>{getTraceTabTitle(node)}</TabButton>
  489. <TabPinButton
  490. pinned={props.pinned}
  491. onClick={e => {
  492. e.stopPropagation();
  493. traceAnalytics.trackTabPin(organization);
  494. props.pinned
  495. ? props.traceDispatch({type: 'unpin tab', payload: props.index})
  496. : props.traceDispatch({type: 'pin tab'});
  497. }}
  498. />
  499. </Tab>
  500. );
  501. }
  502. function TraceLayoutButtons(props: {
  503. traceDispatch: React.Dispatch<TraceReducerAction>;
  504. trace_state: TraceReducerState;
  505. }) {
  506. const organization = useOrganization();
  507. return (
  508. <TabActions>
  509. {props.trace_state.preferences.drawer.layoutOptions.includes('drawer left') ? (
  510. <TabLayoutControlItem>
  511. <TabIconButton
  512. active={props.trace_state.preferences.layout === 'drawer left'}
  513. onClick={() => {
  514. traceAnalytics.trackLayoutChange('drawer left', organization);
  515. props.traceDispatch({type: 'set layout', payload: 'drawer left'});
  516. }}
  517. size="xs"
  518. aria-label={t('Drawer left')}
  519. icon={<IconPanel size="xs" direction="left" />}
  520. />
  521. </TabLayoutControlItem>
  522. ) : null}
  523. {props.trace_state.preferences.drawer.layoutOptions.includes('drawer bottom') ? (
  524. <TabLayoutControlItem>
  525. <TabIconButton
  526. active={props.trace_state.preferences.layout === 'drawer bottom'}
  527. onClick={() => {
  528. traceAnalytics.trackLayoutChange('drawer bottom', organization);
  529. props.traceDispatch({type: 'set layout', payload: 'drawer bottom'});
  530. }}
  531. size="xs"
  532. aria-label={t('Drawer bottom')}
  533. icon={<IconPanel size="xs" direction="down" />}
  534. />
  535. </TabLayoutControlItem>
  536. ) : null}
  537. {props.trace_state.preferences.drawer.layoutOptions.includes('drawer right') ? (
  538. <TabLayoutControlItem>
  539. <TabIconButton
  540. active={props.trace_state.preferences.layout === 'drawer right'}
  541. onClick={() => {
  542. traceAnalytics.trackLayoutChange('drawer right', organization);
  543. props.traceDispatch({type: 'set layout', payload: 'drawer right'});
  544. }}
  545. size="xs"
  546. aria-label={t('Drawer right')}
  547. icon={<IconPanel size="xs" direction="right" />}
  548. />
  549. </TabLayoutControlItem>
  550. ) : null}
  551. </TabActions>
  552. );
  553. }
  554. function TraceLayoutMinimizeButton(props: {
  555. onClick: () => void;
  556. trace_state: TraceReducerState;
  557. }) {
  558. return (
  559. <TabIconButton
  560. size="xs"
  561. active={props.trace_state.preferences.drawer.minimized}
  562. onClick={props.onClick}
  563. aria-label={t('Minimize')}
  564. icon={
  565. <SmallerChevronIcon
  566. size="sm"
  567. isCircled
  568. direction={
  569. props.trace_state.preferences.layout === 'drawer bottom'
  570. ? props.trace_state.preferences.drawer.minimized
  571. ? 'up'
  572. : 'down'
  573. : props.trace_state.preferences.layout === 'drawer left'
  574. ? props.trace_state.preferences.drawer.minimized
  575. ? 'right'
  576. : 'left'
  577. : props.trace_state.preferences.drawer.minimized
  578. ? 'left'
  579. : 'right'
  580. }
  581. />
  582. }
  583. />
  584. );
  585. }
  586. const ResizeableHandle = styled('div')<{
  587. layout: 'drawer bottom' | 'drawer left' | 'drawer right';
  588. }>`
  589. width: ${p => (p.layout === 'drawer bottom' ? '100%' : '12px')};
  590. height: ${p => (p.layout === 'drawer bottom' ? '12px' : '100%')};
  591. cursor: ${p => (p.layout === 'drawer bottom' ? 'ns-resize' : 'ew-resize')};
  592. position: absolute;
  593. top: ${p => (p.layout === 'drawer bottom' ? '-6px' : 0)};
  594. left: ${p =>
  595. p.layout === 'drawer bottom' ? 0 : p.layout === 'drawer right' ? '-6px' : 'initial'};
  596. right: ${p => (p.layout === 'drawer left' ? '-6px' : 0)};
  597. z-index: 1;
  598. `;
  599. const PanelWrapper = styled('div')<{
  600. layout: 'drawer bottom' | 'drawer left' | 'drawer right';
  601. }>`
  602. grid-area: drawer;
  603. display: flex;
  604. flex-direction: column;
  605. overflow: hidden;
  606. width: 100%;
  607. border-top: ${p =>
  608. p.layout === 'drawer bottom' ? `1px solid ${p.theme.border}` : 'none'};
  609. border-left: ${p =>
  610. p.layout === 'drawer right' ? `1px solid ${p.theme.border}` : 'none'};
  611. border-right: ${p =>
  612. p.layout === 'drawer left' ? `1px solid ${p.theme.border}` : 'none'};
  613. bottom: 0;
  614. right: 0;
  615. position: relative;
  616. background: ${p => p.theme.background};
  617. color: ${p => p.theme.textColor};
  618. text-align: left;
  619. z-index: 10;
  620. `;
  621. const SmallerChevronIcon = styled(IconChevron)`
  622. width: 13px;
  623. height: 13px;
  624. transition: none;
  625. `;
  626. const TabsHeightContainer = styled('div')<{
  627. hasIndicators: boolean;
  628. layout: 'drawer bottom' | 'drawer left' | 'drawer right';
  629. absolute?: boolean;
  630. }>`
  631. background: ${p => p.theme.backgroundSecondary};
  632. left: ${p => (p.layout === 'drawer left' ? '0' : 'initial')};
  633. right: ${p => (p.layout === 'drawer right' ? '0' : 'initial')};
  634. position: ${p => (p.absolute ? 'absolute' : 'relative')};
  635. height: ${p => (p.hasIndicators ? '44px' : '26px')};
  636. border-bottom: 1px solid ${p => p.theme.border};
  637. display: flex;
  638. flex-direction: column;
  639. justify-content: end;
  640. `;
  641. const TabsLayout = styled('div')`
  642. display: grid;
  643. grid-template-columns: auto 1fr auto;
  644. padding-left: ${space(0.25)};
  645. padding-right: ${space(0.5)};
  646. `;
  647. const TabsContainer = styled('ul')`
  648. display: grid;
  649. list-style-type: none;
  650. width: 100%;
  651. align-items: center;
  652. justify-content: left;
  653. gap: ${space(0.5)};
  654. padding-left: 0;
  655. margin-bottom: 0;
  656. `;
  657. const TabActions = styled('ul')`
  658. list-style-type: none;
  659. padding-left: 0;
  660. margin-bottom: 0;
  661. flex: none;
  662. button {
  663. padding: 0 ${space(0.5)};
  664. }
  665. `;
  666. const TabSeparator = styled('span')`
  667. display: inline-block;
  668. margin-left: ${space(0.5)};
  669. margin-right: ${space(0.5)};
  670. height: 16px;
  671. width: 1px;
  672. background-color: ${p => p.theme.border};
  673. position: absolute;
  674. top: 50%;
  675. right: 0;
  676. transform: translateY(-50%);
  677. `;
  678. const TabLayoutControlItem = styled('li')`
  679. display: inline-block;
  680. margin: 0;
  681. position: relative;
  682. z-index: 10;
  683. background-color: ${p => p.theme.backgroundSecondary};
  684. `;
  685. const Tab = styled('li')`
  686. height: 100%;
  687. border-top: 2px solid transparent;
  688. display: flex;
  689. align-items: center;
  690. border-bottom: 2px solid transparent;
  691. padding: 0 ${space(0.25)};
  692. position: relative;
  693. &.Static + li:not(.Static) {
  694. margin-left: 10px;
  695. &:after {
  696. display: block;
  697. content: '';
  698. position: absolute;
  699. left: -10px;
  700. top: 50%;
  701. transform: translateY(-50%);
  702. height: 16px;
  703. width: 1px;
  704. background-color: ${p => p.theme.border};
  705. }
  706. }
  707. &:hover {
  708. border-bottom: 2px solid ${p => p.theme.blue200};
  709. button:last-child {
  710. transition: all 0.3s ease-in-out 500ms;
  711. transform: scale(1);
  712. opacity: 1;
  713. }
  714. }
  715. &[aria-selected='true'] {
  716. border-bottom: 2px solid ${p => p.theme.blue400};
  717. }
  718. `;
  719. const TabButtonIndicator = styled('div')<{backgroundColor: string}>`
  720. width: 12px;
  721. height: 12px;
  722. min-width: 12px;
  723. border-radius: 2px;
  724. margin-right: ${space(0.25)};
  725. background-color: ${p => p.backgroundColor};
  726. `;
  727. const TabButton = styled('button')`
  728. height: 100%;
  729. border: none;
  730. max-width: 28ch;
  731. overflow: hidden;
  732. text-overflow: ellipsis;
  733. white-space: nowrap;
  734. border-radius: 0;
  735. margin: 0;
  736. padding: 0 ${space(0.25)};
  737. font-size: ${p => p.theme.fontSizeSmall};
  738. color: ${p => p.theme.textColor};
  739. background: transparent;
  740. `;
  741. const Content = styled('div')<{layout: 'drawer bottom' | 'drawer left' | 'drawer right'}>`
  742. position: relative;
  743. overflow: auto;
  744. padding: ${space(1)};
  745. flex: 1;
  746. td {
  747. max-width: 100% !important;
  748. }
  749. `;
  750. const TabIconButton = styled(Button)<{active: boolean}>`
  751. border: none;
  752. background-color: transparent;
  753. box-shadow: none;
  754. transition: none !important;
  755. opacity: ${p => (p.active ? 0.7 : 0.5)};
  756. height: 24px;
  757. max-height: 24px;
  758. &:not(:last-child) {
  759. margin-right: ${space(1)};
  760. }
  761. &:hover {
  762. border: none;
  763. background-color: transparent;
  764. box-shadow: none;
  765. opacity: ${p => (p.active ? 0.6 : 0.5)};
  766. }
  767. `;
  768. function TabPinButton(props: {
  769. pinned: boolean;
  770. onClick?: (e: React.MouseEvent<HTMLElement>) => void;
  771. }) {
  772. return (
  773. <PinButton
  774. size="zero"
  775. data-test-id="trace-drawer-tab-pin-button"
  776. onClick={props.onClick}
  777. >
  778. <StyledIconPin size="xs" isSolid={props.pinned} />
  779. </PinButton>
  780. );
  781. }
  782. const PinButton = styled(Button)`
  783. padding: ${space(0.5)};
  784. margin: 0;
  785. background-color: transparent;
  786. border: none;
  787. &:hover {
  788. background-color: transparent;
  789. }
  790. `;
  791. const StyledIconPin = styled(IconPin)`
  792. background-color: transparent;
  793. border: none;
  794. `;
  795. const ContentWrapper = styled('div')`
  796. inset: ${space(1)};
  797. position: absolute;
  798. `;