Browse Source

feat(perf): Add landing widgets for most issues and errors (#29209)

This adds some list components and a new widget type for chart/list hybrid widgets. It's called lineChartListWidget currently, but is using the area widget for now since it's already setup with previous periods etc.
Kev 3 years ago
parent
commit
a58a275f8e

+ 4 - 0
static/app/views/performance/data.tsx

@@ -50,6 +50,8 @@ export enum PERFORMANCE_TERM {
   SLOW_FRAMES = 'slowFrames',
   FROZEN_FRAMES = 'frozenFrames',
   STALL_PERCENTAGE = 'stallPercentage',
+  MOST_ISSUES = 'mostIssues',
+  MOST_ERRORS = 'mostErrors',
 }
 
 export type TooltipOption = SelectValue<string> & {
@@ -357,6 +359,8 @@ export const PERFORMANCE_TERMS: Record<PERFORMANCE_TERM, TermFormatter> = {
     t('Warm start is a measure of the application start up time while still in memory.'),
   slowFrames: () => t('The count of the number of slow frames in the transaction.'),
   frozenFrames: () => t('The count of the number of frozen frames in the transaction.'),
+  mostErrors: () => t('Transactions with the most associated errors.'),
+  mostIssues: () => t('The most instances of an issue for a related transaction.'),
   stallPercentage: () =>
     t(
       'The percentage of the transaction duration in which the application is in a stalled state.'

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

@@ -24,12 +24,9 @@ export function AllTransactionsView(props: BasePerformanceViewProps) {
       <DoubleChartRow
         {...props}
         allowedCharts={[
-          // TODO(k-fish): Temporarily adding extra charts here while trends widgets are in progress.
           PerformanceWidgetSetting.TPM_AREA,
-          PerformanceWidgetSetting.TPM_AREA,
-          PerformanceWidgetSetting.TPM_AREA,
-          PerformanceWidgetSetting.MOST_IMPROVED,
-          PerformanceWidgetSetting.MOST_REGRESSED,
+          PerformanceWidgetSetting.MOST_RELATED_ERRORS,
+          PerformanceWidgetSetting.MOST_RELATED_ISSUES,
         ]}
       />
       <Table {...props} setError={usePageError().setPageError} />

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

@@ -26,16 +26,21 @@ export function GenericPerformanceWidget<T extends WidgetDataConstraint>(
   props: WidgetPropUnion<T>
 ) {
   const [widgetData, setWidgetData] = useState<T>({} as T);
+  const [nextWidgetData, setNextWidgetData] = useState<T>({} as T);
 
   const setWidgetDataForKey = useCallback(
     (dataKey: string, result?: WidgetDataResult) => {
       if (result) {
+        setNextWidgetData({...nextWidgetData, [dataKey]: result});
+      }
+      if (result?.hasData || result?.isErrored) {
+        setNextWidgetData({...nextWidgetData, [dataKey]: result});
         setWidgetData({...widgetData, [dataKey]: result});
       }
     },
-    [setWidgetData]
+    [widgetData, nextWidgetData, setWidgetData, setNextWidgetData]
   );
-  const widgetProps = {widgetData, setWidgetDataForKey};
+  const widgetProps = {widgetData, nextWidgetData, setWidgetDataForKey};
 
   const queries = Object.entries(props.Queries).map(([key, definition]) => ({
     ...definition,
@@ -44,6 +49,8 @@ export function GenericPerformanceWidget<T extends WidgetDataConstraint>(
 
   const api = useApi();
 
+  const totalHeight = props.Visualizations.reduce((acc, curr) => acc + curr.height, 0);
+
   return (
     <Fragment>
       <QueryHandler
@@ -53,28 +60,34 @@ export function GenericPerformanceWidget<T extends WidgetDataConstraint>(
         queries={queries}
         api={api}
       />
-      <_DataDisplay<T> {...props} {...widgetProps} />
+      <_DataDisplay<T> {...props} {...widgetProps} totalHeight={totalHeight} />
     </Fragment>
   );
 }
 
 function _DataDisplay<T extends WidgetDataConstraint>(
-  props: GenericPerformanceWidgetProps<T> & WidgetDataProps<T>
+  props: GenericPerformanceWidgetProps<T> &
+    WidgetDataProps<T> & {nextWidgetData: T; totalHeight: number}
 ) {
-  const {Visualizations, chartHeight, containerType} = props;
+  const {Visualizations, chartHeight, totalHeight, containerType} = props;
 
   const Container = getPerformanceWidgetContainer({
     containerType,
   });
 
-  const missingDataKeys = !Object.values(props.widgetData).length;
+  const numberKeys = Object.keys(props.Queries).length;
+  const missingDataKeys = Object.values(props.widgetData).length !== numberKeys;
+  const missingNextDataKeys = Object.values(props.nextWidgetData).length !== numberKeys;
   const hasData =
     !missingDataKeys && Object.values(props.widgetData).every(d => !d || d.hasData);
   const isLoading =
-    !missingDataKeys && Object.values(props.widgetData).some(d => !d || d.isLoading);
+    !missingNextDataKeys &&
+    Object.values(props.nextWidgetData).some(d => !d || d.isLoading);
   const isErrored =
     !missingDataKeys && Object.values(props.widgetData).some(d => d && d.isErrored);
 
+  const paddingOffset = 32; // space(2) * 2;
+
   return (
     <Container data-test-id="performance-widget-container">
       <ContentContainer>
@@ -84,7 +97,7 @@ function _DataDisplay<T extends WidgetDataConstraint>(
         isLoading={isLoading}
         isErrored={isErrored}
         hasData={hasData}
-        errorComponent={<DefaultErrorComponent height={chartHeight} />}
+        errorComponent={<DefaultErrorComponent height={totalHeight - paddingOffset} />}
         dataComponents={Visualizations.map((Visualization, index) => (
           <ContentContainer
             key={index}
@@ -99,7 +112,7 @@ function _DataDisplay<T extends WidgetDataConstraint>(
             />
           </ContentContainer>
         ))}
-        emptyComponent={<Placeholder height={`${chartHeight}px`} />}
+        emptyComponent={<Placeholder height={`${totalHeight - paddingOffset}px`} />}
       />
     </Container>
   );

+ 5 - 0
static/app/views/performance/landing/widgets/components/queryHandler.tsx

@@ -39,6 +39,11 @@ export function QueryHandler<T extends WidgetDataConstraint>(
             project={globalSelection.projects}
             environment={globalSelection.environments}
             organization={props.queryProps.organization}
+            orgSlug={props.queryProps.organization.slug}
+            query={props.queryProps.eventView.getQueryWithAdditionalConditions()}
+            eventView={props.queryProps.eventView}
+            location={props.queryProps.location}
+            widgetData={props.widgetData}
           >
             {results => {
               return (

+ 72 - 0
static/app/views/performance/landing/widgets/components/selectableList.tsx

@@ -0,0 +1,72 @@
+import React, {ReactNode} from 'react';
+import styled from '@emotion/styled';
+
+import Radio from 'app/components/radio';
+import space from 'app/styles/space';
+import {RadioLineItem} from 'app/views/settings/components/forms/controls/radioGroup';
+
+type Props = {
+  selectedIndex: number;
+  setSelectedIndex: (index: number) => void;
+  items: (() => ReactNode)[];
+  radioColor?: string;
+};
+
+export default function SelectableList(props: Props) {
+  return (
+    <div>
+      {props.items.map((item, index) => (
+        <SelectableItem
+          {...props}
+          isSelected={index === props.selectedIndex}
+          currentIndex={index}
+          key={index}
+        >
+          {item()}
+        </SelectableItem>
+      ))}
+    </div>
+  );
+}
+
+function SelectableItem({
+  isSelected,
+  currentIndex: index,
+  children,
+  setSelectedIndex,
+  radioColor,
+}: {isSelected: boolean; currentIndex: number; children: React.ReactNode} & Props) {
+  return (
+    <ListItemContainer>
+      <ItemRadioContainer color={radioColor ?? ''}>
+        <RadioLineItem index={index} role="radio">
+          <Radio checked={isSelected} onChange={() => setSelectedIndex(index)} />
+        </RadioLineItem>
+      </ItemRadioContainer>
+      {children}
+    </ListItemContainer>
+  );
+}
+
+export const RightAlignedCell = styled('div')`
+  text-align: right;
+`;
+
+const ListItemContainer = styled('div')`
+  display: grid;
+  grid-template-columns: 24px auto 150px 30px;
+  grid-template-rows: repeat(2, auto);
+  grid-column-gap: ${space(1)};
+  border-top: 1px solid ${p => p.theme.border};
+  padding: ${space(1)} ${space(2)};
+`;
+
+const ItemRadioContainer = styled('div')`
+  grid-row: 1/3;
+  input {
+    cursor: pointer;
+  }
+  input:checked::after {
+    background-color: ${p => p.color};
+  }
+`;

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

@@ -47,14 +47,14 @@ export const TripleChartRow = (props: ChartRowProps) => <ChartRow {...props} />;
 
 TripleChartRow.defaultProps = {
   chartCount: 3,
-  chartHeight: 160,
+  chartHeight: 120,
 };
 
 export const DoubleChartRow = (props: ChartRowProps) => <ChartRow {...props} />;
 
 DoubleChartRow.defaultProps = {
   chartCount: 2,
-  chartHeight: 300,
+  chartHeight: 220,
 };
 
 const StyledRow = styled(PerformanceLayoutBodyRow)`

+ 4 - 0
static/app/views/performance/landing/widgets/components/widgetContainer.tsx

@@ -10,6 +10,7 @@ import ContextMenu from 'app/views/dashboardsV2/contextMenu';
 
 import {GenericPerformanceWidgetDataType} from '../types';
 import {PerformanceWidgetSetting, WIDGET_DEFINITIONS} from '../widgetDefinitions';
+import {LineChartListWidget} from '../widgets/lineChartListWidget';
 import {SingleFieldAreaWidget} from '../widgets/singleFieldAreaWidget';
 
 import {ChartRowProps} from './widgetChartRow';
@@ -75,6 +76,7 @@ const _WidgetContainer = (props: Props) => {
   };
 
   const widgetProps = {
+    chartSetting,
     ...WIDGET_DEFINITIONS({organization})[chartSetting],
     ContainerActions: containerProps => (
       <WidgetContainerActions
@@ -90,6 +92,8 @@ const _WidgetContainer = (props: Props) => {
       throw new Error('Trends not currently supported.');
     case GenericPerformanceWidgetDataType.area:
       return <SingleFieldAreaWidget {...props} {...widgetProps} />;
+    case GenericPerformanceWidgetDataType.line_list:
+      return <LineChartListWidget {...props} {...widgetProps} />;
     default:
       throw new Error(`Widget type "${widgetProps.dataType}" has no implementation.`);
   }

+ 31 - 0
static/app/views/performance/landing/widgets/transforms/transformDiscoverToList.tsx

@@ -0,0 +1,31 @@
+import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams';
+import {defined} from 'app/utils';
+import {TableData} from 'app/utils/discover/discoverQuery';
+import {GenericChildrenProps} from 'app/utils/discover/genericDiscoverQuery';
+
+import {QueryDefinitionWithKey, WidgetDataConstraint, WidgetPropUnion} from '../types';
+
+export function transformDiscoverToList<T extends WidgetDataConstraint>(
+  widgetProps: WidgetPropUnion<T>,
+  results: GenericChildrenProps<TableData>,
+  _: QueryDefinitionWithKey<T>
+) {
+  const {start, end, utc, interval, statsPeriod} = getParams(widgetProps.location.query);
+
+  const data = results.tableData?.data ?? [];
+
+  const childData = {
+    ...results,
+    isErrored: !!results.error,
+    hasData: defined(data) && !!data.length,
+    data,
+
+    utc: utc === 'true',
+    interval,
+    statsPeriod: statsPeriod ?? undefined,
+    start: start ?? '',
+    end: end ?? '',
+  };
+
+  return childData;
+}

+ 1 - 1
static/app/views/performance/landing/widgets/transforms/transformEventsToArea.tsx

@@ -31,7 +31,7 @@ export function transformEventsRequestToArea<T extends WidgetDataConstraint>(
     ...results,
     isLoading: results.loading,
     isErrored: results.errored,
-    hasData: defined(data) && data.length && !!data[0].data.length,
+    hasData: defined(data) && !!data.length && !!data[0].data.length,
     data,
     dataMean,
     previousData: results.previousTimeseriesData ?? undefined,

+ 10 - 4
static/app/views/performance/landing/widgets/types.tsx

@@ -4,7 +4,7 @@ import {Location} from 'history';
 import {Client} from 'app/api';
 import BaseChart from 'app/components/charts/baseChart';
 import {RenderProps} from 'app/components/charts/eventsRequest';
-import {DateString, Organization} from 'app/types';
+import {DateString, Organization, OrganizationSummary} from 'app/types';
 import EventView from 'app/utils/discover/eventView';
 
 import {PerformanceWidgetContainerTypes} from './components/performanceWidgetContainer';
@@ -20,6 +20,7 @@ export enum GenericPerformanceWidgetDataType {
   histogram = 'histogram',
   area = 'area',
   vitals = 'vitals',
+  line_list = 'line_list',
   trends = 'trends',
 }
 
@@ -35,7 +36,7 @@ export interface WidgetDataConstraint {
 export type QueryChildren = {
   children: (props: any) => ReactNode; // TODO(k-fish): Fix any type.
 };
-export type QueryFC = FunctionComponent<
+export type QueryFC<T extends WidgetDataConstraint> = FunctionComponent<
   QueryChildren & {
     fields?: string | string[];
     yAxis?: string | string[];
@@ -45,7 +46,12 @@ export type QueryFC = FunctionComponent<
     project?: Readonly<number[]>;
     environment?: Readonly<string[]>;
     team?: Readonly<string | string[]>;
-    organization?: Organization;
+    query?: string;
+    orgSlug: string;
+    location: Location;
+    organization: OrganizationSummary;
+    eventView: EventView;
+    widgetData: T;
   }
 >;
 
@@ -53,7 +59,7 @@ export type QueryDefinition<
   T extends WidgetDataConstraint,
   S extends WidgetDataResult | undefined
 > = {
-  component: QueryFC;
+  component: QueryFC<T>;
   fields: string | string[];
   enabled?: (data: T) => boolean;
   transform: (

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