Browse Source

feat(ddm): Multiple focused series (#64855)

- closes https://github.com/getsentry/sentry/issues/63599
ArthurKnaus 1 year ago
parent
commit
867e41a366

+ 6 - 4
static/app/utils/metrics/types.tsx

@@ -15,12 +15,14 @@ export type SortState = {
   order: 'asc' | 'desc';
 };
 
+export interface FocusedMetricsSeries {
+  seriesName: string;
+  groupBy?: Record<string, string>;
+}
+
 export interface MetricWidgetQueryParams extends MetricsQuerySubject {
   displayType: MetricDisplayType;
-  focusedSeries?: {
-    seriesName: string;
-    groupBy?: Record<string, string>;
-  };
+  focusedSeries?: FocusedMetricsSeries[];
   highlightedSample?: string | null;
   powerUserMode?: boolean;
   showSummaryTable?: boolean;

+ 15 - 3
static/app/views/ddm/summaryTable.tsx

@@ -14,7 +14,7 @@ import {trackAnalytics} from 'sentry/utils/analytics';
 import {getUtcDateString} from 'sentry/utils/dates';
 import {DEFAULT_SORT_STATE} from 'sentry/utils/metrics/constants';
 import {formatMetricsUsingUnitAndOp} from 'sentry/utils/metrics/formatters';
-import type {MetricWidgetQueryParams, SortState} from 'sentry/utils/metrics/types';
+import type {FocusedMetricsSeries, SortState} from 'sentry/utils/metrics/types';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
 import type {Series} from 'sentry/views/ddm/widget';
@@ -24,13 +24,15 @@ export const SummaryTable = memo(function SummaryTable({
   series,
   operation,
   onRowClick,
+  onColorDotClick,
   onSortChange,
   sort = DEFAULT_SORT_STATE as SortState,
   setHoveredSeries,
 }: {
-  onRowClick: (series: MetricWidgetQueryParams['focusedSeries']) => void;
+  onRowClick: (series: FocusedMetricsSeries) => void;
   onSortChange: (sortState: SortState) => void;
   series: Series[];
+  onColorDotClick?: (series: FocusedMetricsSeries) => void;
   operation?: string;
   setHoveredSeries?: (seriesName: string) => void;
   sort?: SortState;
@@ -196,7 +198,17 @@ export const SummaryTable = memo(function SummaryTable({
                     }
                   }}
                 >
-                  <Cell>
+                  <Cell
+                    onClick={event => {
+                      event.stopPropagation();
+                      if (hasMultipleSeries) {
+                        onColorDotClick?.({
+                          seriesName,
+                          groupBy,
+                        });
+                      }
+                    }}
+                  >
                     <ColorDot
                       color={color}
                       isHidden={!!hidden}

+ 24 - 0
static/app/views/ddm/utils/index.tsx

@@ -0,0 +1,24 @@
+import {BooleanOperator} from 'sentry/components/searchSyntax/parser';
+import type {MetricWidgetQueryParams} from 'sentry/utils/metrics/types';
+
+function constructQueryString(queryObject: Record<string, string>) {
+  return Object.entries(queryObject)
+    .map(([key, value]) => `${key}:"${value}"`)
+    .join(' ');
+}
+
+export function getQueryWithFocusedSeries(widget: MetricWidgetQueryParams) {
+  const focusedSeriesQuery = widget.focusedSeries
+    ?.map(series => {
+      if (!series.groupBy) {
+        return '';
+      }
+      return `(${constructQueryString(series.groupBy)})`;
+    })
+    .filter(Boolean)
+    .join(` ${BooleanOperator.OR} `);
+
+  return focusedSeriesQuery
+    ? `${widget.query} (${focusedSeriesQuery})`.trim()
+    : widget.query;
+}

+ 68 - 26
static/app/views/ddm/widget.tsx

@@ -25,6 +25,7 @@ import {
 import {metricDisplayTypeOptions} from 'sentry/utils/metrics/constants';
 import {parseMRI} from 'sentry/utils/metrics/mri';
 import type {
+  FocusedMetricsSeries,
   MetricCorrelation,
   MetricWidgetQueryParams,
 } from 'sentry/utils/metrics/types';
@@ -37,6 +38,7 @@ import type {FocusAreaProps} from 'sentry/views/ddm/context';
 import {createChartPalette} from 'sentry/views/ddm/metricsChartPalette';
 import {QuerySymbol} from 'sentry/views/ddm/querySymbol';
 import {SummaryTable} from 'sentry/views/ddm/summaryTable';
+import {getQueryWithFocusedSeries} from 'sentry/views/ddm/utils';
 
 import {DDM_CHART_GROUP, MIN_WIDGET_WIDTH} from './constants';
 
@@ -64,12 +66,6 @@ export type Sample = {
   transactionSpanId: string;
 };
 
-const constructQueryString = (queryObject: Record<string, string>) => {
-  return Object.entries(queryObject)
-    .map(([key, value]) => `${key}:"${value}"`)
-    .join(' ');
-};
-
 export const MetricWidget = memo(
   ({
     widget,
@@ -130,11 +126,14 @@ export const MetricWidget = memo(
       onChange(index, {displayType: value});
     };
 
+    const queryWithFocusedSeries = useMemo(
+      () => getQueryWithFocusedSeries(widget),
+      [widget]
+    );
+
     const samplesQuery = useMetricSamples(metricsQuery.mri, {
       ...focusArea?.selection?.range,
-      query: widget?.focusedSeries?.groupBy
-        ? `${widget.query} ${constructQueryString(widget.focusedSeries.groupBy)}`.trim()
-        : widget?.query,
+      query: queryWithFocusedSeries,
     });
 
     const samples = useMemo(() => {
@@ -272,26 +271,68 @@ const MetricWidgetBody = memo(
       });
     }, []);
 
-    const toggleSeriesVisibility = useCallback(
-      (series: MetricWidgetQueryParams['focusedSeries']) => {
-        setHoveredSeries('');
-        onChange?.({
-          focusedSeries:
-            focusedSeries?.seriesName === series?.seriesName ? undefined : series,
-        });
-      },
-      [focusedSeries, onChange, setHoveredSeries]
-    );
-
     const chartSeries = useMemo(() => {
       return timeseriesData
         ? getChartTimeseries(timeseriesData, {
             getChartPalette,
             mri,
-            focusedSeries: focusedSeries?.seriesName,
+            focusedSeries:
+              focusedSeries && new Set(focusedSeries?.map(s => s.seriesName)),
           })
         : [];
-    }, [timeseriesData, getChartPalette, mri, focusedSeries?.seriesName]);
+    }, [timeseriesData, getChartPalette, mri, focusedSeries]);
+
+    const toggleSeriesVisibility = useCallback(
+      (series: FocusedMetricsSeries) => {
+        setHoveredSeries('');
+
+        // The focused series array is not populated yet, so we can add all series except the one that was de-selected
+        if (!focusedSeries || focusedSeries.length === 0) {
+          onChange?.({
+            focusedSeries: chartSeries
+              .filter(s => s.seriesName !== series.seriesName)
+              .map(s => ({
+                seriesName: s.seriesName,
+                groupBy: s.groupBy,
+              })),
+          });
+          return;
+        }
+
+        const filteredSeries = focusedSeries.filter(
+          s => s.seriesName !== series.seriesName
+        );
+
+        if (filteredSeries.length === focusedSeries.length) {
+          // The series was not focused before so we can add it
+          filteredSeries.push(series);
+        }
+
+        onChange?.({
+          focusedSeries: filteredSeries,
+        });
+      },
+      [chartSeries, focusedSeries, onChange, setHoveredSeries]
+    );
+
+    const setSeriesVisibility = useCallback(
+      (series: FocusedMetricsSeries) => {
+        setHoveredSeries('');
+        if (
+          focusedSeries?.length === 1 &&
+          focusedSeries[0].seriesName === series.seriesName
+        ) {
+          onChange?.({
+            focusedSeries: [],
+          });
+          return;
+        }
+        onChange?.({
+          focusedSeries: [series],
+        });
+      },
+      [focusedSeries, onChange, setHoveredSeries]
+    );
 
     const handleSortChange = useCallback(
       newSort => {
@@ -345,8 +386,9 @@ const MetricWidgetBody = memo(
             onSortChange={handleSortChange}
             sort={sort}
             operation={metricsQuery.op}
-            onRowClick={toggleSeriesVisibility}
-            setHoveredSeries={focusedSeries ? undefined : setHoveredSeries}
+            onRowClick={setSeriesVisibility}
+            onColorDotClick={toggleSeriesVisibility}
+            setHoveredSeries={setHoveredSeries}
           />
         )}
       </StyledMetricWidgetBody>
@@ -363,7 +405,7 @@ export function getChartTimeseries(
   }: {
     getChartPalette: (seriesNames: string[]) => Record<string, string>;
     mri: MRI;
-    focusedSeries?: string;
+    focusedSeries?: Set<string>;
   }
 ) {
   // this assumes that all series have the same unit
@@ -387,7 +429,7 @@ export function getChartTimeseries(
     groupBy: item.groupBy,
     unit,
     color: chartPalette[item.name],
-    hidden: focusedSeries && focusedSeries !== item.name,
+    hidden: focusedSeries && focusedSeries.size > 0 && !focusedSeries.has(item.name),
     data: item.values.map((value, index) => ({
       name: moment(data.intervals[index]).valueOf(),
       value,

+ 9 - 15
static/app/views/ddm/widgetDetails.tsx

@@ -1,4 +1,4 @@
-import {useCallback, useState} from 'react';
+import {useCallback, useMemo, useState} from 'react';
 import styled from '@emotion/styled';
 
 import {TabList, TabPanels, Tabs} from 'sentry/components/tabs';
@@ -12,24 +12,19 @@ import useOrganization from 'sentry/utils/useOrganization';
 import {CodeLocations} from 'sentry/views/ddm/codeLocations';
 import {useDDMContext} from 'sentry/views/ddm/context';
 import {SampleTable} from 'sentry/views/ddm/sampleTable';
+import {getQueryWithFocusedSeries} from 'sentry/views/ddm/utils';
 
 enum Tab {
   SAMPLES = 'samples',
   CODE_LOCATIONS = 'codeLocations',
 }
 
-const constructQueryString = (queryObject: Record<string, string>) => {
-  return Object.entries(queryObject)
-    .map(([key, value]) => `${key}:"${value}"`)
-    .join(' ');
-};
-
 export function WidgetDetails() {
   const organization = useOrganization();
   const {selectedWidgetIndex, widgets, focusArea, setHighlightedSampleId} =
     useDDMContext();
   const [selectedTab, setSelectedTab] = useState(Tab.SAMPLES);
-  // the tray is minimized when the main content is maximized
+
   const selectedWidget = widgets[selectedWidgetIndex] as
     | MetricWidgetQueryParams
     | undefined;
@@ -40,6 +35,11 @@ export function WidgetDetails() {
     setSelectedTab(Tab.SAMPLES);
   }
 
+  const queryWithFocusedSeries = useMemo(
+    () => selectedWidget && getQueryWithFocusedSeries(selectedWidget),
+    [selectedWidget]
+  );
+
   const handleSampleRowHover = useCallback(
     (sampleId?: string) => {
       setHighlightedSampleId(sampleId);
@@ -84,13 +84,7 @@ export function WidgetDetails() {
             <TabPanels.Item key={Tab.SAMPLES}>
               <SampleTable
                 mri={selectedWidget?.mri}
-                query={
-                  selectedWidget?.focusedSeries?.groupBy
-                    ? `${selectedWidget.query} ${constructQueryString(
-                        selectedWidget.focusedSeries.groupBy
-                      )}`.trim()
-                    : selectedWidget?.query
-                }
+                query={queryWithFocusedSeries}
                 {...focusArea?.selection?.range}
                 onRowHover={handleSampleRowHover}
               />