Просмотр исходного кода

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 лет назад
Родитель
Сommit
22bd442eb3

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

@@ -7,6 +7,7 @@ import GenericDiscoverQuery, {
 import withApi from 'app/utils/withApi';
 import withApi from 'app/utils/withApi';
 import {
 import {
   TrendChangeType,
   TrendChangeType,
+  TrendFunctionField,
   TrendsData,
   TrendsData,
   TrendsDataEvents,
   TrendsDataEvents,
   TrendsQuery,
   TrendsQuery,
@@ -20,17 +21,21 @@ import {
 
 
 export type TrendsRequest = {
 export type TrendsRequest = {
   trendChangeType?: TrendChangeType;
   trendChangeType?: TrendChangeType;
+  trendFunctionField?: TrendFunctionField;
   eventView: Partial<TrendView>;
   eventView: Partial<TrendView>;
 };
 };
 
 
 type RequestProps = DiscoverQueryProps & TrendsRequest;
 type RequestProps = DiscoverQueryProps & TrendsRequest;
 
 
-type ChildrenProps = Omit<GenericChildrenProps<TrendsData>, 'tableData'> & {
+export type TrendDiscoveryChildrenProps = Omit<
+  GenericChildrenProps<TrendsData>,
+  'tableData'
+> & {
   trendsData: TrendsData | null;
   trendsData: TrendsData | null;
 };
 };
 
 
 type Props = RequestProps & {
 type Props = RequestProps & {
-  children: (props: ChildrenProps) => React.ReactNode;
+  children: (props: TrendDiscoveryChildrenProps) => React.ReactNode;
 };
 };
 
 
 type EventChildrenProps = Omit<GenericChildrenProps<TrendsDataEvents>, 'tableData'> & {
 type EventChildrenProps = Omit<GenericChildrenProps<TrendsDataEvents>, 'tableData'> & {
@@ -44,13 +49,13 @@ type EventProps = RequestProps & {
 export function getTrendsRequestPayload(props: RequestProps) {
 export function getTrendsRequestPayload(props: RequestProps) {
   const {eventView} = props;
   const {eventView} = props;
   const apiPayload: TrendsQuery = eventView?.getEventsAPIPayload(props.location);
   const apiPayload: TrendsQuery = eventView?.getEventsAPIPayload(props.location);
-  const trendFunction = getCurrentTrendFunction(props.location);
+  const trendFunction = getCurrentTrendFunction(props.location, props.trendFunctionField);
   const trendParameter = getCurrentTrendParameter(props.location);
   const trendParameter = getCurrentTrendParameter(props.location);
   apiPayload.trendFunction = generateTrendFunctionAsString(
   apiPayload.trendFunction = generateTrendFunctionAsString(
     trendFunction.field,
     trendFunction.field,
     trendParameter.column
     trendParameter.column
   );
   );
-  apiPayload.trendType = eventView?.trendType;
+  apiPayload.trendType = eventView?.trendType || props.trendChangeType;
   apiPayload.interval = eventView?.interval;
   apiPayload.interval = eventView?.interval;
   apiPayload.middle = eventView?.middle;
   apiPayload.middle = eventView?.middle;
   return apiPayload;
   return apiPayload;
@@ -59,9 +64,9 @@ export function getTrendsRequestPayload(props: RequestProps) {
 function TrendsDiscoverQuery(props: Props) {
 function TrendsDiscoverQuery(props: Props) {
   return (
   return (
     <GenericDiscoverQuery<TrendsData, TrendsRequest>
     <GenericDiscoverQuery<TrendsData, TrendsRequest>
+      {...props}
       route="events-trends-stats"
       route="events-trends-stats"
       getRequestPayload={getTrendsRequestPayload}
       getRequestPayload={getTrendsRequestPayload}
-      {...props}
     >
     >
       {({tableData, ...rest}) => {
       {({tableData, ...rest}) => {
         return props.children({trendsData: tableData, ...rest});
         return props.children({trendsData: tableData, ...rest});
@@ -73,9 +78,9 @@ function TrendsDiscoverQuery(props: Props) {
 function EventsDiscoverQuery(props: EventProps) {
 function EventsDiscoverQuery(props: EventProps) {
   return (
   return (
     <GenericDiscoverQuery<TrendsDataEvents, TrendsRequest>
     <GenericDiscoverQuery<TrendsDataEvents, TrendsRequest>
+      {...props}
       route="events-trends"
       route="events-trends"
       getRequestPayload={getTrendsRequestPayload}
       getRequestPayload={getTrendsRequestPayload}
-      {...props}
     >
     >
       {({tableData, ...rest}) => {
       {({tableData, ...rest}) => {
         return props.children({trendsData: 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 {trackAnalyticsEvent} from 'app/utils/analytics';
 import EventView from 'app/utils/discover/eventView';
 import EventView from 'app/utils/discover/eventView';
 import {PerformanceEventViewProvider} from 'app/utils/performance/contexts/performanceEventViewContext';
 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 withApi from 'app/utils/withApi';
 import withGlobalSelection from 'app/utils/withGlobalSelection';
 import withGlobalSelection from 'app/utils/withGlobalSelection';
 import withOrganization from 'app/utils/withOrganization';
 import withOrganization from 'app/utils/withOrganization';
 import withProjects from 'app/utils/withProjects';
 import withProjects from 'app/utils/withProjects';
 
 
 import LandingContent from './landing/content';
 import LandingContent from './landing/content';
-import {DEFAULT_MAX_DURATION} from './trends/utils';
 import {DEFAULT_STATS_PERIOD, generatePerformanceEventView} from './data';
 import {DEFAULT_STATS_PERIOD, generatePerformanceEventView} from './data';
 import {PerformanceLanding} from './landing';
 import {PerformanceLanding} from './landing';
 import Onboarding from './onboarding';
 import Onboarding from './onboarding';
-import {addRoutePerformanceContext, getPerformanceTrendsUrl} from './utils';
+import {addRoutePerformanceContext, handleTrendsClick} from './utils';
 
 
 type Props = {
 type Props = {
   api: Client;
   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() {
   shouldShowOnboarding() {
     const {projects, demoMode} = this.props;
     const {projects, demoMode} = this.props;
     const {eventView} = this.state;
     const {eventView} = this.state;
@@ -218,7 +172,7 @@ class PerformanceContent extends Component<Props, State> {
               <Button
               <Button
                 priority="primary"
                 priority="primary"
                 data-test-id="landing-header-trends"
                 data-test-id="landing-header-trends"
-                onClick={this.handleTrendsClick}
+                onClick={() => handleTrendsClick(this.props)}
               >
               >
                 {t('View Trends')}
                 {t('View Trends')}
               </Button>
               </Button>
@@ -259,7 +213,7 @@ class PerformanceContent extends Component<Props, State> {
         eventView={this.state.eventView}
         eventView={this.state.eventView}
         setError={this.setError}
         setError={this.setError}
         handleSearch={this.handleSearch}
         handleSearch={this.handleSearch}
-        handleTrendsClick={this.handleTrendsClick}
+        handleTrendsClick={() => handleTrendsClick(this.props)}
         shouldShowOnboarding={this.shouldShowOnboarding()}
         shouldShowOnboarding={this.shouldShowOnboarding()}
         {...this.props}
         {...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 {browserHistory} from 'react-router';
 import {Location} from 'history';
 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(
 export function getCurrentLandingDisplay(
   location: Location,
   location: Location,
   projects: Project[],
   projects: Project[],

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

@@ -24,9 +24,10 @@ export function AllTransactionsView(props: BasePerformanceViewProps) {
       <DoubleChartRow
       <DoubleChartRow
         {...props}
         {...props}
         allowedCharts={[
         allowedCharts={[
-          PerformanceWidgetSetting.TPM_AREA,
           PerformanceWidgetSetting.MOST_RELATED_ERRORS,
           PerformanceWidgetSetting.MOST_RELATED_ERRORS,
           PerformanceWidgetSetting.MOST_RELATED_ISSUES,
           PerformanceWidgetSetting.MOST_RELATED_ISSUES,
+          PerformanceWidgetSetting.MOST_IMPROVED,
+          PerformanceWidgetSetting.MOST_REGRESSED,
         ]}
         ]}
       />
       />
       <Table {...props} setError={usePageError().setPageError} />
       <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> &
   props: GenericPerformanceWidgetProps<T> &
     WidgetDataProps<T> & {nextWidgetData: T; totalHeight: number}
     WidgetDataProps<T> & {nextWidgetData: T; totalHeight: number}
 ) {
 ) {
-  const {Visualizations, chartHeight, totalHeight, containerType} = props;
+  const {Visualizations, chartHeight, totalHeight, containerType, EmptyComponent} = props;
 
 
   const Container = getPerformanceWidgetContainer({
   const Container = getPerformanceWidgetContainer({
     containerType,
     containerType,
@@ -111,7 +111,14 @@ function _DataDisplay<T extends WidgetDataConstraint>(
             />
             />
           </ContentContainer>
           </ContentContainer>
         ))}
         ))}
-        emptyComponent={<Placeholder height={`${totalHeight - paddingOffset}px`} />}
+        loadingComponent={<Placeholder height={`${totalHeight - paddingOffset}px`} />}
+        emptyComponent={
+          EmptyComponent ? (
+            <EmptyComponent />
+          ) : (
+            <Placeholder height={`${totalHeight - paddingOffset}px`} />
+          )
+        }
       />
       />
     </Container>
     </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')`
 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};
   border-top: 1px solid ${p => p.theme.border};
   padding: ${space(1)} ${space(2)};
   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 {PerformanceWidgetSetting, WIDGET_DEFINITIONS} from '../widgetDefinitions';
 import {LineChartListWidget} from '../widgets/lineChartListWidget';
 import {LineChartListWidget} from '../widgets/lineChartListWidget';
 import {SingleFieldAreaWidget} from '../widgets/singleFieldAreaWidget';
 import {SingleFieldAreaWidget} from '../widgets/singleFieldAreaWidget';
+import {TrendsWidget} from '../widgets/trendsWidget';
 
 
 import {ChartRowProps} from './widgetChartRow';
 import {ChartRowProps} from './widgetChartRow';
 
 
@@ -89,7 +90,7 @@ const _WidgetContainer = (props: Props) => {
 
 
   switch (widgetProps.dataType) {
   switch (widgetProps.dataType) {
     case GenericPerformanceWidgetDataType.trends:
     case GenericPerformanceWidgetDataType.trends:
-      throw new Error('Trends not currently supported.');
+      return <TrendsWidget {...props} {...widgetProps} />;
     case GenericPerformanceWidgetDataType.area:
     case GenericPerformanceWidgetDataType.area:
       return <SingleFieldAreaWidget {...props} {...widgetProps} />;
       return <SingleFieldAreaWidget {...props} {...widgetProps} />;
     case GenericPerformanceWidgetDataType.line_list:
     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
   // Components
   HeaderActions?: HeaderActions<T>;
   HeaderActions?: HeaderActions<T>;
+  EmptyComponent?: FunctionComponent<{height?: number}>;
+
   Queries: Queries<T>;
   Queries: Queries<T>;
   Visualizations: Visualizations<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 styled from '@emotion/styled';
 import {Location} from 'history';
 import {Location} from 'history';
 
 
@@ -7,8 +7,10 @@ import _EventsRequest from 'app/components/charts/eventsRequest';
 import {getInterval} from 'app/components/charts/utils';
 import {getInterval} from 'app/components/charts/utils';
 import Link from 'app/components/links/link';
 import Link from 'app/components/links/link';
 import Tooltip from 'app/components/tooltip';
 import Tooltip from 'app/components/tooltip';
+import Truncate from 'app/components/truncate';
 import {IconClose} from 'app/icons';
 import {IconClose} from 'app/icons';
 import {t} from 'app/locale';
 import {t} from 'app/locale';
+import space from 'app/styles/space';
 import {Organization} from 'app/types';
 import {Organization} from 'app/types';
 import DiscoverQuery from 'app/utils/discover/discoverQuery';
 import DiscoverQuery from 'app/utils/discover/discoverQuery';
 import EventView from 'app/utils/discover/eventView';
 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 _DurationChart from 'app/views/performance/charts/chart';
 import {transactionSummaryRouteWithQuery} from 'app/views/performance/transactionSummary/utils';
 import {transactionSummaryRouteWithQuery} from 'app/views/performance/transactionSummary/utils';
 
 
+import {excludeTransaction} from '../../utils';
 import {GenericPerformanceWidget} from '../components/performanceWidget';
 import {GenericPerformanceWidget} from '../components/performanceWidget';
 import SelectableList, {RightAlignedCell} from '../components/selectableList';
 import SelectableList, {RightAlignedCell} from '../components/selectableList';
 import {transformDiscoverToList} from '../transforms/transformDiscoverToList';
 import {transformDiscoverToList} from '../transforms/transformDiscoverToList';
@@ -43,22 +46,6 @@ type DataType = {
   list: WidgetDataResult & ReturnType<typeof transformDiscoverToList>;
   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) {
 export function LineChartListWidget(props: Props) {
   const [selectedListIndex, setSelectListIndex] = useState<number>(0);
   const [selectedListIndex, setSelectListIndex] = useState<number>(0);
   const {ContainerActions} = props;
   const {ContainerActions} = props;
@@ -190,17 +177,20 @@ export function LineChartListWidget(props: Props) {
               selectedIndex={selectedListIndex}
               selectedIndex={selectedListIndex}
               setSelectedIndex={setSelectListIndex}
               setSelectedIndex={setSelectListIndex}
               items={provided.widgetData.list.data.map(listItem => () => {
               items={provided.widgetData.list.data.map(listItem => () => {
+                const transaction = listItem.transaction as string;
                 const transactionTarget = transactionSummaryRouteWithQuery({
                 const transactionTarget = transactionSummaryRouteWithQuery({
                   orgSlug: props.organization.slug,
                   orgSlug: props.organization.slug,
                   projectID: listItem['project.id'] as string,
                   projectID: listItem['project.id'] as string,
-                  transaction: listItem.transaction as string,
+                  transaction,
                   query: props.eventView.getGlobalSelectionQuery(),
                   query: props.eventView.getGlobalSelectionQuery(),
                 });
                 });
                 switch (props.chartSetting) {
                 switch (props.chartSetting) {
                   case PerformanceWidgetSetting.MOST_RELATED_ISSUES:
                   case PerformanceWidgetSetting.MOST_RELATED_ISSUES:
                     return (
                     return (
                       <Fragment>
                       <Fragment>
-                        <Link to={transactionTarget}>{listItem.transaction}</Link>
+                        <GrowLink to={transactionTarget}>
+                          <Truncate value={transaction} maxLength={40} />
+                        </GrowLink>
                         <RightAlignedCell>
                         <RightAlignedCell>
                           <Tooltip title={listItem.title}>
                           <Tooltip title={listItem.title}>
                             <Link
                             <Link
@@ -222,7 +212,9 @@ export function LineChartListWidget(props: Props) {
                   default:
                   default:
                     return (
                     return (
                       <Fragment>
                       <Fragment>
-                        <Link to={transactionTarget}>{listItem.transaction}</Link>
+                        <GrowLink to={transactionTarget}>
+                          <Truncate value={transaction} maxLength={40} />
+                        </GrowLink>
                         <RightAlignedCell>{listItem.failure_count}</RightAlignedCell>
                         <RightAlignedCell>{listItem.failure_count}</RightAlignedCell>
                         <CloseContainer>
                         <CloseContainer>
                           <StyledIconClose
                           <StyledIconClose
@@ -255,6 +247,10 @@ const CloseContainer = styled('div')`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
+  padding-left: ${space(1)};
+`;
+const GrowLink = styled(Link)`
+  flex-grow: 1;
 `;
 `;
 
 
 const StyledIconClose = styled(IconClose)`
 const StyledIconClose = styled(IconClose)`

Некоторые файлы не были показаны из-за большого количества измененных файлов