content.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. import * as React from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import Alert from 'sentry/components/alert';
  6. import Breadcrumbs from 'sentry/components/breadcrumbs';
  7. import DropdownControl, {DropdownItem} from 'sentry/components/dropdownControl';
  8. import SearchBar from 'sentry/components/events/searchBar';
  9. import * as Layout from 'sentry/components/layouts/thirds';
  10. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  11. import {MAX_QUERY_LENGTH} from 'sentry/constants';
  12. import {IconFlag} from 'sentry/icons/iconFlag';
  13. import {t} from 'sentry/locale';
  14. import space from 'sentry/styles/space';
  15. import {Organization, PageFilters} from 'sentry/types';
  16. import {trackAnalyticsEvent} from 'sentry/utils/analytics';
  17. import EventView from 'sentry/utils/discover/eventView';
  18. import {generateAggregateFields} from 'sentry/utils/discover/fields';
  19. import {decodeScalar} from 'sentry/utils/queryString';
  20. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  21. import withPageFilters from 'sentry/utils/withPageFilters';
  22. import {getPerformanceLandingUrl, getTransactionSearchQuery} from '../utils';
  23. import ChangedTransactions from './changedTransactions';
  24. import {TrendChangeType, TrendFunctionField, TrendView} from './types';
  25. import {
  26. DEFAULT_MAX_DURATION,
  27. DEFAULT_TRENDS_STATS_PERIOD,
  28. getCurrentTrendFunction,
  29. getCurrentTrendParameter,
  30. getSelectedQueryKey,
  31. modifyTrendsViewDefaultPeriod,
  32. resetCursors,
  33. TRENDS_FUNCTIONS,
  34. TRENDS_PARAMETERS,
  35. } from './utils';
  36. type Props = {
  37. organization: Organization;
  38. location: Location;
  39. eventView: EventView;
  40. selection: PageFilters;
  41. };
  42. type State = {
  43. error?: string;
  44. previousTrendFunction?: TrendFunctionField;
  45. };
  46. class TrendsContent extends React.Component<Props, State> {
  47. state: State = {};
  48. handleSearch = (searchQuery: string) => {
  49. const {location} = this.props;
  50. const cursors = resetCursors();
  51. browserHistory.push({
  52. pathname: location.pathname,
  53. query: {
  54. ...location.query,
  55. ...cursors,
  56. query: String(searchQuery).trim() || undefined,
  57. },
  58. });
  59. };
  60. setError = (error: string | undefined) => {
  61. this.setState({error});
  62. };
  63. handleTrendFunctionChange = (field: string) => {
  64. const {organization, location} = this.props;
  65. const offsets = {};
  66. Object.values(TrendChangeType).forEach(trendChangeType => {
  67. const queryKey = getSelectedQueryKey(trendChangeType);
  68. offsets[queryKey] = undefined;
  69. });
  70. trackAnalyticsEvent({
  71. eventKey: 'performance_views.trends.change_function',
  72. eventName: 'Performance Views: Change Function',
  73. organization_id: parseInt(organization.id, 10),
  74. function_name: field,
  75. });
  76. this.setState({
  77. previousTrendFunction: getCurrentTrendFunction(location).field,
  78. });
  79. const cursors = resetCursors();
  80. browserHistory.push({
  81. pathname: location.pathname,
  82. query: {
  83. ...location.query,
  84. ...offsets,
  85. ...cursors,
  86. trendFunction: field,
  87. },
  88. });
  89. };
  90. renderError() {
  91. const {error} = this.state;
  92. if (!error) {
  93. return null;
  94. }
  95. return (
  96. <Alert type="error" icon={<IconFlag size="md" />}>
  97. {error}
  98. </Alert>
  99. );
  100. }
  101. handleParameterChange = (label: string) => {
  102. const {organization, location} = this.props;
  103. const cursors = resetCursors();
  104. trackAnalyticsEvent({
  105. eventKey: 'performance_views.trends.change_parameter',
  106. eventName: 'Performance Views: Change Parameter',
  107. organization_id: parseInt(organization.id, 10),
  108. parameter_name: label,
  109. });
  110. browserHistory.push({
  111. pathname: location.pathname,
  112. query: {
  113. ...location.query,
  114. ...cursors,
  115. trendParameter: label,
  116. },
  117. });
  118. };
  119. getPerformanceLink() {
  120. const {location} = this.props;
  121. const newQuery = {
  122. ...location.query,
  123. };
  124. const query = decodeScalar(location.query.query, '');
  125. const conditions = new MutableSearch(query);
  126. // This stops errors from occurring when navigating to other views since we are appending aggregates to the trends view
  127. conditions.removeFilter('tpm()');
  128. conditions.removeFilter('confidence()');
  129. conditions.removeFilter('transaction.duration');
  130. newQuery.query = conditions.formatString();
  131. return {
  132. pathname: getPerformanceLandingUrl(this.props.organization),
  133. query: newQuery,
  134. };
  135. }
  136. render() {
  137. const {organization, eventView, location} = this.props;
  138. const {previousTrendFunction} = this.state;
  139. const trendView = eventView.clone() as TrendView;
  140. modifyTrendsViewDefaultPeriod(trendView, location);
  141. const fields = generateAggregateFields(
  142. organization,
  143. [
  144. {
  145. field: 'absolute_correlation()',
  146. },
  147. {
  148. field: 'trend_percentage()',
  149. },
  150. {
  151. field: 'trend_difference()',
  152. },
  153. {
  154. field: 'count_percentage()',
  155. },
  156. {
  157. field: 'tpm()',
  158. },
  159. {
  160. field: 'tps()',
  161. },
  162. ],
  163. ['epm()', 'eps()']
  164. );
  165. const currentTrendFunction = getCurrentTrendFunction(location);
  166. const currentTrendParameter = getCurrentTrendParameter(location);
  167. const query = getTransactionSearchQuery(location);
  168. return (
  169. <PageFiltersContainer
  170. defaultSelection={{
  171. datetime: {
  172. start: null,
  173. end: null,
  174. utc: false,
  175. period: DEFAULT_TRENDS_STATS_PERIOD,
  176. },
  177. }}
  178. >
  179. <Layout.Header>
  180. <Layout.HeaderContent>
  181. <Breadcrumbs
  182. crumbs={[
  183. {
  184. label: 'Performance',
  185. to: this.getPerformanceLink(),
  186. },
  187. {
  188. label: 'Trends',
  189. },
  190. ]}
  191. />
  192. <Layout.Title>{t('Trends')}</Layout.Title>
  193. </Layout.HeaderContent>
  194. </Layout.Header>
  195. <Layout.Body>
  196. <Layout.Main fullWidth>
  197. <DefaultTrends location={location} eventView={eventView}>
  198. <StyledSearchContainer>
  199. <StyledSearchBar
  200. searchSource="trends"
  201. organization={organization}
  202. projectIds={trendView.project}
  203. query={query}
  204. fields={fields}
  205. onSearch={this.handleSearch}
  206. maxQueryLength={MAX_QUERY_LENGTH}
  207. />
  208. <TrendsDropdown>
  209. <DropdownControl
  210. buttonProps={{prefix: t('Percentile')}}
  211. label={currentTrendFunction.label}
  212. >
  213. {TRENDS_FUNCTIONS.map(({label, field}) => (
  214. <DropdownItem
  215. key={field}
  216. onSelect={this.handleTrendFunctionChange}
  217. eventKey={field}
  218. data-test-id={field}
  219. isActive={field === currentTrendFunction.field}
  220. >
  221. {label}
  222. </DropdownItem>
  223. ))}
  224. </DropdownControl>
  225. </TrendsDropdown>
  226. <TrendsDropdown>
  227. <DropdownControl
  228. buttonProps={{prefix: t('Parameter')}}
  229. label={currentTrendParameter.label}
  230. >
  231. {TRENDS_PARAMETERS.map(({label}) => (
  232. <DropdownItem
  233. key={label}
  234. onSelect={this.handleParameterChange}
  235. eventKey={label}
  236. data-test-id={label}
  237. isActive={label === currentTrendParameter.label}
  238. >
  239. {label}
  240. </DropdownItem>
  241. ))}
  242. </DropdownControl>
  243. </TrendsDropdown>
  244. </StyledSearchContainer>
  245. <TrendsLayoutContainer>
  246. <ChangedTransactions
  247. trendChangeType={TrendChangeType.IMPROVED}
  248. previousTrendFunction={previousTrendFunction}
  249. trendView={trendView}
  250. location={location}
  251. setError={this.setError}
  252. />
  253. <ChangedTransactions
  254. trendChangeType={TrendChangeType.REGRESSION}
  255. previousTrendFunction={previousTrendFunction}
  256. trendView={trendView}
  257. location={location}
  258. setError={this.setError}
  259. />
  260. </TrendsLayoutContainer>
  261. </DefaultTrends>
  262. </Layout.Main>
  263. </Layout.Body>
  264. </PageFiltersContainer>
  265. );
  266. }
  267. }
  268. type DefaultTrendsProps = {
  269. children: React.ReactNode[];
  270. location: Location;
  271. eventView: EventView;
  272. };
  273. class DefaultTrends extends React.Component<DefaultTrendsProps> {
  274. hasPushedDefaults = false;
  275. render() {
  276. const {children, location, eventView} = this.props;
  277. const queryString = decodeScalar(location.query.query);
  278. const trendParameter = getCurrentTrendParameter(location);
  279. const conditions = new MutableSearch(queryString || '');
  280. if (queryString || this.hasPushedDefaults) {
  281. this.hasPushedDefaults = true;
  282. return <React.Fragment>{children}</React.Fragment>;
  283. }
  284. this.hasPushedDefaults = true;
  285. conditions.setFilterValues('tpm()', ['>0.01']);
  286. conditions.setFilterValues(trendParameter.column, ['>0', `<${DEFAULT_MAX_DURATION}`]);
  287. const query = conditions.formatString();
  288. eventView.query = query;
  289. browserHistory.push({
  290. pathname: location.pathname,
  291. query: {
  292. ...location.query,
  293. cursor: undefined,
  294. query: String(query).trim() || undefined,
  295. },
  296. });
  297. return null;
  298. }
  299. }
  300. const StyledSearchBar = styled(SearchBar)`
  301. flex-grow: 1;
  302. margin-bottom: ${space(2)};
  303. `;
  304. const TrendsDropdown = styled('div')`
  305. margin-left: ${space(1)};
  306. flex-grow: 0;
  307. `;
  308. const StyledSearchContainer = styled('div')`
  309. display: flex;
  310. `;
  311. const TrendsLayoutContainer = styled('div')`
  312. display: grid;
  313. grid-gap: ${space(2)};
  314. @media (min-width: ${p => p.theme.breakpoints[1]}) {
  315. grid-template-columns: repeat(2, minmax(0, 1fr));
  316. align-items: stretch;
  317. }
  318. `;
  319. export default withPageFilters(TrendsContent);