vitalDetailContent.tsx 8.2 KB

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