styles.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757
  1. import {
  2. Children,
  3. Fragment,
  4. type PropsWithChildren,
  5. useMemo,
  6. useRef,
  7. useState,
  8. } from 'react';
  9. import styled from '@emotion/styled';
  10. import type {LocationDescriptor} from 'history';
  11. import startCase from 'lodash/startCase';
  12. import * as qs from 'query-string';
  13. import {Button} from 'sentry/components/button';
  14. import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
  15. import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
  16. import {useIssueDetailsColumnCount} from 'sentry/components/events/eventTags/util';
  17. import NewTagsUI from 'sentry/components/events/eventTagsAndScreenshot/tags';
  18. import {DataSection} from 'sentry/components/events/styles';
  19. import FileSize from 'sentry/components/fileSize';
  20. import {LazyRender, type LazyRenderProps} from 'sentry/components/lazyRender';
  21. import Link from 'sentry/components/links/link';
  22. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  23. import Panel from 'sentry/components/panels/panel';
  24. import QuestionTooltip from 'sentry/components/questionTooltip';
  25. import {StructuredData} from 'sentry/components/structuredEventData';
  26. import {Tooltip} from 'sentry/components/tooltip';
  27. import {IconChevron, IconOpen} from 'sentry/icons';
  28. import {t, tct} from 'sentry/locale';
  29. import {space} from 'sentry/styles/space';
  30. import type {KeyValueListDataItem} from 'sentry/types';
  31. import type {Event} from 'sentry/types/event';
  32. import type {Organization} from 'sentry/types/organization';
  33. import {defined, formatBytesBase10} from 'sentry/utils';
  34. import getDuration from 'sentry/utils/duration/getDuration';
  35. import {decodeScalar} from 'sentry/utils/queryString';
  36. import type {ColorOrAlias} from 'sentry/utils/theme';
  37. import useOrganization from 'sentry/utils/useOrganization';
  38. import {useParams} from 'sentry/utils/useParams';
  39. import {
  40. isAutogroupedNode,
  41. isMissingInstrumentationNode,
  42. isNoDataNode,
  43. isRootNode,
  44. isSpanNode,
  45. isTraceErrorNode,
  46. isTransactionNode,
  47. } from 'sentry/views/performance/newTraceDetails/guards';
  48. import {traceAnalytics} from 'sentry/views/performance/newTraceDetails/traceAnalytics';
  49. import type {
  50. MissingInstrumentationNode,
  51. NoDataNode,
  52. ParentAutogroupNode,
  53. SiblingAutogroupNode,
  54. TraceTree,
  55. TraceTreeNode,
  56. } from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
  57. const DetailContainer = styled('div')`
  58. display: flex;
  59. flex-direction: column;
  60. gap: ${space(2)};
  61. padding: ${space(1)};
  62. ${DataSection} {
  63. padding: 0;
  64. }
  65. `;
  66. const FlexBox = styled('div')`
  67. display: flex;
  68. align-items: center;
  69. `;
  70. const Actions = styled(FlexBox)`
  71. gap: ${space(0.5)};
  72. flex-wrap: wrap;
  73. justify-content: end;
  74. width: 100%;
  75. `;
  76. const Title = styled(FlexBox)`
  77. gap: ${space(1)};
  78. width: 50%;
  79. > span {
  80. min-width: 30px;
  81. }
  82. `;
  83. const TitleText = styled('div')`
  84. ${p => p.theme.overflowEllipsis}
  85. `;
  86. function TitleWithTestId(props: PropsWithChildren<{}>) {
  87. return <Title data-test-id="trace-drawer-title">{props.children}</Title>;
  88. }
  89. const Type = styled('div')`
  90. font-size: ${p => p.theme.fontSizeSmall};
  91. `;
  92. const TitleOp = styled('div')`
  93. font-size: 15px;
  94. font-weight: bold;
  95. ${p => p.theme.overflowEllipsis}
  96. `;
  97. const Table = styled('table')`
  98. margin-bottom: 0 !important;
  99. td {
  100. overflow: hidden;
  101. }
  102. `;
  103. const IconTitleWrapper = styled(FlexBox)`
  104. gap: ${space(1)};
  105. min-width: 30px;
  106. `;
  107. const IconBorder = styled('div')<{backgroundColor: string; errored?: boolean}>`
  108. background-color: ${p => p.backgroundColor};
  109. border-radius: ${p => p.theme.borderRadius};
  110. padding: 0;
  111. display: flex;
  112. align-items: center;
  113. justify-content: center;
  114. width: 30px;
  115. height: 30px;
  116. min-width: 30px;
  117. svg {
  118. fill: ${p => p.theme.white};
  119. width: 14px;
  120. height: 14px;
  121. }
  122. `;
  123. const HeaderContainer = styled(Title)`
  124. justify-content: space-between;
  125. width: 100%;
  126. z-index: 10;
  127. flex: 1 1 auto;
  128. `;
  129. const DURATION_COMPARISON_STATUS_COLORS: {
  130. equal: {light: ColorOrAlias; normal: ColorOrAlias};
  131. faster: {light: ColorOrAlias; normal: ColorOrAlias};
  132. slower: {light: ColorOrAlias; normal: ColorOrAlias};
  133. } = {
  134. faster: {
  135. light: 'green100',
  136. normal: 'green300',
  137. },
  138. slower: {
  139. light: 'red100',
  140. normal: 'red300',
  141. },
  142. equal: {
  143. light: 'gray100',
  144. normal: 'gray300',
  145. },
  146. };
  147. const MIN_PCT_DURATION_DIFFERENCE = 10;
  148. type DurationProps = {
  149. baseline: number | undefined;
  150. duration: number;
  151. baseDescription?: string;
  152. ratio?: number;
  153. };
  154. function Duration(props: DurationProps) {
  155. if (typeof props.duration !== 'number' || Number.isNaN(props.duration)) {
  156. return <DurationContainer>{t('unknown')}</DurationContainer>;
  157. }
  158. if (props.baseline === undefined || props.baseline === 0) {
  159. return <DurationContainer>{getDuration(props.duration, 2, true)}</DurationContainer>;
  160. }
  161. const delta = props.duration - props.baseline;
  162. const deltaPct = Math.round(Math.abs((delta / props.baseline) * 100));
  163. const status = delta > 0 ? 'slower' : delta < 0 ? 'faster' : 'equal';
  164. const formattedBaseDuration = (
  165. <Tooltip
  166. title={props.baseDescription}
  167. showUnderline
  168. underlineColor={DURATION_COMPARISON_STATUS_COLORS[status].normal}
  169. >
  170. {getDuration(props.baseline, 2, true)}
  171. </Tooltip>
  172. );
  173. const deltaText =
  174. status === 'equal'
  175. ? tct(`equal to the avg of [formattedBaseDuration]`, {
  176. formattedBaseDuration,
  177. })
  178. : status === 'faster'
  179. ? tct(`[deltaPct] faster than the avg of [formattedBaseDuration]`, {
  180. formattedBaseDuration,
  181. deltaPct: `${deltaPct}%`,
  182. })
  183. : tct(`[deltaPct] slower than the avg of [formattedBaseDuration]`, {
  184. formattedBaseDuration,
  185. deltaPct: `${deltaPct}%`,
  186. });
  187. return (
  188. <Fragment>
  189. <DurationContainer>
  190. {getDuration(props.duration, 2, true)}{' '}
  191. {props.ratio ? `(${(props.ratio * 100).toFixed()}%)` : null}
  192. </DurationContainer>
  193. {deltaPct >= MIN_PCT_DURATION_DIFFERENCE ? (
  194. <Comparison status={status}>{deltaText}</Comparison>
  195. ) : null}
  196. </Fragment>
  197. );
  198. }
  199. function TableRow({
  200. title,
  201. keep,
  202. children,
  203. prefix,
  204. extra = null,
  205. toolTipText,
  206. }: {
  207. children: React.ReactNode;
  208. title: JSX.Element | string | null;
  209. extra?: React.ReactNode;
  210. keep?: boolean;
  211. prefix?: JSX.Element;
  212. toolTipText?: string;
  213. }) {
  214. if (!keep && !children) {
  215. return null;
  216. }
  217. return (
  218. <tr>
  219. <td className="key">
  220. <Flex>
  221. {prefix}
  222. {title}
  223. {toolTipText ? <StyledQuestionTooltip size="xs" title={toolTipText} /> : null}
  224. </Flex>
  225. </td>
  226. <ValueTd className="value">
  227. <TableValueRow>
  228. <StyledPre>
  229. <span className="val-string">{children}</span>
  230. </StyledPre>
  231. <TableRowButtonContainer>{extra}</TableRowButtonContainer>
  232. </TableValueRow>
  233. </ValueTd>
  234. </tr>
  235. );
  236. }
  237. function getSearchParamFromNode(node: TraceTreeNode<TraceTree.NodeValue>) {
  238. if (isTransactionNode(node) || isTraceErrorNode(node)) {
  239. return `id:${node.value.event_id}`;
  240. }
  241. // Issues associated to a span or autogrouped node are not queryable, so we query by
  242. // the parent transaction's id
  243. const parentTransaction = node.parent_transaction;
  244. if ((isSpanNode(node) || isAutogroupedNode(node)) && parentTransaction) {
  245. return `id:${parentTransaction.value.event_id}`;
  246. }
  247. if (isMissingInstrumentationNode(node)) {
  248. throw new Error('Missing instrumentation nodes do not have associated issues');
  249. }
  250. return '';
  251. }
  252. function IssuesLink({
  253. node,
  254. children,
  255. }: {
  256. children: React.ReactNode;
  257. node?: TraceTreeNode<TraceTree.NodeValue>;
  258. }) {
  259. const organization = useOrganization();
  260. const params = useParams<{traceSlug?: string}>();
  261. const traceSlug = params.traceSlug?.trim() ?? '';
  262. const dateSelection = useMemo(() => {
  263. const normalizedParams = normalizeDateTimeParams(qs.parse(window.location.search), {
  264. allowAbsolutePageDatetime: true,
  265. });
  266. const start = decodeScalar(normalizedParams.start);
  267. const end = decodeScalar(normalizedParams.end);
  268. const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
  269. return {start, end, statsPeriod};
  270. }, []);
  271. return (
  272. <Link
  273. to={{
  274. pathname: `/organizations/${organization.slug}/issues/`,
  275. query: {
  276. query: `trace:${traceSlug} ${node ? getSearchParamFromNode(node) : ''}`,
  277. start: dateSelection.start,
  278. end: dateSelection.end,
  279. statsPeriod: dateSelection.statsPeriod,
  280. // If we don't pass the project param, the issues page will filter by the last selected project.
  281. // Traces can have multiple projects, so we query issues by all projects and rely on our search query to filter the results.
  282. project: -1,
  283. },
  284. }}
  285. >
  286. {children}
  287. </Link>
  288. );
  289. }
  290. const LAZY_RENDER_PROPS: Partial<LazyRenderProps> = {
  291. observerOptions: {rootMargin: '50px'},
  292. };
  293. const DurationContainer = styled('span')`
  294. font-weight: bold;
  295. margin-right: ${space(1)};
  296. `;
  297. const Comparison = styled('span')<{status: 'faster' | 'slower' | 'equal'}>`
  298. color: ${p => p.theme[DURATION_COMPARISON_STATUS_COLORS[p.status].normal]};
  299. `;
  300. const Flex = styled('div')`
  301. display: flex;
  302. align-items: center;
  303. `;
  304. const TableValueRow = styled('div')`
  305. display: grid;
  306. grid-template-columns: auto min-content;
  307. gap: ${space(1)};
  308. border-radius: 4px;
  309. background-color: ${p => p.theme.surface200};
  310. margin: 2px;
  311. `;
  312. const StyledQuestionTooltip = styled(QuestionTooltip)`
  313. margin-left: ${space(0.5)};
  314. `;
  315. const StyledPre = styled('pre')`
  316. margin: 0 !important;
  317. background-color: transparent !important;
  318. `;
  319. const TableRowButtonContainer = styled('div')`
  320. padding: 8px 10px;
  321. `;
  322. const ValueTd = styled('td')`
  323. position: relative;
  324. `;
  325. function NodeActions(props: {
  326. node: TraceTreeNode<any>;
  327. onTabScrollToNode: (
  328. node:
  329. | TraceTreeNode<any>
  330. | ParentAutogroupNode
  331. | SiblingAutogroupNode
  332. | NoDataNode
  333. | MissingInstrumentationNode
  334. ) => void;
  335. organization: Organization;
  336. eventSize?: number | undefined;
  337. }) {
  338. const items = useMemo(() => {
  339. const showInView: MenuItemProps = {
  340. key: 'show-in-view',
  341. label: t('Show in View'),
  342. onAction: () => {
  343. traceAnalytics.trackShowInView(props.organization);
  344. props.onTabScrollToNode(props.node);
  345. },
  346. };
  347. const eventId =
  348. props.node.metadata.event_id ?? props.node.parent_transaction?.metadata.event_id;
  349. const projectSlug =
  350. props.node.metadata.project_slug ??
  351. props.node.parent_transaction?.metadata.project_slug;
  352. const eventSize = props.eventSize;
  353. const jsonDetails: MenuItemProps = {
  354. key: 'json-details',
  355. onAction: () => {
  356. traceAnalytics.trackViewEventJSON(props.organization);
  357. window.open(
  358. `/api/0/projects/${props.organization.slug}/${projectSlug}/events/${eventId}/json/`,
  359. '_blank'
  360. );
  361. },
  362. label:
  363. t('JSON') +
  364. (typeof eventSize === 'number' ? ` (${formatBytesBase10(eventSize, 0)})` : ''),
  365. };
  366. if (isTransactionNode(props.node)) {
  367. return [showInView, jsonDetails];
  368. }
  369. if (isSpanNode(props.node)) {
  370. return [showInView];
  371. }
  372. if (isMissingInstrumentationNode(props.node)) {
  373. return [showInView];
  374. }
  375. if (isTraceErrorNode(props.node)) {
  376. return [showInView];
  377. }
  378. if (isRootNode(props.node)) {
  379. return [showInView];
  380. }
  381. if (isAutogroupedNode(props.node)) {
  382. return [showInView];
  383. }
  384. if (isNoDataNode(props.node)) {
  385. return [showInView];
  386. }
  387. return [showInView];
  388. }, [props]);
  389. return (
  390. <ActionsContainer>
  391. <Actions className="Actions">
  392. <Button
  393. size="xs"
  394. onClick={_e => {
  395. traceAnalytics.trackShowInView(props.organization);
  396. props.onTabScrollToNode(props.node);
  397. }}
  398. >
  399. {t('Show in view')}
  400. </Button>
  401. {isTransactionNode(props.node) ? (
  402. <Button
  403. size="xs"
  404. icon={<IconOpen />}
  405. onClick={() => traceAnalytics.trackViewEventJSON(props.organization)}
  406. href={`/api/0/projects/${props.organization.slug}/${props.node.value.project_slug}/events/${props.node.value.event_id}/json/`}
  407. external
  408. >
  409. {t('JSON')} (<FileSize bytes={props.eventSize ?? 0} />)
  410. </Button>
  411. ) : null}
  412. </Actions>
  413. <DropdownMenu
  414. items={items}
  415. className="DropdownMenu"
  416. position="bottom-end"
  417. trigger={triggerProps => (
  418. <ActionsButtonTrigger size="xs" {...triggerProps}>
  419. {t('Actions')}
  420. <IconChevron direction="down" size="xs" />
  421. </ActionsButtonTrigger>
  422. )}
  423. />
  424. </ActionsContainer>
  425. );
  426. }
  427. const ActionsButtonTrigger = styled(Button)`
  428. svg {
  429. margin-left: ${space(0.5)};
  430. width: 10px;
  431. height: 10px;
  432. }
  433. `;
  434. const ActionsContainer = styled('div')`
  435. display: flex;
  436. justify-content: end;
  437. align-items: center;
  438. gap: ${space(1)};
  439. container-type: inline-size;
  440. min-width: 24px;
  441. width: 100%;
  442. @container (max-width: 380px) {
  443. .DropdownMenu {
  444. display: block;
  445. }
  446. .Actions {
  447. display: none;
  448. }
  449. }
  450. @container (min-width: 381px) {
  451. .DropdownMenu {
  452. display: none;
  453. }
  454. }
  455. `;
  456. function EventTags({projectSlug, event}: {event: Event; projectSlug: string}) {
  457. return (
  458. <LazyRender {...TraceDrawerComponents.LAZY_RENDER_PROPS} containerHeight={200}>
  459. <TagsWrapper>
  460. <NewTagsUI event={event} projectSlug={projectSlug} />
  461. </TagsWrapper>
  462. </LazyRender>
  463. );
  464. }
  465. const TagsWrapper = styled('div')`
  466. h3 {
  467. color: ${p => p.theme.textColor};
  468. }
  469. `;
  470. interface SectionCardContentConfig {
  471. disableErrors?: boolean;
  472. includeAliasInSubject?: boolean;
  473. }
  474. type SectionCardKeyValue = Omit<KeyValueListDataItem, 'subject'> & {
  475. subject: React.ReactNode;
  476. };
  477. export type SectionCardKeyValueList = SectionCardKeyValue[];
  478. interface SectionCardContentProps {
  479. item: SectionCardKeyValue;
  480. meta: Record<string, any>;
  481. alias?: string;
  482. config?: SectionCardContentConfig;
  483. }
  484. function SectionCardContent({
  485. item,
  486. alias,
  487. meta,
  488. config,
  489. ...props
  490. }: SectionCardContentProps) {
  491. const {key, subject, value, action = {}} = item;
  492. if (key === 'type') {
  493. return null;
  494. }
  495. const dataComponent = (
  496. <StructuredData
  497. value={value}
  498. depth={0}
  499. maxDefaultDepth={0}
  500. meta={meta?.[key]}
  501. withAnnotatedText
  502. withOnlyFormattedText
  503. />
  504. );
  505. const contextSubject = subject
  506. ? config?.includeAliasInSubject && alias
  507. ? `${startCase(alias)}: ${subject}`
  508. : subject
  509. : null;
  510. return (
  511. <ContentContainer {...props}>
  512. {contextSubject ? <CardContentSubject>{contextSubject}</CardContentSubject> : null}
  513. <CardContentValueWrapper hasSubject={!!contextSubject} className="ctx-row-value">
  514. {defined(action?.link) ? (
  515. <Link to={action.link}>{dataComponent}</Link>
  516. ) : (
  517. dataComponent
  518. )}
  519. </CardContentValueWrapper>
  520. </ContentContainer>
  521. );
  522. }
  523. function SectionCard({
  524. items,
  525. title,
  526. disableTruncate,
  527. }: {
  528. items: SectionCardKeyValueList;
  529. title: React.ReactNode;
  530. disableTruncate?: boolean;
  531. }) {
  532. const [showingAll, setShowingAll] = useState(false);
  533. const renderText = showingAll ? t('Show less') : t('Show more') + '...';
  534. if (items.length === 0) {
  535. return null;
  536. }
  537. return (
  538. <Card>
  539. <CardContentTitle>{title}</CardContentTitle>
  540. {items.slice(0, showingAll || disableTruncate ? items.length : 5).map(item => (
  541. <SectionCardContent key={`context-card-${item.key}`} meta={{}} item={item} />
  542. ))}
  543. {items.length > 5 && !disableTruncate ? (
  544. <TruncateActionWrapper>
  545. <a onClick={() => setShowingAll(prev => !prev)}>{renderText}</a>
  546. </TruncateActionWrapper>
  547. ) : null}
  548. </Card>
  549. );
  550. }
  551. function SectionCardGroup({children}: {children: React.ReactNode}) {
  552. const containerRef = useRef<HTMLDivElement>(null);
  553. const columnCount = useIssueDetailsColumnCount(containerRef);
  554. const columns: React.ReactNode[] = [];
  555. const cards = Children.toArray(children);
  556. // Evenly distributing the cards into columns.
  557. const columnSize = Math.ceil(cards.length / columnCount);
  558. for (let i = 0; i < cards.length; i += columnSize) {
  559. columns.push(<CardsColumn key={i}>{cards.slice(i, i + columnSize)}</CardsColumn>);
  560. }
  561. return (
  562. <CardsWrapper columnCount={columnCount} ref={containerRef}>
  563. {columns}
  564. </CardsWrapper>
  565. );
  566. }
  567. const CardsWrapper = styled('div')<{columnCount: number}>`
  568. display: grid;
  569. align-items: start;
  570. grid-template-columns: repeat(${p => p.columnCount}, 1fr);
  571. gap: 10px;
  572. `;
  573. const CardsColumn = styled('div')`
  574. grid-column: span 1;
  575. `;
  576. function CardValueWithCopy({
  577. value,
  578. linkTarget,
  579. linkText,
  580. }: {
  581. value: string;
  582. linkTarget?: LocationDescriptor;
  583. linkText?: string;
  584. }) {
  585. return (
  586. <CardValueContainer>
  587. <CardValueText>
  588. {value}
  589. <StyledCopyToClipboardButton borderless size="zero" iconSize="xs" text={value} />
  590. </CardValueText>
  591. {linkTarget && linkTarget ? <Link to={linkTarget}>{linkText}</Link> : null}
  592. </CardValueContainer>
  593. );
  594. }
  595. const StyledCopyToClipboardButton = styled(CopyToClipboardButton)`
  596. transform: translateY(2px);
  597. `;
  598. const CardValueContainer = styled(FlexBox)`
  599. justify-content: space-between;
  600. gap: ${space(1)};
  601. flex-wrap: wrap;
  602. `;
  603. const CardValueText = styled('span')`
  604. overflow-wrap: anywhere;
  605. `;
  606. const Card = styled(Panel)`
  607. margin-bottom: 10px;
  608. padding: ${space(0.75)};
  609. font-size: ${p => p.theme.fontSizeSmall};
  610. `;
  611. const CardContentTitle = styled('p')`
  612. grid-column: 1 / -1;
  613. padding: ${space(0.25)} ${space(0.75)};
  614. margin: 0;
  615. color: ${p => p.theme.headingColor};
  616. font-weight: bold;
  617. `;
  618. const ContentContainer = styled('div')`
  619. display: grid;
  620. column-gap: ${space(1.5)};
  621. grid-template-columns: minmax(100px, 150px) 1fr 30px;
  622. padding: ${space(0.25)} ${space(0.75)};
  623. border-radius: 4px;
  624. color: ${p => p.theme.subText};
  625. border: 1px solid 'transparent';
  626. background-color: ${p => p.theme.background};
  627. &:nth-child(odd) {
  628. background-color: ${p => p.theme.backgroundSecondary};
  629. }
  630. `;
  631. export const CardContentSubject = styled('div')`
  632. grid-column: span 1;
  633. font-family: ${p => p.theme.text.familyMono};
  634. word-wrap: break-word;
  635. `;
  636. const CardContentValueWrapper = styled(CardContentSubject)<{hasSubject: boolean}>`
  637. color: ${p => p.theme.textColor};
  638. grid-column: ${p => (p.hasSubject ? 'span 2' : '1 / -1')};
  639. `;
  640. const TruncateActionWrapper = styled('div')`
  641. grid-column: 1 / -1;
  642. margin: ${space(0.5)} 0;
  643. display: flex;
  644. justify-content: center;
  645. `;
  646. const TraceDrawerComponents = {
  647. DetailContainer,
  648. FlexBox,
  649. Title: TitleWithTestId,
  650. Type,
  651. TitleOp,
  652. HeaderContainer,
  653. Actions,
  654. NodeActions,
  655. Table,
  656. IconTitleWrapper,
  657. IconBorder,
  658. TitleText,
  659. Duration,
  660. TableRow,
  661. LAZY_RENDER_PROPS,
  662. TableRowButtonContainer,
  663. TableValueRow,
  664. IssuesLink,
  665. SectionCard,
  666. CardValueWithCopy,
  667. EventTags,
  668. SectionCardGroup,
  669. };
  670. export {TraceDrawerComponents};