Browse Source

feat(perf): Add button to reset min and max in span details page (#33507)

* add a zoom reset button

* add tests

* fix

* update

* use enums for min max zoomkeys

* update tests
Dameli Ushbayeva 2 years ago
parent
commit
c4aee3171a

+ 2 - 5
static/app/utils/performance/suspectSpans/spanExamplesQuery.tsx

@@ -28,14 +28,11 @@ function getSuspectSpanPayload(props: RequestProps) {
   const {spanOp, spanGroup} = props;
   const span =
     defined(spanOp) && defined(spanGroup) ? `${spanOp}:${spanGroup}` : undefined;
-  const payload = {span};
-  if (!defined(payload.span)) {
-    delete payload.span;
-  }
+  const payload = defined(span) ? {span} : {};
   const additionalPayload = omit(props.eventView.getEventsAPIPayload(props.location), [
     'field',
   ]);
-  return Object.assign(payload, additionalPayload);
+  return {...payload, ...additionalPayload};
 }
 
 function SuspectSpansQuery(props: Props) {

+ 1 - 3
static/app/views/performance/transactionSummary/transactionSpans/spanDetails/chart.tsx

@@ -17,13 +17,11 @@ import {Organization} from 'sentry/types';
 import {defined} from 'sentry/utils';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import EventView from 'sentry/utils/discover/eventView';
-import {removeHistogramQueryStrings} from 'sentry/utils/performance/histogram';
 import {SpanSlug} from 'sentry/utils/performance/suspectSpans/types';
 import {decodeScalar} from 'sentry/utils/queryString';
 
 import ExclusiveTimeHistogram from './exclusiveTimeHistogram';
 import ExclusiveTimeTimeSeries from './exclusiveTimeTimeSeries';
-import {MAX, MIN} from './utils';
 
 type Props = WithRouterProps & {
   eventView: EventView;
@@ -59,7 +57,7 @@ function Chart(props: Props) {
     browserHistory.push({
       pathname: location.pathname,
       query: {
-        ...removeHistogramQueryStrings(location, [MIN, MAX]),
+        ...location.query,
         display: value,
       },
     });

+ 2 - 2
static/app/views/performance/transactionSummary/transactionSpans/spanDetails/content.tsx

@@ -20,8 +20,8 @@ import {SpanSortOthers} from '../types';
 import {getTotalsView} from '../utils';
 
 import SpanChart from './chart';
+import SpanDetailsControls from './spanDetailsControls';
 import SpanDetailsHeader from './spanDetailsHeader';
-import SpanDetailsSearchBar from './spanDetailsSearchBar';
 import SpanTable from './spanDetailsTable';
 
 type Props = {
@@ -149,7 +149,7 @@ function SpanDetailsContent(props: ContentProps) {
         suspectSpan={suspectSpan}
       />
       <Feature features={['performance-span-histogram-view']}>
-        <SpanDetailsSearchBar
+        <SpanDetailsControls
           organization={organization}
           location={location}
           eventView={eventView}

+ 5 - 5
static/app/views/performance/transactionSummary/transactionSpans/spanDetails/exclusiveTimeHistogram.tsx

@@ -25,7 +25,7 @@ import {
 } from 'sentry/utils/performance/histogram/utils';
 import {SpanSlug} from 'sentry/utils/performance/suspectSpans/types';
 
-import {MAX, MIN} from './utils';
+import {ZoomKeys} from './utils';
 
 const NUM_BUCKETS = 50;
 const PRECISION = 0;
@@ -40,8 +40,8 @@ type Props = WithRouterProps & {
 export default function ExclusiveTimeHistogram(props: Props) {
   const {location, organization, eventView, spanSlug} = props;
 
-  const start = location.query[MIN];
-  const end = location.query[MAX];
+  const start = location.query[ZoomKeys.MIN];
+  const end = location.query[ZoomKeys.MAX];
 
   return (
     <Fragment>
@@ -81,8 +81,8 @@ export default function ExclusiveTimeHistogram(props: Props) {
               <BarChartZoom
                 minZoomWidth={1}
                 location={location}
-                paramStart={MIN}
-                paramEnd={MAX}
+                paramStart={ZoomKeys.MIN}
+                paramEnd={ZoomKeys.MAX}
                 xAxisIndex={[0]}
                 buckets={histogram ? computeBuckets(histogram) : []}
               >

+ 73 - 0
static/app/views/performance/transactionSummary/transactionSpans/spanDetails/spanDetailsControls.tsx

@@ -0,0 +1,73 @@
+import {browserHistory} from 'react-router';
+import styled from '@emotion/styled';
+import {Location} from 'history';
+
+import Button from 'sentry/components/button';
+import SearchBar from 'sentry/components/events/searchBar';
+import {t} from 'sentry/locale';
+import space from 'sentry/styles/space';
+import {Organization} from 'sentry/types';
+import EventView from 'sentry/utils/discover/eventView';
+import {removeHistogramQueryStrings} from 'sentry/utils/performance/histogram';
+import {decodeScalar} from 'sentry/utils/queryString';
+
+import {ZoomKeys} from './utils';
+
+interface SpanDetailsControlsProps {
+  eventView: EventView;
+  location: Location;
+  organization: Organization;
+}
+
+export default function SpanDetailsControls({
+  organization,
+  eventView,
+  location,
+}: SpanDetailsControlsProps) {
+  const query = decodeScalar(location.query.query, '');
+
+  const handleSearchQuery = (searchQuery: string): void => {
+    browserHistory.push({
+      pathname: location.pathname,
+      query: {
+        ...location.query,
+        cursor: undefined,
+        query: String(searchQuery).trim() || undefined,
+      },
+    });
+  };
+
+  const handleResetView = () => {
+    browserHistory.push({
+      pathname: location.pathname,
+      query: removeHistogramQueryStrings(location, Object.values(ZoomKeys)),
+    });
+  };
+
+  const isZoomed = () => Object.values(ZoomKeys).some(key => location.query[key]);
+
+  return (
+    <StyledActions>
+      <SearchBar
+        placeholder={t('Filter Transactions')}
+        organization={organization}
+        projectIds={eventView.project}
+        query={query}
+        fields={eventView.fields}
+        onSearch={handleSearchQuery}
+      />
+      <Button onClick={handleResetView} disabled={!isZoomed()}>
+        {t('Reset View')}
+      </Button>
+    </StyledActions>
+  );
+}
+
+const StyledActions = styled('div')`
+  display: grid;
+  gap: ${space(2)};
+  grid-template-columns: auto max-content;
+  grid-template-rows: auto;
+  align-items: center;
+  margin-bottom: ${space(2)};
+`;

+ 0 - 50
static/app/views/performance/transactionSummary/transactionSpans/spanDetails/spanDetailsSearchBar.tsx

@@ -1,50 +0,0 @@
-import {browserHistory} from 'react-router';
-import styled from '@emotion/styled';
-import {Location} from 'history';
-
-import SearchBar from 'sentry/components/events/searchBar';
-import {t} from 'sentry/locale';
-import space from 'sentry/styles/space';
-import {Organization} from 'sentry/types';
-import EventView from 'sentry/utils/discover/eventView';
-import {decodeScalar} from 'sentry/utils/queryString';
-
-interface SpanDetailsSearchBarProps {
-  eventView: EventView;
-  location: Location;
-  organization: Organization;
-}
-
-export default function SpanDetailsSearchBar({
-  organization,
-  eventView,
-  location,
-}: SpanDetailsSearchBarProps) {
-  const query = decodeScalar(location.query.query, '');
-
-  const handleSearchQuery = (searchQuery: string): void => {
-    browserHistory.push({
-      pathname: location.pathname,
-      query: {
-        ...location.query,
-        cursor: undefined,
-        query: String(searchQuery).trim() || undefined,
-      },
-    });
-  };
-
-  return (
-    <StyledSearchBar
-      placeholder={t('Filter Transactions')}
-      organization={organization}
-      projectIds={eventView.project}
-      query={query}
-      fields={eventView.fields}
-      onSearch={handleSearchQuery}
-    />
-  );
-}
-
-const StyledSearchBar = styled(SearchBar)`
-  margin-bottom: ${space(2)};
-`;

+ 4 - 2
static/app/views/performance/transactionSummary/transactionSpans/spanDetails/utils.tsx

@@ -45,5 +45,7 @@ export function spanDetailsRouteWithQuery({
   };
 }
 
-export const MIN = 'min';
-export const MAX = 'max';
+export enum ZoomKeys {
+  MIN = 'min',
+  MAX = 'max',
+}

+ 57 - 0
tests/js/spec/views/performance/transactionSpans/spanDetails.spec.tsx

@@ -32,6 +32,8 @@ describe('Performance > Transaction Spans > Span Summary', function () {
   afterEach(function () {
     MockApiClient.clearMockResponses();
     ProjectsStore.reset();
+    // need to typecast to any to be able to call mockReset
+    (browserHistory.push as any).mockReset();
   });
 
   describe('Without Span Data', function () {
@@ -362,6 +364,61 @@ describe('Performance > Transaction Spans > Span Summary', function () {
         expect(searchBarNode).toBeInTheDocument();
       });
 
+      it('disables reset button when no min or max query parameters were set', function () {
+        const data = initializeData({
+          features: FEATURES,
+          query: {project: '1', transaction: 'transaction'},
+        });
+
+        render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
+          context: data.routerContext,
+          organization: data.organization,
+        });
+
+        const resetButton = screen.getByRole('button', {
+          name: /reset view/i,
+        });
+        expect(resetButton).toBeInTheDocument();
+        expect(resetButton).toBeDisabled();
+      });
+
+      it('enables reset button when min and max are set', function () {
+        const data = initializeData({
+          features: FEATURES,
+          query: {project: '1', transaction: 'transaction', min: 10, max: 100},
+        });
+
+        render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
+          context: data.routerContext,
+          organization: data.organization,
+        });
+
+        const resetButton = screen.getByRole('button', {
+          name: /reset view/i,
+        });
+        expect(resetButton).toBeEnabled();
+      });
+
+      it('clears min and max query parameters when reset button is clicked', function () {
+        const data = initializeData({
+          features: FEATURES,
+          query: {project: '1', transaction: 'transaction', min: 10, max: 100},
+        });
+
+        render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
+          context: data.routerContext,
+          organization: data.organization,
+        });
+
+        const resetButton = screen.getByRole('button', {
+          name: /reset view/i,
+        });
+        resetButton.click();
+        expect(browserHistory.push).toHaveBeenCalledWith(
+          expect.not.objectContaining({min: expect.any(Number), max: expect.any(Number)})
+        );
+      });
+
       it('does not add aggregate filters to the query', async function () {
         const data = initializeData({
           features: FEATURES,