traceDrawer.tsx 27 KB

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