styles.tsx 18 KB

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