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

feat(crons): Render timeline view in monitor details page (#55117)

<img width="914" alt="image"
src="https://github.com/getsentry/sentry/assets/9372512/4a2ae4f1-3b38-4b46-a1fe-d69f91a22351">
David Wang 1 год назад
Родитель
Сommit
c2c8014ede

+ 128 - 0
static/app/views/monitors/components/cronDetailsTimeline.tsx

@@ -0,0 +1,128 @@
+import {useRef} from 'react';
+import styled from '@emotion/styled';
+import moment from 'moment';
+
+import Panel from 'sentry/components/panels/panel';
+import Text from 'sentry/components/text';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {Organization} from 'sentry/types';
+import {parsePeriodToHours} from 'sentry/utils/dates';
+import {useApiQuery} from 'sentry/utils/queryClient';
+import {useDimensions} from 'sentry/utils/useDimensions';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import useRouter from 'sentry/utils/useRouter';
+import {
+  GridLineOverlay,
+  GridLineTimeLabels,
+} from 'sentry/views/monitors/components/overviewTimeline/gridLines';
+import {TimelineTableRow} from 'sentry/views/monitors/components/overviewTimeline/timelineTableRow';
+import {MonitorBucketData} from 'sentry/views/monitors/components/overviewTimeline/types';
+import {getConfigFromTimeRange} from 'sentry/views/monitors/components/overviewTimeline/utils';
+import {Monitor} from 'sentry/views/monitors/types';
+
+interface Props {
+  monitor: Monitor;
+  organization: Organization;
+}
+
+export function CronDetailsTimeline({monitor, organization}: Props) {
+  const {location} = useRouter();
+  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 monitorStatsQueryKey = `/organizations/${organization.slug}/monitors-stats/`;
+  const {data: monitorStats, isLoading} = useApiQuery<Record<string, MonitorBucketData>>(
+    [
+      monitorStatsQueryKey,
+      {
+        query: {
+          until: Math.floor(end.getTime() / 1000),
+          since: Math.floor(start.getTime() / 1000),
+          monitor: monitor.slug,
+          resolution: `${rollup}s`,
+          ...location.query,
+        },
+      },
+    ],
+    {
+      staleTime: 0,
+      enabled: timelineWidth > 0,
+    }
+  );
+
+  return (
+    <TimelineContainer>
+      <TimelineWidthTracker ref={elementRef} />
+      <TimelineTitle>{t('Check-Ins')}</TimelineTitle>
+      <StyledGridLineTimeLabels
+        timeWindowConfig={config}
+        start={start}
+        end={end}
+        width={timelineWidth}
+      />
+      <StyledGridLineOverlay
+        showCursor={!isLoading}
+        timeWindowConfig={config}
+        start={start}
+        end={end}
+        width={timelineWidth}
+      />
+      <TimelineTableRow
+        monitor={monitor}
+        bucketedData={monitorStats?.[monitor.slug]}
+        timeWindowConfig={config}
+        end={end}
+        start={start}
+        width={timelineWidth}
+        singleMonitorView
+      />
+    </TimelineContainer>
+  );
+}
+
+const TimelineContainer = styled(Panel)`
+  display: grid;
+  grid-template-columns: 135px 1fr;
+  align-items: center;
+`;
+
+const StyledGridLineTimeLabels = styled(GridLineTimeLabels)`
+  grid-column: 2;
+`;
+
+const StyledGridLineOverlay = styled(GridLineOverlay)`
+  grid-column: 2;
+`;
+
+const TimelineWidthTracker = styled('div')`
+  position: absolute;
+  width: 100%;
+  grid-row: 1;
+  grid-column: 2;
+`;
+
+const TimelineTitle = styled(Text)`
+  ${p => p.theme.text.cardTitle};
+  border-bottom: 1px solid ${p => p.theme.border};
+  padding: ${space(2)};
+`;

+ 1 - 1
static/app/views/monitors/components/monitorStats.tsx

@@ -119,7 +119,7 @@ function MonitorStats({monitor, monitorEnvs, orgSlug}: Props) {
     <Fragment>
       <Panel>
         <PanelBody withPadding>
-          <StyledHeaderTitle>{t('Check-Ins')}</StyledHeaderTitle>
+          <StyledHeaderTitle>{t('Status')}</StyledHeaderTitle>
           {isLoading ? (
             <Placeholder height={`${height}px`} />
           ) : (

+ 25 - 11
static/app/views/monitors/components/overviewTimeline/timelineTableRow.tsx

@@ -1,5 +1,6 @@
 import {useState} from 'react';
 import {Link} from 'react-router';
+import {css} from '@emotion/react';
 import styled from '@emotion/styled';
 
 import {Button} from 'sentry/components/button';
@@ -18,11 +19,21 @@ import {MonitorBucket} from './types';
 interface Props extends Omit<CheckInTimelineProps, 'bucketedData' | 'environment'> {
   monitor: Monitor;
   bucketedData?: MonitorBucket[];
+  /**
+   * Whether only one monitor is being rendered in a larger view with this component
+   * turns off things like zebra striping, hover effect, and showing monitor name
+   */
+  singleMonitorView?: boolean;
 }
 
 const MAX_SHOWN_ENVIRONMENTS = 4;
 
-export function TimelineTableRow({monitor, bucketedData, ...timelineProps}: Props) {
+export function TimelineTableRow({
+  monitor,
+  bucketedData,
+  singleMonitorView,
+  ...timelineProps
+}: Props) {
   const [isExpanded, setExpanded] = useState(
     monitor.environments.length <= MAX_SHOWN_ENVIRONMENTS
   );
@@ -32,8 +43,8 @@ export function TimelineTableRow({monitor, bucketedData, ...timelineProps}: Prop
     : monitor.environments.slice(0, MAX_SHOWN_ENVIRONMENTS);
 
   return (
-    <TimelineRow key={monitor.id}>
-      <MonitorDetails monitor={monitor} />
+    <TimelineRow key={monitor.id} singleMonitorView={singleMonitorView}>
+      {!singleMonitorView && <MonitorDetails monitor={monitor} />}
       <MonitorEnvContainer>
         {environments.map(({name, status}) => {
           const envStatus =
@@ -91,16 +102,19 @@ function MonitorDetails({monitor}: {monitor: Monitor}) {
   );
 }
 
-const TimelineRow = styled('div')`
+const TimelineRow = styled('div')<{singleMonitorView?: boolean}>`
   display: contents;
 
-  &:nth-child(odd) > * {
-    background: ${p => p.theme.backgroundSecondary};
-  }
-
-  &:hover > * {
-    background: ${p => p.theme.backgroundTertiary};
-  }
+  ${p =>
+    !p.singleMonitorView &&
+    css`
+      &:nth-child(odd) > * {
+        background: ${p.theme.backgroundSecondary};
+      }
+      &:hover > * {
+        background: ${p.theme.backgroundTertiary};
+      }
+    `}
 
   > * {
     transition: background 50ms ease-in-out;

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

@@ -12,6 +12,7 @@ import {space} from 'sentry/styles/space';
 import {setApiQueryData, useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
+import {CronDetailsTimeline} from 'sentry/views/monitors/components/cronDetailsTimeline';
 import DetailsSidebar from 'sentry/views/monitors/components/detailsSidebar';
 
 import MonitorCheckIns from './components/monitorCheckIns';
@@ -93,6 +94,7 @@ function MonitorDetails({params, location}: Props) {
               <MonitorOnboarding monitor={monitor} />
             ) : (
               <Fragment>
+                <CronDetailsTimeline organization={organization} monitor={monitor} />
                 <MonitorStats
                   orgSlug={organization.slug}
                   monitor={monitor}