changeExplorer.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import moment from 'moment';
  5. import {Button} from 'sentry/components/button';
  6. import {getArbitraryRelativePeriod} from 'sentry/components/organizations/timeRangeSelector/utils';
  7. import {DEFAULT_RELATIVE_PERIODS} from 'sentry/constants';
  8. import {IconFire, IconOpen} from 'sentry/icons';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import {Organization, Project} from 'sentry/types';
  12. import {trackAnalytics} from 'sentry/utils/analytics';
  13. import theme from 'sentry/utils/theme';
  14. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  15. import {
  16. DisplayModes,
  17. transactionSummaryRouteWithQuery,
  18. } from 'sentry/views/performance/transactionSummary/utils';
  19. import {MetricsTable} from 'sentry/views/performance/trends/changeExplorerUtils/metricsTable';
  20. import {SpansList} from 'sentry/views/performance/trends/changeExplorerUtils/spansList';
  21. import {Chart} from 'sentry/views/performance/trends/chart';
  22. import {
  23. NormalizedTrendsTransaction,
  24. TrendChangeType,
  25. TrendParameter,
  26. TrendsStats,
  27. TrendView,
  28. } from 'sentry/views/performance/trends/types';
  29. import {getTrendProjectId} from 'sentry/views/performance/trends/utils';
  30. import DetailPanel from 'sentry/views/starfish/components/detailPanel';
  31. type PerformanceChangeExplorerProps = {
  32. collapsed: boolean;
  33. isLoading: boolean;
  34. location: Location;
  35. onClose: () => void;
  36. organization: Organization;
  37. projects: Project[];
  38. statsData: TrendsStats;
  39. transaction: NormalizedTrendsTransaction;
  40. trendChangeType: TrendChangeType;
  41. trendFunction: string;
  42. trendParameter: TrendParameter;
  43. trendView: TrendView;
  44. };
  45. type ExplorerBodyProps = {
  46. isLoading: boolean;
  47. location: Location;
  48. organization: Organization;
  49. projects: Project[];
  50. statsData: TrendsStats;
  51. transaction: NormalizedTrendsTransaction;
  52. trendChangeType: TrendChangeType;
  53. trendFunction: string;
  54. trendParameter: TrendParameter;
  55. trendView: TrendView;
  56. };
  57. type HeaderProps = {
  58. organization: Organization;
  59. projects: Project[];
  60. transaction: NormalizedTrendsTransaction;
  61. trendChangeType: TrendChangeType;
  62. trendFunction: string;
  63. trendParameter: TrendParameter;
  64. trendView: TrendView;
  65. };
  66. export function PerformanceChangeExplorer({
  67. collapsed,
  68. transaction,
  69. onClose,
  70. trendChangeType,
  71. trendFunction,
  72. trendView,
  73. statsData,
  74. isLoading,
  75. organization,
  76. projects,
  77. trendParameter,
  78. location,
  79. }: PerformanceChangeExplorerProps) {
  80. return (
  81. <DetailPanel detailKey={!collapsed ? transaction.transaction : ''} onClose={onClose}>
  82. {!collapsed && (
  83. <PanelBodyWrapper>
  84. <ExplorerBody
  85. transaction={transaction}
  86. trendChangeType={trendChangeType}
  87. trendFunction={trendFunction}
  88. trendView={trendView}
  89. statsData={statsData}
  90. isLoading={isLoading}
  91. organization={organization}
  92. projects={projects}
  93. trendParameter={trendParameter}
  94. location={location}
  95. />
  96. </PanelBodyWrapper>
  97. )}
  98. </DetailPanel>
  99. );
  100. }
  101. function ExplorerBody(props: ExplorerBodyProps) {
  102. const {
  103. transaction,
  104. trendChangeType,
  105. trendFunction,
  106. trendView,
  107. trendParameter,
  108. isLoading,
  109. location,
  110. organization,
  111. projects,
  112. } = props;
  113. const breakpointDate = transaction.breakpoint
  114. ? moment(transaction.breakpoint * 1000).format('ddd, DD MMM YYYY HH:mm:ss z')
  115. : '';
  116. const start = moment(trendView.start).format('DD MMM YYYY HH:mm:ss z');
  117. const end = moment(trendView.end).format('DD MMM YYYY HH:mm:ss z');
  118. return (
  119. <Fragment>
  120. <Header
  121. transaction={transaction}
  122. trendChangeType={trendChangeType}
  123. trendView={trendView}
  124. projects={projects}
  125. organization={organization}
  126. trendFunction={trendFunction}
  127. trendParameter={trendParameter}
  128. />
  129. <div style={{display: 'flex', gap: space(4)}}>
  130. <InfoItem
  131. label={
  132. trendChangeType === TrendChangeType.REGRESSION
  133. ? t('Regression Metric')
  134. : t('Improvement Metric')
  135. }
  136. value={trendFunction}
  137. />
  138. <InfoItem label={t('Start Time')} value={breakpointDate} />
  139. </div>
  140. <GraphPanel data-test-id="pce-graph">
  141. <strong>{`${trendParameter.label} (${trendFunction})`}</strong>
  142. <ExplorerText color={theme.gray300} margin={`-${space(3)}`}>
  143. {trendView.statsPeriod
  144. ? DEFAULT_RELATIVE_PERIODS[trendView.statsPeriod] ||
  145. getArbitraryRelativePeriod(trendView.statsPeriod)[trendView.statsPeriod]
  146. : `${start} - ${end}`}
  147. </ExplorerText>
  148. <Chart
  149. query={trendView.query}
  150. project={trendView.project}
  151. environment={trendView.environment}
  152. start={trendView.start}
  153. end={trendView.end}
  154. statsPeriod={trendView.statsPeriod}
  155. disableXAxis
  156. disableLegend
  157. neutralColor
  158. {...props}
  159. />
  160. </GraphPanel>
  161. <MetricsTable
  162. isLoading={isLoading}
  163. location={location}
  164. transaction={transaction}
  165. trendFunction={trendFunction}
  166. trendView={trendView}
  167. organization={organization}
  168. />
  169. <SpansList
  170. location={location}
  171. organization={organization}
  172. trendView={trendView}
  173. transaction={transaction}
  174. breakpoint={transaction.breakpoint!}
  175. trendChangeType={trendChangeType}
  176. />
  177. </Fragment>
  178. );
  179. }
  180. function InfoItem({label, value}: {label: string; value: string}) {
  181. return (
  182. <div>
  183. <InfoLabel>{label}</InfoLabel>
  184. <InfoText>{value}</InfoText>
  185. </div>
  186. );
  187. }
  188. function Header(props: HeaderProps) {
  189. const {
  190. transaction,
  191. trendChangeType,
  192. trendView,
  193. projects,
  194. organization,
  195. trendFunction,
  196. trendParameter,
  197. } = props;
  198. const regression = trendChangeType === TrendChangeType.REGRESSION;
  199. const transactionSummaryLink = getTransactionSummaryLink(
  200. trendView,
  201. transaction,
  202. projects,
  203. organization,
  204. trendFunction,
  205. trendParameter
  206. );
  207. const handleClickAnalytics = () => {
  208. trackAnalytics('performance_views.performance_change_explorer.summary_link_clicked', {
  209. organization,
  210. transaction: transaction.transaction,
  211. });
  212. };
  213. return (
  214. <HeaderWrapper data-test-id="pce-header">
  215. <FireIcon regression={regression}>
  216. <IconFire color="white" />
  217. </FireIcon>
  218. <HeaderTextWrapper>
  219. <ChangeType regression={regression}>
  220. {regression ? t('Ongoing Regression') : t('Ongoing Improvement')}
  221. </ChangeType>
  222. <TransactionNameWrapper>
  223. <TransactionName>{transaction.transaction}</TransactionName>
  224. <ViewTransactionButton
  225. borderless
  226. to={normalizeUrl(transactionSummaryLink)}
  227. icon={<IconOpen />}
  228. aria-label={t('View transaction summary')}
  229. onClick={handleClickAnalytics}
  230. />
  231. </TransactionNameWrapper>
  232. </HeaderTextWrapper>
  233. </HeaderWrapper>
  234. );
  235. }
  236. function getTransactionSummaryLink(
  237. eventView: TrendView,
  238. transaction: NormalizedTrendsTransaction,
  239. projects: Project[],
  240. organization: Organization,
  241. currentTrendFunction: string,
  242. trendParameter: TrendParameter
  243. ) {
  244. const summaryView = eventView.clone();
  245. const projectID = getTrendProjectId(transaction, projects);
  246. const target = transactionSummaryRouteWithQuery({
  247. orgSlug: organization.slug,
  248. transaction: String(transaction.transaction),
  249. query: summaryView.generateQueryStringObject(),
  250. projectID,
  251. display: DisplayModes.TREND,
  252. trendFunction: currentTrendFunction,
  253. additionalQuery: {
  254. trendParameter: trendParameter.column,
  255. },
  256. });
  257. return target;
  258. }
  259. const PanelBodyWrapper = styled('div')`
  260. padding: 0 ${space(2)};
  261. margin-top: ${space(1)};
  262. `;
  263. const HeaderWrapper = styled('div')`
  264. display: flex;
  265. flex-wrap: nowrap;
  266. margin-bottom: ${space(3)};
  267. `;
  268. const HeaderTextWrapper = styled('div')`
  269. ${p => p.theme.overflowEllipsis};
  270. `;
  271. type ChangeTypeProps = {regression: boolean};
  272. const ChangeType = styled('p')<ChangeTypeProps>`
  273. color: ${p => (p.regression ? p.theme.danger : p.theme.success)};
  274. margin-bottom: ${space(0)};
  275. `;
  276. const FireIcon = styled('div')<ChangeTypeProps>`
  277. padding: ${space(1.5)};
  278. background-color: ${p => (p.regression ? p.theme.danger : p.theme.success)};
  279. border-radius: ${space(0.5)};
  280. margin-right: ${space(2)};
  281. float: left;
  282. height: 40px;
  283. `;
  284. const TransactionName = styled('h4')`
  285. margin-right: ${space(1)};
  286. margin-bottom: ${space(0)};
  287. ${p => p.theme.overflowEllipsis};
  288. `;
  289. const TransactionNameWrapper = styled('div')`
  290. display: flex;
  291. align-items: center;
  292. margin-bottom: ${space(3)};
  293. max-width: fit-content;
  294. `;
  295. const ViewTransactionButton = styled(Button)`
  296. padding: ${space(0)};
  297. height: min-content;
  298. min-height: 0px;
  299. `;
  300. const InfoLabel = styled('strong')`
  301. color: ${p => p.theme.gray300};
  302. `;
  303. const InfoText = styled('h3')`
  304. font-weight: normal;
  305. `;
  306. const GraphPanel = styled('div')`
  307. border: 1px solid ${p => p.theme.border};
  308. border-radius: ${p => p.theme.panelBorderRadius};
  309. margin-bottom: ${space(2)};
  310. padding: ${space(3)};
  311. display: block;
  312. `;
  313. export const ExplorerText = styled('p')<{
  314. align?: string;
  315. color?: string;
  316. margin?: string;
  317. }>`
  318. margin-bottom: ${p => (p.margin ? p.margin : space(0))};
  319. color: ${p => p.color};
  320. text-align: ${p => p.align};
  321. `;