styles.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import {Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as qs from 'query-string';
  4. import {Button as CommonButton, LinkButton} from 'sentry/components/button';
  5. import {DataSection} from 'sentry/components/events/styles';
  6. import {Tooltip} from 'sentry/components/tooltip';
  7. import {t, tct} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {getDuration} from 'sentry/utils/formatters';
  10. import type {ColorOrAlias} from 'sentry/utils/theme';
  11. const DetailContainer = styled('div')`
  12. display: flex;
  13. flex-direction: column;
  14. gap: ${space(2)};
  15. padding: ${space(1)};
  16. ${DataSection} {
  17. padding: 0;
  18. }
  19. `;
  20. const FlexBox = styled('div')`
  21. display: flex;
  22. align-items: center;
  23. `;
  24. const Actions = styled(FlexBox)`
  25. gap: ${space(0.5)};
  26. flex-wrap: wrap;
  27. justify-content: end;
  28. `;
  29. const Title = styled(FlexBox)`
  30. gap: ${space(1)};
  31. flex: none;
  32. width: 50%;
  33. `;
  34. const TitleText = styled('div')`
  35. ${p => p.theme.overflowEllipsis}
  36. `;
  37. const Type = styled('div')`
  38. font-size: ${p => p.theme.fontSizeSmall};
  39. `;
  40. const TitleOp = styled('div')`
  41. font-size: 15px;
  42. font-weight: bold;
  43. ${p => p.theme.overflowEllipsis}
  44. `;
  45. const Table = styled('table')`
  46. margin-bottom: 0 !important;
  47. td {
  48. overflow: hidden;
  49. }
  50. `;
  51. const IconTitleWrapper = styled(FlexBox)`
  52. gap: ${space(1)};
  53. `;
  54. const IconBorder = styled('div')<{backgroundColor: string; errored?: boolean}>`
  55. background-color: ${p => p.backgroundColor};
  56. border-radius: ${p => p.theme.borderRadius};
  57. padding: 0;
  58. display: flex;
  59. align-items: center;
  60. justify-content: center;
  61. width: 30px;
  62. height: 30px;
  63. svg {
  64. fill: ${p => p.theme.white};
  65. width: 14px;
  66. height: 14px;
  67. }
  68. `;
  69. const Button = styled(CommonButton)`
  70. position: absolute;
  71. top: ${space(0.75)};
  72. right: ${space(0.5)};
  73. `;
  74. const HeaderContainer = styled(Title)`
  75. justify-content: space-between;
  76. overflow: hidden;
  77. width: 100%;
  78. `;
  79. function EventDetailsLink(props: {eventId: string; projectSlug?: string}) {
  80. const query = useMemo(() => {
  81. return {...qs.parse(location.search), legacy: 1};
  82. }, []);
  83. return (
  84. <LinkButton
  85. disabled={!props.eventId || !props.projectSlug}
  86. title={
  87. !props.eventId || !props.projectSlug
  88. ? t('Event ID or Project Slug missing')
  89. : undefined
  90. }
  91. size="xs"
  92. to={{
  93. pathname: `/performance/${props.projectSlug}:${props.eventId}/`,
  94. query: query,
  95. }}
  96. >
  97. {t('View Event Details')}
  98. </LinkButton>
  99. );
  100. }
  101. const DURATION_COMPARISON_STATUS_COLORS: {
  102. equal: {light: ColorOrAlias; normal: ColorOrAlias};
  103. faster: {light: ColorOrAlias; normal: ColorOrAlias};
  104. slower: {light: ColorOrAlias; normal: ColorOrAlias};
  105. } = {
  106. faster: {
  107. light: 'green100',
  108. normal: 'green300',
  109. },
  110. slower: {
  111. light: 'red100',
  112. normal: 'red300',
  113. },
  114. equal: {
  115. light: 'gray100',
  116. normal: 'gray300',
  117. },
  118. };
  119. const MIN_PCT_DURATION_DIFFERENCE = 10;
  120. type DurationProps = {
  121. baseline: number | undefined;
  122. duration: number;
  123. baseDescription?: string;
  124. ratio?: number;
  125. };
  126. function Duration(props: DurationProps) {
  127. if (typeof props.duration !== 'number' || Number.isNaN(props.duration)) {
  128. return <DurationContainer>{t('unknown')}</DurationContainer>;
  129. }
  130. if (props.baseline === undefined || props.baseline === 0) {
  131. return <DurationContainer>{getDuration(props.duration, 2, true)}</DurationContainer>;
  132. }
  133. const delta = props.duration - props.baseline;
  134. const deltaPct = Math.round(Math.abs((delta / props.baseline) * 100));
  135. const status = delta > 0 ? 'slower' : delta < 0 ? 'faster' : 'equal';
  136. const formattedBaseDuration = (
  137. <Tooltip
  138. title={props.baseDescription}
  139. showUnderline
  140. underlineColor={DURATION_COMPARISON_STATUS_COLORS[status].normal}
  141. >
  142. {getDuration(props.baseline, 2, true)}
  143. </Tooltip>
  144. );
  145. const deltaText =
  146. status === 'equal'
  147. ? tct(`equal to the avg of [formattedBaseDuration]`, {
  148. formattedBaseDuration,
  149. })
  150. : status === 'faster'
  151. ? tct(`[deltaPct] faster than the avg of [formattedBaseDuration]`, {
  152. formattedBaseDuration,
  153. deltaPct: `${deltaPct}%`,
  154. })
  155. : tct(`[deltaPct] slower than the avg of [formattedBaseDuration]`, {
  156. formattedBaseDuration,
  157. deltaPct: `${deltaPct}%`,
  158. });
  159. return (
  160. <Fragment>
  161. <DurationContainer>
  162. {getDuration(props.duration, 2, true)}{' '}
  163. {props.ratio ? `(${(props.ratio * 100).toFixed()}%)` : null}
  164. </DurationContainer>
  165. {deltaPct >= MIN_PCT_DURATION_DIFFERENCE ? (
  166. <Comparison status={status}>{deltaText}</Comparison>
  167. ) : null}
  168. </Fragment>
  169. );
  170. }
  171. const DurationContainer = styled('span')`
  172. font-weight: bold;
  173. margin-right: ${space(1)};
  174. `;
  175. const Comparison = styled('span')<{status: 'faster' | 'slower' | 'equal'}>`
  176. color: ${p => p.theme[DURATION_COMPARISON_STATUS_COLORS[p.status].normal]};
  177. `;
  178. const TraceDrawerComponents = {
  179. DetailContainer,
  180. FlexBox,
  181. Title,
  182. Type,
  183. TitleOp,
  184. HeaderContainer,
  185. Actions,
  186. Table,
  187. IconTitleWrapper,
  188. IconBorder,
  189. EventDetailsLink,
  190. Button,
  191. TitleText,
  192. Duration,
  193. };
  194. export {TraceDrawerComponents};