From 27cc6545d6be48e928a6ec8e9ca2dfccb86574ad Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Wed, 1 Sep 2021 12:57:29 -0400 Subject: [PATCH] ref(performance): Separate header from content in transaction vitals (#28343) In preparation to unify some of the page layouts in transaction summary pages, this change separates the header from the content in the transaction vitals tab. --- .../transactionVitals/content.tsx | 184 +++++++---------- .../transactionVitals/index.tsx | 185 ++++++++---------- 2 files changed, 154 insertions(+), 215 deletions(-) diff --git a/static/app/views/performance/transactionSummary/transactionVitals/content.tsx b/static/app/views/performance/transactionSummary/transactionVitals/content.tsx index 06ae487542ede9..89c01684f23ea3 100644 --- a/static/app/views/performance/transactionSummary/transactionVitals/content.tsx +++ b/static/app/views/performance/transactionSummary/transactionVitals/content.tsx @@ -1,52 +1,38 @@ -import * as React from 'react'; import {browserHistory} from 'react-router'; import styled from '@emotion/styled'; import {Location} from 'history'; import Button from 'app/components/button'; -import {CreateAlertFromViewButton} from 'app/components/createAlertButton'; import DropdownControl, {DropdownItem} from 'app/components/dropdownControl'; import SearchBar from 'app/components/events/searchBar'; import * as Layout from 'app/components/layouts/thirds'; import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams'; import {t} from 'app/locale'; import space from 'app/styles/space'; -import {Organization, Project} from 'app/types'; +import {Organization} from 'app/types'; import {trackAnalyticsEvent} from 'app/utils/analytics'; import EventView from 'app/utils/discover/eventView'; import Histogram from 'app/utils/performance/histogram'; import {FILTER_OPTIONS} from 'app/utils/performance/histogram/constants'; import {decodeScalar} from 'app/utils/queryString'; -import TransactionHeader from '../header'; -import Tab from '../tabs'; - import {ZOOM_KEYS} from './constants'; import VitalsPanel from './vitalsPanel'; type Props = { location: Location; - eventView: EventView; - transactionName: string; organization: Organization; - projects: Project[]; -}; - -type State = { - incompatibleAlertNotice: React.ReactNode; + eventView: EventView; }; -class VitalsContent extends React.Component { - state: State = { - incompatibleAlertNotice: null, - }; - - handleSearch = (query: string) => { - const {location} = this.props; +function VitalsContent(props: Props) { + const {location, organization, eventView} = props; + const query = decodeScalar(location.query.query, ''); + const handleSearch = (newQuery: string) => { const queryParams = getParams({ ...(location.query || {}), - query, + query: newQuery, }); // do not propagate pagination when making a new search @@ -58,101 +44,67 @@ class VitalsContent extends React.Component { }); }; - handleIncompatibleQuery: React.ComponentProps< - typeof CreateAlertFromViewButton - >['onIncompatibleQuery'] = (incompatibleAlertNoticeFn, _errors) => { - const incompatibleAlertNotice = incompatibleAlertNoticeFn(() => - this.setState({incompatibleAlertNotice: null}) - ); - this.setState({incompatibleAlertNotice}); - }; - - render() { - const {transactionName, location, eventView, projects, organization} = this.props; - const {incompatibleAlertNotice} = this.state; - const query = decodeScalar(location.query.query, ''); - - return ( - - - - {({activeFilter, handleFilterChange, handleResetView, isZoomed}) => { - return ( - - {incompatibleAlertNotice && ( - {incompatibleAlertNotice} - )} - - - - - {FILTER_OPTIONS.map(({label, value}) => ( - { - trackAnalyticsEvent({ - eventKey: 'performance_views.vitals.filter_changed', - eventName: 'Performance Views: Change vitals filter', - organization_id: organization.id, - value: filterOption, - }); - handleFilterChange(filterOption); - }} - eventKey={value} - isActive={value === activeFilter.value} - > - {label} - - ))} - - - - - - - ); - }} - - - ); - } + return ( + + {({activeFilter, handleFilterChange, handleResetView, isZoomed}) => ( + + + + + {FILTER_OPTIONS.map(({label, value}) => ( + { + trackAnalyticsEvent({ + eventKey: 'performance_views.vitals.filter_changed', + eventName: 'Performance Views: Change vitals filter', + organization_id: organization.id, + value: filterOption, + }); + handleFilterChange(filterOption); + }} + eventKey={value} + isActive={value === activeFilter.value} + > + {label} + + ))} + + + + + + )} + + ); } const StyledSearchBar = styled(SearchBar)` diff --git a/static/app/views/performance/transactionSummary/transactionVitals/index.tsx b/static/app/views/performance/transactionSummary/transactionVitals/index.tsx index 3986ce532ef99f..7ca0b6e6a775d7 100644 --- a/static/app/views/performance/transactionSummary/transactionVitals/index.tsx +++ b/static/app/views/performance/transactionSummary/transactionVitals/index.tsx @@ -1,147 +1,134 @@ -import {Component} from 'react'; +import {ReactNode, useState} from 'react'; import {browserHistory} from 'react-router'; import styled from '@emotion/styled'; import {Location} from 'history'; import Feature from 'app/components/acl/feature'; import Alert from 'app/components/alert'; +import * as Layout from 'app/components/layouts/thirds'; import LightWeightNoProjectMessage from 'app/components/lightWeightNoProjectMessage'; import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader'; import SentryDocumentTitle from 'app/components/sentryDocumentTitle'; import {t} from 'app/locale'; import {PageContent} from 'app/styles/organization'; -import {GlobalSelection, Organization, Project} from 'app/types'; +import {Organization, Project} from 'app/types'; +import {defined} from 'app/utils'; import EventView from 'app/utils/discover/eventView'; import {isAggregateField, WebVital} from 'app/utils/discover/fields'; import {WEB_VITAL_DETAILS} from 'app/utils/performance/vitals/constants'; import {decodeScalar} from 'app/utils/queryString'; import {MutableSearch} from 'app/utils/tokenizeSearch'; -import withGlobalSelection from 'app/utils/withGlobalSelection'; import withOrganization from 'app/utils/withOrganization'; import withProjects from 'app/utils/withProjects'; import {getTransactionName} from '../../utils'; +import TransactionHeader from '../header'; +import Tab from '../tabs'; import {PERCENTILE, VITAL_GROUPS} from './constants'; -import RumContent from './content'; +import VitalsContent from './content'; type Props = { location: Location; organization: Organization; projects: Project[]; - selection: GlobalSelection; }; -type State = { - eventView: EventView | undefined; -}; - -class TransactionVitals extends Component { - state: State = { - eventView: generateRumEventView( - this.props.location, - getTransactionName(this.props.location) - ), - }; - - static getDerivedStateFromProps(nextProps: Readonly, prevState: State): State { - return { - ...prevState, - eventView: generateRumEventView( - nextProps.location, - getTransactionName(nextProps.location) - ), - }; +function TransactionVitals(props: Props) { + const {location, organization, projects} = props; + const projectId = decodeScalar(location.query.project); + const transactionName = getTransactionName(location); + + if (!defined(projectId) || !defined(transactionName)) { + // If there is no transaction name, redirect to the Performance landing page + browserHistory.replace({ + pathname: `/organizations/${organization.slug}/performance/`, + query: { + ...location.query, + }, + }); + return null; } - getDocumentTitle(): string { - const name = getTransactionName(this.props.location); - - const hasTransactionName = typeof name === 'string' && String(name).trim().length > 0; + const project = projects.find(p => p.id === projectId); - if (hasTransactionName) { - return [String(name).trim(), t('Vitals')].join(' \u2014 '); - } - - return [t('Summary'), t('Vitals')].join(' \u2014 '); - } - - renderNoAccess = () => { - return {t("You don't have access to this feature")}; + const [incompatibleAlertNotice, setIncompatibleAlertNotice] = useState(null); + const handleIncompatibleQuery = (incompatibleAlertNoticeFn, _errors) => { + const notice = incompatibleAlertNoticeFn(() => setIncompatibleAlertNotice(null)); + setIncompatibleAlertNotice(notice); }; - render() { - const {organization, projects, location} = this.props; - const {eventView} = this.state; - const transactionName = getTransactionName(location); - if (!eventView || transactionName === undefined) { - // If there is no transaction name, redirect to the Performance landing page - browserHistory.replace({ - pathname: `/organizations/${organization.slug}/performance/`, - query: { - ...location.query, - }, - }); - return null; - } - - const shouldForceProject = eventView.project.length === 1; - const forceProject = shouldForceProject - ? projects.find(p => parseInt(p.id, 10) === eventView.project[0]) - : undefined; - const projectSlugs = eventView.project - .map(projectId => projects.find(p => parseInt(p.id, 10) === projectId)) - .filter((p: Project | undefined): p is Project => p !== undefined) - .map(p => p.slug); - - return ( - + - - - - - + + + + {incompatibleAlertNotice && ( + {incompatibleAlertNotice} + )} + - - - - - - ); + + + + + + + ); +} + +function getDocumentTitle(transactionName): string { + const hasTransactionName = + typeof transactionName === 'string' && String(transactionName).trim().length > 0; + + if (hasTransactionName) { + return [String(transactionName).trim(), t('Vitals')].join(' \u2014 '); } + + return [t('Summary'), t('Vitals')].join(' \u2014 '); +} + +function NoAccess() { + return {t("You don't have access to this feature")}; } const StyledPageContent = styled(PageContent)` padding: 0; `; -function generateRumEventView( - location: Location, - transactionName: string | undefined -): EventView | undefined { - if (transactionName === undefined) { - return undefined; - } +function generateEventView(location: Location, transactionName: string): EventView { const query = decodeScalar(location.query.query, ''); const conditions = new MutableSearch(query); conditions @@ -176,4 +163,4 @@ function generateRumEventView( ); } -export default withGlobalSelection(withProjects(withOrganization(TransactionVitals))); +export default withProjects(withOrganization(TransactionVitals));