Browse Source

ref(crons): Switch overview to DatePageFilter (#67807)

This change does a few things

- Extracts the logic to compute the selection API query values used by
the monitor-stats endpoint into a hook

- Replaces this logic in the monitor details CronDetailsTimeline with
the hook version.

- Replaces the ResolutionSelector in the OverviewTimeline with the same
hook and introduces the DatePageFilter

Looks like this:

<img width="896" alt="image"
src="https://github.com/getsentry/sentry/assets/1421724/57beabdf-426e-4e82-8772-3cda2c71d447">

---------

Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
Evan Purkhiser 11 months ago
parent
commit
6a3cc150fe

+ 12 - 33
static/app/views/monitors/components/cronDetailsTimeline.tsx

@@ -1,6 +1,5 @@
 import {useRef} from 'react';
 import styled from '@emotion/styled';
-import moment from 'moment';
 
 import {
   deleteMonitorEnvironment,
@@ -11,11 +10,9 @@ import Text from 'sentry/components/text';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {Organization} from 'sentry/types';
-import {parsePeriodToHours} from 'sentry/utils/dates';
 import {setApiQueryData, useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
 import useApi from 'sentry/utils/useApi';
 import {useDimensions} from 'sentry/utils/useDimensions';
-import usePageFilters from 'sentry/utils/usePageFilters';
 import useRouter from 'sentry/utils/useRouter';
 import {
   GridLineOverlay,
@@ -23,9 +20,9 @@ import {
 } from 'sentry/views/monitors/components/overviewTimeline/gridLines';
 import {TimelineTableRow} from 'sentry/views/monitors/components/overviewTimeline/timelineTableRow';
 import type {MonitorBucketData} from 'sentry/views/monitors/components/overviewTimeline/types';
-import {getConfigFromTimeRange} from 'sentry/views/monitors/components/overviewTimeline/utils';
 import type {Monitor} from 'sentry/views/monitors/types';
 import {makeMonitorDetailsQueryKey} from 'sentry/views/monitors/utils';
+import {useMonitorTimes} from 'sentry/views/monitors/utils/useMonitorDates';
 
 interface Props {
   monitor: Monitor;
@@ -36,27 +33,11 @@ export function CronDetailsTimeline({monitor, organization}: Props) {
   const {location} = useRouter();
   const api = useApi();
   const queryClient = useQueryClient();
-  const nowRef = useRef<Date>(new Date());
-  const {selection} = usePageFilters();
-  const {period} = selection.datetime;
-  let {end, start} = selection.datetime;
-
-  if (!start || !end) {
-    end = nowRef.current;
-    start = moment(end)
-      .subtract(parsePeriodToHours(period ?? '24h'), 'hour')
-      .toDate();
-  } else {
-    start = new Date(start);
-    end = new Date(end);
-  }
 
   const elementRef = useRef<HTMLDivElement>(null);
   const {width: timelineWidth} = useDimensions<HTMLDivElement>({elementRef});
-  const config = getConfigFromTimeRange(start, end, timelineWidth);
 
-  const elapsedMinutes = config.elapsedMinutes;
-  const rollup = Math.floor((elapsedMinutes * 60) / timelineWidth);
+  const {dates, selectionQuery, timeWindowConfig} = useMonitorTimes({timelineWidth});
 
   const monitorStatsQueryKey = `/organizations/${organization.slug}/monitors-stats/`;
   const {data: monitorStats, isLoading} = useApiQuery<Record<string, MonitorBucketData>>(
@@ -64,10 +45,8 @@ export function CronDetailsTimeline({monitor, organization}: Props) {
       monitorStatsQueryKey,
       {
         query: {
-          until: Math.floor(end.getTime() / 1000),
-          since: Math.floor(start.getTime() / 1000),
           monitor: monitor.slug,
-          resolution: `${rollup}s`,
+          ...selectionQuery,
           ...location.query,
         },
       },
@@ -137,25 +116,25 @@ export function CronDetailsTimeline({monitor, organization}: Props) {
       <Header>
         <TimelineTitle>{t('Check-Ins')}</TimelineTitle>
         <GridLineTimeLabels
-          timeWindowConfig={config}
-          start={start}
-          end={end}
+          timeWindowConfig={timeWindowConfig}
+          start={dates.start}
+          end={dates.end}
           width={timelineWidth}
         />
       </Header>
       <StyledGridLineOverlay
         showCursor={!isLoading}
-        timeWindowConfig={config}
-        start={start}
-        end={end}
+        timeWindowConfig={timeWindowConfig}
+        start={dates.start}
+        end={dates.end}
         width={timelineWidth}
       />
       <TimelineTableRow
         monitor={monitor}
         bucketedData={monitorStats?.[monitor.slug]}
-        timeWindowConfig={config}
-        end={end}
-        start={start}
+        timeWindowConfig={timeWindowConfig}
+        start={dates.start}
+        end={dates.end}
         width={timelineWidth}
         onDeleteEnvironment={handleDeleteEnvironment}
         onToggleMuteEnvironment={handleToggleMuteEnvironment}

+ 12 - 20
static/app/views/monitors/components/overviewTimeline/index.tsx

@@ -19,14 +19,12 @@ import {
   GridLineTimeLabels,
 } from 'sentry/views/monitors/components/overviewTimeline/gridLines';
 import {SortSelector} from 'sentry/views/monitors/components/overviewTimeline/sortSelector';
+import type {Monitor} from 'sentry/views/monitors/types';
 import {makeMonitorListQueryKey} from 'sentry/views/monitors/utils';
+import {useMonitorTimes} from 'sentry/views/monitors/utils/useMonitorDates';
 
-import type {Monitor} from '../../types';
-
-import {ResolutionSelector} from './resolutionSelector';
 import {TimelineTableRow} from './timelineTableRow';
-import type {MonitorBucketData, TimeWindow} from './types';
-import {getConfigFromTimeRange, getStartFromTimeWindow} from './utils';
+import type {MonitorBucketData} from './types';
 
 interface Props {
   monitorList: Monitor[];
@@ -39,24 +37,19 @@ export function OverviewTimeline({monitorList}: Props) {
   const router = useRouter();
   const location = router.location;
 
-  const timeWindow: TimeWindow = location.query?.timeWindow ?? '24h';
-  const nowRef = useRef(new Date());
-  const start = getStartFromTimeWindow(nowRef.current, timeWindow);
   const elementRef = useRef<HTMLDivElement>(null);
   const {width: timelineWidth} = useDimensions<HTMLDivElement>({elementRef});
 
-  const timeWindowConfig = getConfigFromTimeRange(start, nowRef.current, timelineWidth);
-  const rollup = Math.floor((timeWindowConfig.elapsedMinutes * 60) / timelineWidth);
+  const {dates, selectionQuery, timeWindowConfig} = useMonitorTimes({timelineWidth});
+
   const monitorStatsQueryKey = `/organizations/${organization.slug}/monitors-stats/`;
   const {data: monitorStats, isLoading} = useApiQuery<Record<string, MonitorBucketData>>(
     [
       monitorStatsQueryKey,
       {
         query: {
-          until: Math.floor(nowRef.current.getTime() / 1000),
-          since: Math.floor(start.getTime() / 1000),
           monitor: monitorList.map(m => m.slug),
-          resolution: `${rollup}s`,
+          ...selectionQuery,
           ...location.query,
         },
       },
@@ -146,13 +139,12 @@ export function OverviewTimeline({monitorList}: Props) {
       <TimelineWidthTracker ref={elementRef} />
       <Header>
         <HeaderControls>
-          <ResolutionSelector />
           <SortSelector size="xs" />
         </HeaderControls>
         <GridLineTimeLabels
           timeWindowConfig={timeWindowConfig}
-          start={start}
-          end={nowRef.current}
+          start={dates.start}
+          end={dates.end}
           width={timelineWidth}
         />
       </Header>
@@ -160,8 +152,8 @@ export function OverviewTimeline({monitorList}: Props) {
         stickyCursor
         showCursor={!isLoading}
         timeWindowConfig={timeWindowConfig}
-        start={start}
-        end={nowRef.current}
+        start={dates.start}
+        end={dates.end}
         width={timelineWidth}
       />
 
@@ -170,9 +162,9 @@ export function OverviewTimeline({monitorList}: Props) {
           key={monitor.id}
           monitor={monitor}
           timeWindowConfig={timeWindowConfig}
-          start={start}
           bucketedData={monitorStats?.[monitor.slug]}
-          end={nowRef.current}
+          start={dates.start}
+          end={dates.end}
           width={timelineWidth}
           onDeleteEnvironment={env => handleDeleteEnvironment(monitor, env)}
           onToggleMuteEnvironment={(env, isMuted) =>

+ 1 - 35
static/app/views/monitors/components/overviewTimeline/utils.spec.tsx

@@ -1,41 +1,7 @@
 import {getFormat} from 'sentry/utils/dates';
-import {
-  getConfigFromTimeRange,
-  getStartFromTimeWindow,
-} from 'sentry/views/monitors/components/overviewTimeline/utils';
+import {getConfigFromTimeRange} from 'sentry/views/monitors/components/overviewTimeline/utils';
 
 describe('Crons Timeline Utils', function () {
-  describe('getStartFromTimeWindow', function () {
-    const end = new Date('2023-06-15T12:00:00Z');
-    it('correctly computes for 1h', function () {
-      const expectedStart = new Date('2023-06-15T11:00:00Z');
-      const start = getStartFromTimeWindow(end, '1h');
-
-      expect(start).toEqual(expectedStart);
-    });
-
-    it('correctly computes for 24h', function () {
-      const expectedStart = new Date('2023-06-14T12:00:00Z');
-      const start = getStartFromTimeWindow(end, '24h');
-
-      expect(start).toEqual(expectedStart);
-    });
-
-    it('correctly computes for 7d', function () {
-      const expectedStart = new Date('2023-06-08T12:00:00Z');
-      const start = getStartFromTimeWindow(end, '7d');
-
-      expect(start).toEqual(expectedStart);
-    });
-
-    it('correctly computes for 30d', function () {
-      const expectedStart = new Date('2023-05-16T12:00:00Z');
-      const start = getStartFromTimeWindow(end, '30d');
-
-      expect(start).toEqual(expectedStart);
-    });
-  });
-
   describe('getConfigFromTimeRange', function () {
     const timelineWidth = 800;
 

+ 0 - 8
static/app/views/monitors/components/overviewTimeline/utils.tsx

@@ -1,5 +1,3 @@
-import moment from 'moment';
-
 import {getFormat} from 'sentry/utils/dates';
 
 import type {TimeWindow, TimeWindowConfig} from './types';
@@ -12,12 +10,6 @@ export const resolutionElapsedMinutes: Record<TimeWindow, number> = {
   '30d': 60 * 24 * 30,
 };
 
-export function getStartFromTimeWindow(end: Date, timeWindow: TimeWindow): Date {
-  const start = moment(end).subtract(resolutionElapsedMinutes[timeWindow], 'minute');
-
-  return start.toDate();
-}
-
 // The pixels to allocate to each time label based on (MMM DD HH:SS AM/PM)
 const TIMELABEL_WIDTH = 100;
 

+ 2 - 0
static/app/views/monitors/overview.tsx

@@ -9,6 +9,7 @@ import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidg
 import HookOrDefault from 'sentry/components/hookOrDefault';
 import * as Layout from 'sentry/components/layouts/thirds';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
+import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
 import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
@@ -119,6 +120,7 @@ export default function Monitors() {
               <PageFilterBar>
                 <ProjectPageFilter resetParamsOnChange={['cursor']} />
                 <EnvironmentPageFilter resetParamsOnChange={['cursor']} />
+                <DatePageFilter resetParamsOnChange={['cursor']} />
               </PageFilterBar>
               <SearchBar
                 query={decodeScalar(qs.parse(location.search)?.query, '')}

+ 83 - 0
static/app/views/monitors/utils/useMonitorDates.tsx

@@ -0,0 +1,83 @@
+import {useRef} from 'react';
+import moment from 'moment';
+
+import {intervalToMilliseconds} from 'sentry/utils/dates';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import {getConfigFromTimeRange} from 'sentry/views/monitors/components/overviewTimeline/utils';
+
+import type {TimeWindowConfig} from '../components/overviewTimeline/types';
+
+interface Options {
+  /**
+   * The width of the timeline influences ho we caluclate the rollup value
+   */
+  timelineWidth: number;
+}
+
+interface Dates {
+  end: Date;
+  start: Date;
+}
+
+interface SelectionQuery {
+  resolution: string;
+  since: number;
+  until: number;
+}
+
+interface UseMonitorTimesResult {
+  /**
+   * Contains Date objects representing the start and end times of the
+   * selection.
+   */
+  dates: Dates;
+  /**
+   * Contains values used in the monitor-stats API query
+   */
+  selectionQuery: SelectionQuery;
+  /**
+   * The computed timeWindowConfig
+   */
+  timeWindowConfig: TimeWindowConfig;
+}
+
+/**
+ * Computes since, until, and resolution for monitor stats based on the current
+ * selected page filters.
+ */
+export function useMonitorTimes({timelineWidth}: Options): UseMonitorTimesResult {
+  const nowRef = useRef<Date>(new Date());
+  const {selection} = usePageFilters();
+  const {start, end, period} = selection.datetime;
+
+  let since: Date;
+  let until: Date;
+
+  if (!start || !end) {
+    until = nowRef.current;
+    since = moment(nowRef.current)
+      .subtract(intervalToMilliseconds(period ?? '24h'), 'milliseconds')
+      .toDate();
+  } else {
+    since = new Date(start);
+    until = new Date(end);
+  }
+
+  const timeWindowConfig = getConfigFromTimeRange(since, until, timelineWidth);
+
+  const elapsedMinutes = timeWindowConfig.elapsedMinutes;
+  const rollup = Math.floor((elapsedMinutes * 60) / timelineWidth);
+
+  const dates = {
+    start: since,
+    end: until,
+  };
+
+  const selectionQuery = {
+    since: Math.floor(since.getTime() / 1000),
+    until: Math.floor(until.getTime() / 1000),
+    resolution: `${rollup}s`,
+  };
+
+  return {selectionQuery, dates, timeWindowConfig};
+}