Browse Source

feat(perf): Add UI barebones for landing v3 (#28280)

* feat(perf): Add UI barebones for landing v3

This is setup work for the upcoming performance landing changes (widgets). It redirects to a dedicated landing index page if the performance-landing-widgets feature flag is enabled. This also makes a couple changes to other components to help make the new performance landing almost entirely functional components, where reasonable.
Kev 3 years ago
parent
commit
0c7a139183

+ 21 - 4
static/app/views/performance/content.tsx

@@ -5,6 +5,7 @@ import isEqual from 'lodash/isEqual';
 
 import {loadOrganizationTags} from 'app/actionCreators/tags';
 import {Client} from 'app/api';
+import Feature from 'app/components/acl/feature';
 import Alert from 'app/components/alert';
 import Button from 'app/components/button';
 import GlobalSdkUpdateAlert from 'app/components/globalSdkUpdateAlert';
@@ -29,6 +30,7 @@ import withProjects from 'app/utils/withProjects';
 import LandingContent from './landing/content';
 import {DEFAULT_MAX_DURATION} from './trends/utils';
 import {DEFAULT_STATS_PERIOD, generatePerformanceEventView} from './data';
+import {PerformanceLanding} from './landing';
 import Onboarding from './onboarding';
 import {addRoutePerformanceContext, getPerformanceTrendsUrl} from './utils';
 
@@ -128,7 +130,7 @@ class PerformanceContent extends Component<Props, State> {
     });
   };
 
-  handleTrendsClick() {
+  handleTrendsClick = () => {
     const {location, organization} = this.props;
 
     const newQuery = {
@@ -169,7 +171,7 @@ class PerformanceContent extends Component<Props, State> {
       pathname: getPerformanceTrendsUrl(organization),
       query: {...newQuery},
     });
-  }
+  };
 
   shouldShowOnboarding() {
     const {projects, demoMode} = this.props;
@@ -215,7 +217,7 @@ class PerformanceContent extends Component<Props, State> {
               <Button
                 priority="primary"
                 data-test-id="landing-header-trends"
-                onClick={() => this.handleTrendsClick()}
+                onClick={this.handleTrendsClick}
               >
                 {t('View Trends')}
               </Button>
@@ -250,6 +252,19 @@ class PerformanceContent extends Component<Props, State> {
     );
   }
 
+  renderLandingV3() {
+    return (
+      <PerformanceLanding
+        eventView={this.state.eventView}
+        setError={this.setError}
+        handleSearch={this.handleSearch}
+        handleTrendsClick={this.handleTrendsClick}
+        shouldShowOnboarding={this.shouldShowOnboarding()}
+        {...this.props}
+      />
+    );
+  }
+
   render() {
     const {organization} = this.props;
 
@@ -265,7 +280,9 @@ class PerformanceContent extends Component<Props, State> {
             },
           }}
         >
-          {this.renderBody()}
+          <Feature features={['organizations:performance-landing-widgets']}>
+            {({hasFeature}) => (hasFeature ? this.renderLandingV3() : this.renderBody())}
+          </Feature>
         </GlobalSelectionHeader>
       </SentryDocumentTitle>
     );

+ 33 - 0
static/app/views/performance/landing/contexts/operationBreakdownFilter.tsx

@@ -0,0 +1,33 @@
+import {createContext, useContext, useState} from 'react';
+
+import {SpanOperationBreakdownFilter} from '../../transactionSummary/filter';
+
+const OpBreakdownFilterContext = createContext<{
+  opBreakdownFilter: SpanOperationBreakdownFilter;
+  setOpBreakdownFilter: (filter: SpanOperationBreakdownFilter) => void;
+}>({
+  opBreakdownFilter: SpanOperationBreakdownFilter.None,
+  setOpBreakdownFilter: (_: SpanOperationBreakdownFilter) => {},
+});
+
+export const OpBreakdownFilterProvider = ({
+  filter,
+  children,
+}: {
+  filter?: SpanOperationBreakdownFilter;
+  children: React.ReactNode;
+}) => {
+  const [opBreakdownFilter, setOpBreakdownFilter] = useState(filter);
+  return (
+    <OpBreakdownFilterContext.Provider
+      value={{
+        opBreakdownFilter: opBreakdownFilter ?? SpanOperationBreakdownFilter.None,
+        setOpBreakdownFilter,
+      }}
+    >
+      {children}
+    </OpBreakdownFilterContext.Provider>
+  );
+};
+
+export const useOpBreakdownFilter = () => useContext(OpBreakdownFilterContext);

+ 176 - 0
static/app/views/performance/landing/index.tsx

@@ -0,0 +1,176 @@
+import {FC, useState} from 'react';
+import styled from '@emotion/styled';
+import {Location} from 'history';
+
+import Button from 'app/components/button';
+import SearchBar from 'app/components/events/searchBar';
+import FeatureBadge from 'app/components/featureBadge';
+import GlobalSdkUpdateAlert from 'app/components/globalSdkUpdateAlert';
+import * as Layout from 'app/components/layouts/thirds';
+import NavTabs from 'app/components/navTabs';
+import PageHeading from 'app/components/pageHeading';
+import * as TeamKeyTransactionManager from 'app/components/performance/teamKeyTransactionsManager';
+import {MAX_QUERY_LENGTH} from 'app/constants';
+import {t} from 'app/locale';
+import space from 'app/styles/space';
+import {Organization, Project, Team} from 'app/types';
+import EventView from 'app/utils/discover/eventView';
+import {generateAggregateFields} from 'app/utils/discover/fields';
+import {isActiveSuperuser} from 'app/utils/isActiveSuperuser';
+import withTeams from 'app/utils/withTeams';
+
+import Filter, {SpanOperationBreakdownFilter} from '../transactionSummary/filter';
+import {getTransactionSearchQuery} from '../utils';
+
+import {OpBreakdownFilterProvider} from './contexts/operationBreakdownFilter';
+import {AllTransactionsView} from './views/allTransactionsView';
+import {BackendView} from './views/backendView';
+import {FrontendOtherView} from './views/frontendOtherView';
+import {FrontendPageloadView} from './views/frontendPageloadView';
+import {MobileView} from './views/mobileView';
+import {
+  getCurrentLandingDisplay,
+  handleLandingDisplayChange,
+  LANDING_DISPLAYS,
+  LandingDisplayField,
+} from './utils';
+
+type Props = {
+  organization: Organization;
+  eventView: EventView;
+  location: Location;
+  projects: Project[];
+  teams: Team[];
+  shouldShowOnboarding: boolean;
+  setError: (msg: string | undefined) => void;
+  handleSearch: (searchQuery: string) => void;
+  handleTrendsClick: () => void;
+};
+
+const fieldToViewMap: Record<LandingDisplayField, FC<Props>> = {
+  [LandingDisplayField.ALL]: AllTransactionsView,
+  [LandingDisplayField.BACKEND]: BackendView,
+  [LandingDisplayField.FRONTEND_OTHER]: FrontendOtherView,
+  [LandingDisplayField.FRONTEND_PAGELOAD]: FrontendPageloadView,
+  [LandingDisplayField.MOBILE]: MobileView,
+};
+
+function _PerformanceLanding(props: Props) {
+  const {
+    organization,
+    location,
+    eventView,
+    projects,
+    teams,
+    handleSearch,
+    handleTrendsClick,
+    shouldShowOnboarding,
+  } = props;
+
+  const currentLandingDisplay = getCurrentLandingDisplay(location, projects, eventView);
+  const filterString = getTransactionSearchQuery(location, eventView.query);
+
+  const isSuperuser = isActiveSuperuser();
+  const userTeams = teams.filter(({isMember}) => isMember || isSuperuser);
+
+  const [spanFilter, setSpanFilter] = useState(SpanOperationBreakdownFilter.None);
+  const showOnboarding = shouldShowOnboarding;
+
+  const shownLandingDisplays = LANDING_DISPLAYS.filter(
+    ({isShown}) => !isShown || isShown(organization)
+  );
+
+  const ViewComponent = fieldToViewMap[currentLandingDisplay.field];
+
+  return (
+    <div data-test-id="performance-landing-v3">
+      <Layout.Header>
+        <Layout.HeaderContent>
+          <StyledHeading>{t('Performance')}</StyledHeading>
+        </Layout.HeaderContent>
+        <Layout.HeaderActions>
+          {!showOnboarding && (
+            <Button
+              priority="primary"
+              data-test-id="landing-header-trends"
+              onClick={() => handleTrendsClick()}
+            >
+              {t('View Trends')}
+            </Button>
+          )}
+        </Layout.HeaderActions>
+
+        <StyledNavTabs>
+          {shownLandingDisplays.map(({badge, label, field}) => (
+            <li
+              key={label}
+              className={currentLandingDisplay.field === field ? 'active' : ''}
+            >
+              <a href="#" onClick={() => handleLandingDisplayChange(field, location)}>
+                {t(label)}
+                {badge && <FeatureBadge type={badge} />}
+              </a>
+            </li>
+          ))}
+        </StyledNavTabs>
+      </Layout.Header>
+      <Layout.Body>
+        <Layout.Main fullWidth>
+          <GlobalSdkUpdateAlert />
+          <OpBreakdownFilterProvider>
+            <SearchContainerWithFilter>
+              <Filter
+                organization={organization}
+                currentFilter={spanFilter}
+                onChangeFilter={setSpanFilter}
+              />
+              <SearchBar
+                searchSource="performance_landing"
+                organization={organization}
+                projectIds={eventView.project}
+                query={filterString}
+                fields={generateAggregateFields(
+                  organization,
+                  [...eventView.fields, {field: 'tps()'}],
+                  ['epm()', 'eps()']
+                )}
+                onSearch={handleSearch}
+                maxQueryLength={MAX_QUERY_LENGTH}
+              />
+            </SearchContainerWithFilter>
+            <TeamKeyTransactionManager.Provider
+              organization={organization}
+              teams={userTeams}
+              selectedTeams={['myteams']}
+              selectedProjects={eventView.project.map(String)}
+            >
+              <ViewComponent {...props} />
+            </TeamKeyTransactionManager.Provider>
+          </OpBreakdownFilterProvider>
+        </Layout.Main>
+      </Layout.Body>
+    </div>
+  );
+}
+
+export const PerformanceLanding = withTeams(_PerformanceLanding);
+
+const StyledHeading = styled(PageHeading)`
+  line-height: 40px;
+`;
+
+const StyledNavTabs = styled(NavTabs)`
+  margin-bottom: 0;
+  /* Makes sure the tabs are pushed into another row */
+  width: 100%;
+`;
+
+const SearchContainerWithFilter = styled('div')`
+  display: grid;
+  grid-gap: ${space(0)};
+  margin-bottom: ${space(2)};
+
+  @media (min-width: ${p => p.theme.breakpoints[0]}) {
+    grid-template-columns: min-content 1fr;
+  }
+`;

+ 23 - 0
static/app/views/performance/landing/utils.tsx

@@ -1,3 +1,4 @@
+import {browserHistory} from 'react-router';
 import {Location} from 'history';
 
 import {t} from 'app/locale';
@@ -11,6 +12,7 @@ import {
 } from 'app/utils/formatters';
 import {HistogramData} from 'app/utils/performance/histogram/types';
 import {decodeScalar} from 'app/utils/queryString';
+import {MutableSearch} from 'app/utils/tokenizeSearch';
 
 import {AxisOption, getTermHelp, PERFORMANCE_TERM} from '../data';
 import {Rectangle} from '../transactionSummary/transactionVitals/types';
@@ -76,6 +78,27 @@ export function getCurrentLandingDisplay(
   return defaultDisplay || LANDING_DISPLAYS[0];
 }
 
+export function handleLandingDisplayChange(field: string, location: Location) {
+  const newQuery = {...location.query};
+
+  delete newQuery[LEFT_AXIS_QUERY_KEY];
+  delete newQuery[RIGHT_AXIS_QUERY_KEY];
+
+  // Transaction op can affect the display and show no results if it is explicitly set.
+  const query = decodeScalar(location.query.query, '');
+  const searchConditions = new MutableSearch(query);
+  searchConditions.removeFilter('transaction.op');
+
+  browserHistory.push({
+    pathname: location.pathname,
+    query: {
+      ...newQuery,
+      query: searchConditions.formatString(),
+      landingDisplay: field,
+    },
+  });
+}
+
 export function getChartWidth(chartData: HistogramData, refPixelRect: Rectangle | null) {
   const distance = refPixelRect ? refPixelRect.point2.x - refPixelRect.point1.x : 0;
   const chartWidth = chartData.length * distance;

+ 5 - 0
static/app/views/performance/landing/views/allTransactionsView.tsx

@@ -0,0 +1,5 @@
+import {BasePerformanceViewProps} from './types';
+
+export function AllTransactionsView(_: BasePerformanceViewProps) {
+  return <div />;
+}

+ 5 - 0
static/app/views/performance/landing/views/backendView.tsx

@@ -0,0 +1,5 @@
+import {BasePerformanceViewProps} from './types';
+
+export function BackendView(_: BasePerformanceViewProps) {
+  return <div />;
+}

+ 5 - 0
static/app/views/performance/landing/views/frontendOtherView.tsx

@@ -0,0 +1,5 @@
+import {BasePerformanceViewProps} from './types';
+
+export function FrontendOtherView(_: BasePerformanceViewProps) {
+  return <div />;
+}

+ 5 - 0
static/app/views/performance/landing/views/frontendPageloadView.tsx

@@ -0,0 +1,5 @@
+import {BasePerformanceViewProps} from './types';
+
+export function FrontendPageloadView(_: BasePerformanceViewProps) {
+  return <div data-test-id="frontend-pageload-view" />;
+}

+ 5 - 0
static/app/views/performance/landing/views/mobileView.tsx

@@ -0,0 +1,5 @@
+import {BasePerformanceViewProps} from './types';
+
+export function MobileView(_: BasePerformanceViewProps) {
+  return <div />;
+}

+ 1 - 0
static/app/views/performance/landing/views/types.tsx

@@ -0,0 +1 @@
+export type BasePerformanceViewProps = {};

Some files were not shown because too many files changed in this diff