vitalDetailContent.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import * as React from 'react';
  2. import {browserHistory, InjectedRouter} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import omit from 'lodash/omit';
  6. import Feature from 'sentry/components/acl/feature';
  7. import Alert from 'sentry/components/alert';
  8. import Button from 'sentry/components/button';
  9. import ButtonBar from 'sentry/components/buttonBar';
  10. import {CreateAlertFromViewButton} from 'sentry/components/createAlertButton';
  11. import SearchBar from 'sentry/components/events/searchBar';
  12. import * as Layout from 'sentry/components/layouts/thirds';
  13. import LoadingIndicator from 'sentry/components/loadingIndicator';
  14. import {getParams} from 'sentry/components/organizations/globalSelectionHeader/getParams';
  15. import * as TeamKeyTransactionManager from 'sentry/components/performance/teamKeyTransactionsManager';
  16. import {IconChevron} from 'sentry/icons';
  17. import {IconFlag} from 'sentry/icons/iconFlag';
  18. import {t} from 'sentry/locale';
  19. import space from 'sentry/styles/space';
  20. import {Organization, Project} from 'sentry/types';
  21. import {generateQueryWithTag} from 'sentry/utils';
  22. import EventView from 'sentry/utils/discover/eventView';
  23. import {WebVital} from 'sentry/utils/discover/fields';
  24. import {decodeScalar} from 'sentry/utils/queryString';
  25. import Teams from 'sentry/utils/teams';
  26. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  27. import withProjects from 'sentry/utils/withProjects';
  28. import Breadcrumb from '../breadcrumb';
  29. import MetricsSearchBar from '../metricsSearchBar';
  30. import {MetricsSwitch} from '../metricsSwitch';
  31. import {getTransactionSearchQuery} from '../utils';
  32. import Table from './table';
  33. import {vitalDescription, vitalMap} from './utils';
  34. import VitalChart from './vitalChart';
  35. import VitalInfo from './vitalInfo';
  36. const FRONTEND_VITALS = [WebVital.FCP, WebVital.LCP, WebVital.FID, WebVital.CLS];
  37. type Props = {
  38. location: Location;
  39. eventView: EventView;
  40. organization: Organization;
  41. projects: Project[];
  42. router: InjectedRouter;
  43. vitalName: WebVital;
  44. isMetricsData: boolean;
  45. };
  46. type State = {
  47. incompatibleAlertNotice: React.ReactNode;
  48. error: string | undefined;
  49. };
  50. function getSummaryConditions(query: string) {
  51. const parsed = new MutableSearch(query);
  52. parsed.freeText = [];
  53. return parsed.formatString();
  54. }
  55. class VitalDetailContent extends React.Component<Props, State> {
  56. state: State = {
  57. incompatibleAlertNotice: null,
  58. error: undefined,
  59. };
  60. handleSearch = (query: string) => {
  61. const {location} = this.props;
  62. const queryParams = getParams({
  63. ...(location.query || {}),
  64. query,
  65. });
  66. // do not propagate pagination when making a new search
  67. const searchQueryParams = omit(queryParams, 'cursor');
  68. browserHistory.push({
  69. pathname: location.pathname,
  70. query: searchQueryParams,
  71. });
  72. };
  73. generateTagUrl = (key: string, value: string) => {
  74. const {location} = this.props;
  75. const query = generateQueryWithTag(location.query, {key, value});
  76. return {
  77. ...location,
  78. query,
  79. };
  80. };
  81. handleIncompatibleQuery: React.ComponentProps<
  82. typeof CreateAlertFromViewButton
  83. >['onIncompatibleQuery'] = (incompatibleAlertNoticeFn, _errors) => {
  84. const incompatibleAlertNotice = incompatibleAlertNoticeFn(() =>
  85. this.setState({incompatibleAlertNotice: null})
  86. );
  87. this.setState({incompatibleAlertNotice});
  88. };
  89. renderCreateAlertButton() {
  90. const {eventView, organization, projects} = this.props;
  91. return (
  92. <CreateAlertFromViewButton
  93. eventView={eventView}
  94. organization={organization}
  95. projects={projects}
  96. onIncompatibleQuery={this.handleIncompatibleQuery}
  97. onSuccess={() => {}}
  98. referrer="performance"
  99. />
  100. );
  101. }
  102. renderVitalSwitcher() {
  103. const {vitalName, location} = this.props;
  104. const position = FRONTEND_VITALS.indexOf(vitalName);
  105. if (position < 0) {
  106. return null;
  107. }
  108. const previousDisabled = position === 0;
  109. const nextDisabled = position === FRONTEND_VITALS.length - 1;
  110. const switchVital = newVitalName => {
  111. return () => {
  112. browserHistory.push({
  113. pathname: location.pathname,
  114. query: {
  115. ...location.query,
  116. vitalName: newVitalName,
  117. },
  118. });
  119. };
  120. };
  121. return (
  122. <ButtonBar merged>
  123. <Button
  124. icon={<IconChevron direction="left" size="sm" />}
  125. aria-label={t('Previous')}
  126. disabled={previousDisabled}
  127. onClick={switchVital(FRONTEND_VITALS[position - 1])}
  128. />
  129. <Button
  130. icon={<IconChevron direction="right" size="sm" />}
  131. aria-label={t('Next')}
  132. disabled={nextDisabled}
  133. onClick={switchVital(FRONTEND_VITALS[position + 1])}
  134. />
  135. </ButtonBar>
  136. );
  137. }
  138. setError = (error: string | undefined) => {
  139. this.setState({error});
  140. };
  141. renderError() {
  142. const {error} = this.state;
  143. if (!error) {
  144. return null;
  145. }
  146. return (
  147. <Alert type="error" icon={<IconFlag size="md" />}>
  148. {error}
  149. </Alert>
  150. );
  151. }
  152. renderContent(vital: WebVital) {
  153. const {isMetricsData, location, organization, eventView} = this.props;
  154. const query = decodeScalar(location.query.query, '');
  155. if (isMetricsData) {
  156. return (
  157. <React.Fragment>
  158. <StyledMetricsSearchBar
  159. searchSource="performance_vitals_metrics"
  160. orgSlug={organization.slug}
  161. projectIds={eventView.project}
  162. query={query}
  163. onSearch={this.handleSearch}
  164. />
  165. <div>{'TODO'}</div>
  166. <StyledVitalInfo>{'TODO'}</StyledVitalInfo>
  167. </React.Fragment>
  168. );
  169. }
  170. return (
  171. <React.Fragment>
  172. <StyledSearchBar
  173. searchSource="performance_vitals"
  174. organization={organization}
  175. projectIds={eventView.project}
  176. query={query}
  177. fields={eventView.fields}
  178. onSearch={this.handleSearch}
  179. />
  180. <VitalChart
  181. organization={organization}
  182. query={eventView.query}
  183. project={eventView.project}
  184. environment={eventView.environment}
  185. start={eventView.start}
  186. end={eventView.end}
  187. statsPeriod={eventView.statsPeriod}
  188. />
  189. <StyledVitalInfo>
  190. <VitalInfo location={location} vital={vital} />
  191. </StyledVitalInfo>
  192. </React.Fragment>
  193. );
  194. }
  195. render() {
  196. const {location, eventView, organization, vitalName, projects} = this.props;
  197. const {incompatibleAlertNotice} = this.state;
  198. const vital = vitalName || WebVital.LCP;
  199. const filterString = getTransactionSearchQuery(location);
  200. const summaryConditions = getSummaryConditions(filterString);
  201. const description = vitalDescription[vitalName];
  202. return (
  203. <React.Fragment>
  204. <Layout.Header>
  205. <Layout.HeaderContent>
  206. <Breadcrumb
  207. organization={organization}
  208. location={location}
  209. vitalName={vital}
  210. />
  211. <Layout.Title>{vitalMap[vital]}</Layout.Title>
  212. </Layout.HeaderContent>
  213. <Layout.HeaderActions>
  214. <ButtonBar gap={1}>
  215. <MetricsSwitch onSwitch={() => this.handleSearch('')} />
  216. <Feature organization={organization} features={['incidents']}>
  217. {({hasFeature}) => hasFeature && this.renderCreateAlertButton()}
  218. </Feature>
  219. {this.renderVitalSwitcher()}
  220. </ButtonBar>
  221. </Layout.HeaderActions>
  222. </Layout.Header>
  223. <Layout.Body>
  224. {this.renderError()}
  225. {incompatibleAlertNotice && (
  226. <Layout.Main fullWidth>{incompatibleAlertNotice}</Layout.Main>
  227. )}
  228. <Layout.Main fullWidth>
  229. <StyledDescription>{description}</StyledDescription>
  230. {this.renderContent(vital)}
  231. <Teams provideUserTeams>
  232. {({teams, initiallyLoaded}) =>
  233. initiallyLoaded ? (
  234. <TeamKeyTransactionManager.Provider
  235. organization={organization}
  236. teams={teams}
  237. selectedTeams={['myteams']}
  238. selectedProjects={eventView.project.map(String)}
  239. >
  240. <Table
  241. eventView={eventView}
  242. projects={projects}
  243. organization={organization}
  244. location={location}
  245. setError={this.setError}
  246. summaryConditions={summaryConditions}
  247. />
  248. </TeamKeyTransactionManager.Provider>
  249. ) : (
  250. <LoadingIndicator />
  251. )
  252. }
  253. </Teams>
  254. </Layout.Main>
  255. </Layout.Body>
  256. </React.Fragment>
  257. );
  258. }
  259. }
  260. export default withProjects(VitalDetailContent);
  261. const StyledDescription = styled('div')`
  262. font-size: ${p => p.theme.fontSizeMedium};
  263. margin-bottom: ${space(3)};
  264. `;
  265. const StyledSearchBar = styled(SearchBar)`
  266. margin-bottom: ${space(2)};
  267. `;
  268. const StyledVitalInfo = styled('div')`
  269. margin-bottom: ${space(3)};
  270. `;
  271. const StyledMetricsSearchBar = styled(MetricsSearchBar)`
  272. margin-bottom: ${space(2)};
  273. `;