Browse Source

feat(perf): Add trends widgets for landing page v3 (#29461)

* feat(perf): Add trends widgets for landing page v3

This adds trends widgets in the new generic performance widget format. Clicking on the transaction will bring you to the trends page scope to that transaction, which should help discovery of trends as a feature. Made some modifications to the trends chart and query to support alternate use, and moved a few shared functions.

Other:
- Did some light fixes for multi-breakpoints, will need to do more still since it's not great between wide screen and phone size.
Kev 3 years ago
parent
commit
22bd442eb3

+ 11 - 6
static/app/utils/performance/trends/trendsDiscoverQuery.tsx

@@ -7,6 +7,7 @@ import GenericDiscoverQuery, {
 import withApi from 'app/utils/withApi';
 import {
   TrendChangeType,
+  TrendFunctionField,
   TrendsData,
   TrendsDataEvents,
   TrendsQuery,
@@ -20,17 +21,21 @@ import {
 
 export type TrendsRequest = {
   trendChangeType?: TrendChangeType;
+  trendFunctionField?: TrendFunctionField;
   eventView: Partial<TrendView>;
 };
 
 type RequestProps = DiscoverQueryProps & TrendsRequest;
 
-type ChildrenProps = Omit<GenericChildrenProps<TrendsData>, 'tableData'> & {
+export type TrendDiscoveryChildrenProps = Omit<
+  GenericChildrenProps<TrendsData>,
+  'tableData'
+> & {
   trendsData: TrendsData | null;
 };
 
 type Props = RequestProps & {
-  children: (props: ChildrenProps) => React.ReactNode;
+  children: (props: TrendDiscoveryChildrenProps) => React.ReactNode;
 };
 
 type EventChildrenProps = Omit<GenericChildrenProps<TrendsDataEvents>, 'tableData'> & {
@@ -44,13 +49,13 @@ type EventProps = RequestProps & {
 export function getTrendsRequestPayload(props: RequestProps) {
   const {eventView} = props;
   const apiPayload: TrendsQuery = eventView?.getEventsAPIPayload(props.location);
-  const trendFunction = getCurrentTrendFunction(props.location);
+  const trendFunction = getCurrentTrendFunction(props.location, props.trendFunctionField);
   const trendParameter = getCurrentTrendParameter(props.location);
   apiPayload.trendFunction = generateTrendFunctionAsString(
     trendFunction.field,
     trendParameter.column
   );
-  apiPayload.trendType = eventView?.trendType;
+  apiPayload.trendType = eventView?.trendType || props.trendChangeType;
   apiPayload.interval = eventView?.interval;
   apiPayload.middle = eventView?.middle;
   return apiPayload;
@@ -59,9 +64,9 @@ export function getTrendsRequestPayload(props: RequestProps) {
 function TrendsDiscoverQuery(props: Props) {
   return (
     <GenericDiscoverQuery<TrendsData, TrendsRequest>
+      {...props}
       route="events-trends-stats"
       getRequestPayload={getTrendsRequestPayload}
-      {...props}
     >
       {({tableData, ...rest}) => {
         return props.children({trendsData: tableData, ...rest});
@@ -73,9 +78,9 @@ function TrendsDiscoverQuery(props: Props) {
 function EventsDiscoverQuery(props: EventProps) {
   return (
     <GenericDiscoverQuery<TrendsDataEvents, TrendsRequest>
+      {...props}
       route="events-trends"
       getRequestPayload={getTrendsRequestPayload}
-      {...props}
     >
       {({tableData, ...rest}) => {
         return props.children({trendsData: tableData, ...rest});

+ 3 - 49
static/app/views/performance/content.tsx

@@ -21,19 +21,16 @@ import {GlobalSelection, Organization, Project} from 'app/types';
 import {trackAnalyticsEvent} from 'app/utils/analytics';
 import EventView from 'app/utils/discover/eventView';
 import {PerformanceEventViewProvider} from 'app/utils/performance/contexts/performanceEventViewContext';
-import {decodeScalar} from 'app/utils/queryString';
-import {MutableSearch} from 'app/utils/tokenizeSearch';
 import withApi from 'app/utils/withApi';
 import withGlobalSelection from 'app/utils/withGlobalSelection';
 import withOrganization from 'app/utils/withOrganization';
 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';
+import {addRoutePerformanceContext, handleTrendsClick} from './utils';
 
 type Props = {
   api: Client;
@@ -131,49 +128,6 @@ class PerformanceContent extends Component<Props, State> {
     });
   };
 
-  handleTrendsClick = () => {
-    const {location, organization} = this.props;
-
-    const newQuery = {
-      ...location.query,
-    };
-
-    const query = decodeScalar(location.query.query, '');
-    const conditions = new MutableSearch(query);
-
-    trackAnalyticsEvent({
-      eventKey: 'performance_views.change_view',
-      eventName: 'Performance Views: Change View',
-      organization_id: parseInt(organization.id, 10),
-      view_name: 'TRENDS',
-    });
-
-    const modifiedConditions = new MutableSearch([]);
-
-    if (conditions.hasFilter('tpm()')) {
-      modifiedConditions.setFilterValues('tpm()', conditions.getFilterValues('tpm()'));
-    } else {
-      modifiedConditions.setFilterValues('tpm()', ['>0.01']);
-    }
-    if (conditions.hasFilter('transaction.duration')) {
-      modifiedConditions.setFilterValues(
-        'transaction.duration',
-        conditions.getFilterValues('transaction.duration')
-      );
-    } else {
-      modifiedConditions.setFilterValues('transaction.duration', [
-        '>0',
-        `<${DEFAULT_MAX_DURATION}`,
-      ]);
-    }
-    newQuery.query = modifiedConditions.formatString();
-
-    browserHistory.push({
-      pathname: getPerformanceTrendsUrl(organization),
-      query: {...newQuery},
-    });
-  };
-
   shouldShowOnboarding() {
     const {projects, demoMode} = this.props;
     const {eventView} = this.state;
@@ -218,7 +172,7 @@ class PerformanceContent extends Component<Props, State> {
               <Button
                 priority="primary"
                 data-test-id="landing-header-trends"
-                onClick={this.handleTrendsClick}
+                onClick={() => handleTrendsClick(this.props)}
               >
                 {t('View Trends')}
               </Button>
@@ -259,7 +213,7 @@ class PerformanceContent extends Component<Props, State> {
         eventView={this.state.eventView}
         setError={this.setError}
         handleSearch={this.handleSearch}
-        handleTrendsClick={this.handleTrendsClick}
+        handleTrendsClick={() => handleTrendsClick(this.props)}
         shouldShowOnboarding={this.shouldShowOnboarding()}
         {...this.props}
       />

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

@@ -1,3 +1,4 @@
+import {ReactText} from 'react';
 import {browserHistory} from 'react-router';
 import {Location} from 'history';
 
@@ -60,6 +61,25 @@ export const LANDING_DISPLAYS = [
   },
 ];
 
+export function excludeTransaction(
+  transaction: string | ReactText,
+  props: {eventView: EventView; location: Location}
+) {
+  const {eventView, location} = props;
+
+  const searchConditions = new MutableSearch(eventView.query);
+  searchConditions.addFilterValues('!transaction', [`${transaction}`]);
+
+  browserHistory.push({
+    pathname: location.pathname,
+    query: {
+      ...location.query,
+      cursor: undefined,
+      query: searchConditions.formatString(),
+    },
+  });
+}
+
 export function getCurrentLandingDisplay(
   location: Location,
   projects: Project[],

+ 2 - 1
static/app/views/performance/landing/views/allTransactionsView.tsx

@@ -24,9 +24,10 @@ export function AllTransactionsView(props: BasePerformanceViewProps) {
       <DoubleChartRow
         {...props}
         allowedCharts={[
-          PerformanceWidgetSetting.TPM_AREA,
           PerformanceWidgetSetting.MOST_RELATED_ERRORS,
           PerformanceWidgetSetting.MOST_RELATED_ISSUES,
+          PerformanceWidgetSetting.MOST_IMPROVED,
+          PerformanceWidgetSetting.MOST_REGRESSED,
         ]}
       />
       <Table {...props} setError={usePageError().setPageError} />

+ 9 - 2
static/app/views/performance/landing/widgets/components/performanceWidget.tsx

@@ -68,7 +68,7 @@ function _DataDisplay<T extends WidgetDataConstraint>(
   props: GenericPerformanceWidgetProps<T> &
     WidgetDataProps<T> & {nextWidgetData: T; totalHeight: number}
 ) {
-  const {Visualizations, chartHeight, totalHeight, containerType} = props;
+  const {Visualizations, chartHeight, totalHeight, containerType, EmptyComponent} = props;
 
   const Container = getPerformanceWidgetContainer({
     containerType,
@@ -111,7 +111,14 @@ function _DataDisplay<T extends WidgetDataConstraint>(
             />
           </ContentContainer>
         ))}
-        emptyComponent={<Placeholder height={`${totalHeight - paddingOffset}px`} />}
+        loadingComponent={<Placeholder height={`${totalHeight - paddingOffset}px`} />}
+        emptyComponent={
+          EmptyComponent ? (
+            <EmptyComponent />
+          ) : (
+            <Placeholder height={`${totalHeight - paddingOffset}px`} />
+          )
+        }
       />
     </Container>
   );

+ 2 - 4
static/app/views/performance/landing/widgets/components/selectableList.tsx

@@ -53,10 +53,8 @@ export const RightAlignedCell = styled('div')`
 `;
 
 const ListItemContainer = styled('div')`
-  display: grid;
-  grid-template-columns: 24px auto 150px 30px;
-  grid-template-rows: repeat(2, auto);
-  grid-column-gap: ${space(1)};
+  display: flex;
+
   border-top: 1px solid ${p => p.theme.border};
   padding: ${space(1)} ${space(2)};
 `;

+ 2 - 1
static/app/views/performance/landing/widgets/components/widgetContainer.tsx

@@ -12,6 +12,7 @@ import {GenericPerformanceWidgetDataType} from '../types';
 import {PerformanceWidgetSetting, WIDGET_DEFINITIONS} from '../widgetDefinitions';
 import {LineChartListWidget} from '../widgets/lineChartListWidget';
 import {SingleFieldAreaWidget} from '../widgets/singleFieldAreaWidget';
+import {TrendsWidget} from '../widgets/trendsWidget';
 
 import {ChartRowProps} from './widgetChartRow';
 
@@ -89,7 +90,7 @@ const _WidgetContainer = (props: Props) => {
 
   switch (widgetProps.dataType) {
     case GenericPerformanceWidgetDataType.trends:
-      throw new Error('Trends not currently supported.');
+      return <TrendsWidget {...props} {...widgetProps} />;
     case GenericPerformanceWidgetDataType.area:
       return <SingleFieldAreaWidget {...props} {...widgetProps} />;
     case GenericPerformanceWidgetDataType.line_list:

+ 21 - 0
static/app/views/performance/landing/widgets/transforms/transformTrendsDiscover.tsx

@@ -0,0 +1,21 @@
+import {TrendDiscoveryChildrenProps} from 'app/utils/performance/trends/trendsDiscoverQuery';
+import {normalizeTrends} from 'app/views/performance/trends/utils';
+
+export function transformTrendsDiscover(_: any, props: TrendDiscoveryChildrenProps) {
+  const {trendsData} = props;
+  const events = trendsData
+    ? normalizeTrends((trendsData && trendsData.events && trendsData.events.data) || [])
+    : [];
+  return {
+    ...props,
+    data: trendsData,
+    hasData: !!trendsData?.events?.data.length,
+    loading: props.isLoading,
+    isLoading: props.isLoading,
+    isErrored: !!props.error,
+    errored: props.error,
+    statsData: trendsData ? trendsData.stats : {},
+    transactionsList: events && events.slice ? events.slice(0, 3) : [],
+    events,
+  };
+}

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

@@ -110,6 +110,8 @@ export type GenericPerformanceWidgetProps<T extends WidgetDataConstraint> = {
 
   // Components
   HeaderActions?: HeaderActions<T>;
+  EmptyComponent?: FunctionComponent<{height?: number}>;
+
   Queries: Queries<T>;
   Visualizations: Visualizations<T>;
 };

+ 17 - 21
static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx

@@ -1,5 +1,5 @@
-import {Fragment, FunctionComponent, ReactText, useMemo, useState} from 'react';
-import {browserHistory, withRouter} from 'react-router';
+import {Fragment, FunctionComponent, useMemo, useState} from 'react';
+import {withRouter} from 'react-router';
 import styled from '@emotion/styled';
 import {Location} from 'history';
 
@@ -7,8 +7,10 @@ import _EventsRequest from 'app/components/charts/eventsRequest';
 import {getInterval} from 'app/components/charts/utils';
 import Link from 'app/components/links/link';
 import Tooltip from 'app/components/tooltip';
+import Truncate from 'app/components/truncate';
 import {IconClose} from 'app/icons';
 import {t} from 'app/locale';
+import space from 'app/styles/space';
 import {Organization} from 'app/types';
 import DiscoverQuery from 'app/utils/discover/discoverQuery';
 import EventView from 'app/utils/discover/eventView';
@@ -17,6 +19,7 @@ import withApi from 'app/utils/withApi';
 import _DurationChart from 'app/views/performance/charts/chart';
 import {transactionSummaryRouteWithQuery} from 'app/views/performance/transactionSummary/utils';
 
+import {excludeTransaction} from '../../utils';
 import {GenericPerformanceWidget} from '../components/performanceWidget';
 import SelectableList, {RightAlignedCell} from '../components/selectableList';
 import {transformDiscoverToList} from '../transforms/transformDiscoverToList';
@@ -43,22 +46,6 @@ type DataType = {
   list: WidgetDataResult & ReturnType<typeof transformDiscoverToList>;
 };
 
-function excludeTransaction(transaction: string | ReactText, props: Props) {
-  const {eventView, location} = props;
-
-  const searchConditions = new MutableSearch(eventView.query);
-  searchConditions.addFilterValues('!transaction', [`${transaction}`]);
-
-  browserHistory.push({
-    pathname: location.pathname,
-    query: {
-      ...location.query,
-      cursor: undefined,
-      query: searchConditions.formatString(),
-    },
-  });
-}
-
 export function LineChartListWidget(props: Props) {
   const [selectedListIndex, setSelectListIndex] = useState<number>(0);
   const {ContainerActions} = props;
@@ -190,17 +177,20 @@ export function LineChartListWidget(props: Props) {
               selectedIndex={selectedListIndex}
               setSelectedIndex={setSelectListIndex}
               items={provided.widgetData.list.data.map(listItem => () => {
+                const transaction = listItem.transaction as string;
                 const transactionTarget = transactionSummaryRouteWithQuery({
                   orgSlug: props.organization.slug,
                   projectID: listItem['project.id'] as string,
-                  transaction: listItem.transaction as string,
+                  transaction,
                   query: props.eventView.getGlobalSelectionQuery(),
                 });
                 switch (props.chartSetting) {
                   case PerformanceWidgetSetting.MOST_RELATED_ISSUES:
                     return (
                       <Fragment>
-                        <Link to={transactionTarget}>{listItem.transaction}</Link>
+                        <GrowLink to={transactionTarget}>
+                          <Truncate value={transaction} maxLength={40} />
+                        </GrowLink>
                         <RightAlignedCell>
                           <Tooltip title={listItem.title}>
                             <Link
@@ -222,7 +212,9 @@ export function LineChartListWidget(props: Props) {
                   default:
                     return (
                       <Fragment>
-                        <Link to={transactionTarget}>{listItem.transaction}</Link>
+                        <GrowLink to={transactionTarget}>
+                          <Truncate value={transaction} maxLength={40} />
+                        </GrowLink>
                         <RightAlignedCell>{listItem.failure_count}</RightAlignedCell>
                         <CloseContainer>
                           <StyledIconClose
@@ -255,6 +247,10 @@ const CloseContainer = styled('div')`
   display: flex;
   align-items: center;
   justify-content: center;
+  padding-left: ${space(1)};
+`;
+const GrowLink = styled(Link)`
+  flex-grow: 1;
 `;
 
 const StyledIconClose = styled(IconClose)`

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