details.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import {Fragment, useCallback, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import sortBy from 'lodash/sortBy';
  4. import {
  5. deleteMonitorProcessingErrorByType,
  6. updateMonitor,
  7. } from 'sentry/actionCreators/monitors';
  8. import Alert from 'sentry/components/alert';
  9. import * as Layout from 'sentry/components/layouts/thirds';
  10. import LoadingError from 'sentry/components/loadingError';
  11. import LoadingIndicator from 'sentry/components/loadingIndicator';
  12. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  13. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  14. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  15. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  16. import {t} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
  19. import {setApiQueryData, useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
  20. import useApi from 'sentry/utils/useApi';
  21. import useOrganization from 'sentry/utils/useOrganization';
  22. import DetailsSidebar from 'sentry/views/monitors/components/detailsSidebar';
  23. import {DetailsTimeline} from 'sentry/views/monitors/components/detailsTimeline';
  24. import MonitorProcessingErrors from 'sentry/views/monitors/components/processingErrors/monitorProcessingErrors';
  25. import {makeMonitorErrorsQueryKey} from 'sentry/views/monitors/components/processingErrors/utils';
  26. import {makeMonitorDetailsQueryKey} from 'sentry/views/monitors/utils';
  27. import MonitorCheckIns from './components/monitorCheckIns';
  28. import MonitorHeader from './components/monitorHeader';
  29. import MonitorIssues from './components/monitorIssues';
  30. import MonitorStats from './components/monitorStats';
  31. import MonitorOnboarding from './components/onboarding';
  32. import {StatusToggleButton} from './components/statusToggleButton';
  33. import type {MonitorBucket} from './components/timeline/types';
  34. import type {CheckinProcessingError, Monitor, ProcessingErrorType} from './types';
  35. const DEFAULT_POLL_INTERVAL_MS = 5000;
  36. type Props = RouteComponentProps<{monitorSlug: string; projectId: string}, {}>;
  37. function hasLastCheckIn(monitor: Monitor) {
  38. return monitor.environments.some(e => e.lastCheckIn);
  39. }
  40. function MonitorDetails({params, location}: Props) {
  41. const api = useApi();
  42. const organization = useOrganization();
  43. const queryClient = useQueryClient();
  44. const queryKey = makeMonitorDetailsQueryKey(
  45. organization,
  46. params.projectId,
  47. params.monitorSlug,
  48. {
  49. environment: location.query.environment,
  50. }
  51. );
  52. const {data: monitor, isError} = useApiQuery<Monitor>(queryKey, {
  53. staleTime: 0,
  54. refetchOnWindowFocus: true,
  55. // Refetches while we are waiting for the user to send their first check-in
  56. refetchInterval: query => {
  57. if (!query.state.data) {
  58. return false;
  59. }
  60. const [monitorData] = query.state.data;
  61. return hasLastCheckIn(monitorData) ? false : DEFAULT_POLL_INTERVAL_MS;
  62. },
  63. });
  64. const {data: checkinErrors, refetch: refetchErrors} = useApiQuery<
  65. CheckinProcessingError[]
  66. >(makeMonitorErrorsQueryKey(organization, params.projectId, params.monitorSlug), {
  67. staleTime: 0,
  68. refetchOnWindowFocus: true,
  69. });
  70. function onUpdate(data: Monitor) {
  71. const updatedMonitor = {
  72. ...data,
  73. // TODO(davidenwang): This is a bit of a hack, due to the PUT request
  74. // which pauses/unpauses a monitor not returning monitor environments
  75. // we should reuse the environments retrieved from the initial request
  76. environments: monitor?.environments,
  77. };
  78. setApiQueryData(queryClient, queryKey, updatedMonitor);
  79. }
  80. const handleUpdate = async (data: Partial<Monitor>) => {
  81. if (monitor === undefined) {
  82. return;
  83. }
  84. const resp = await updateMonitor(api, organization.slug, monitor, data);
  85. if (resp !== null) {
  86. onUpdate(resp);
  87. }
  88. };
  89. function handleDismissError(errortype: ProcessingErrorType) {
  90. deleteMonitorProcessingErrorByType(
  91. api,
  92. organization.slug,
  93. params.projectId,
  94. params.monitorSlug,
  95. errortype
  96. );
  97. refetchErrors();
  98. }
  99. // Only display the unknown legend when there are visible unknown check-ins
  100. // in the timeline
  101. const [showUnknownLegend, setShowUnknownLegend] = useState(false);
  102. const checkHasUnknown = useCallback((stats: MonitorBucket[]) => {
  103. const hasUnknown = stats.some(bucket =>
  104. Object.values(bucket[1]).some(envBucket => Boolean(envBucket.unknown))
  105. );
  106. setShowUnknownLegend(hasUnknown);
  107. }, []);
  108. if (isError) {
  109. return (
  110. <LoadingError message={t('The monitor you were looking for was not found.')} />
  111. );
  112. }
  113. if (!monitor) {
  114. return (
  115. <Layout.Page>
  116. <LoadingIndicator />
  117. </Layout.Page>
  118. );
  119. }
  120. const envsSortedByLastCheck = sortBy(monitor.environments, e => e.lastCheckIn);
  121. return (
  122. <SentryDocumentTitle title={`Crons — ${monitor.name}`}>
  123. <Layout.Page>
  124. <MonitorHeader
  125. monitor={monitor}
  126. orgSlug={organization.slug}
  127. onUpdate={onUpdate}
  128. />
  129. <Layout.Body>
  130. <Layout.Main>
  131. <StyledPageFilterBar condensed>
  132. <DatePageFilter />
  133. <EnvironmentPageFilter />
  134. </StyledPageFilterBar>
  135. {monitor.status === 'disabled' && (
  136. <Alert
  137. type="muted"
  138. showIcon
  139. trailingItems={
  140. <StatusToggleButton
  141. monitor={monitor}
  142. size="xs"
  143. onToggleStatus={status => handleUpdate({status})}
  144. >
  145. {t('Enable')}
  146. </StatusToggleButton>
  147. }
  148. >
  149. {t('This monitor is disabled and is not accepting check-ins.')}
  150. </Alert>
  151. )}
  152. {!!checkinErrors?.length && (
  153. <MonitorProcessingErrors
  154. checkinErrors={checkinErrors}
  155. onDismiss={handleDismissError}
  156. >
  157. {t('Errors were encountered while ingesting check-ins for this monitor')}
  158. </MonitorProcessingErrors>
  159. )}
  160. {!hasLastCheckIn(monitor) ? (
  161. <MonitorOnboarding monitor={monitor} />
  162. ) : (
  163. <Fragment>
  164. <DetailsTimeline monitor={monitor} onStatsLoaded={checkHasUnknown} />
  165. <MonitorStats
  166. orgSlug={organization.slug}
  167. monitor={monitor}
  168. monitorEnvs={monitor.environments}
  169. />
  170. <MonitorIssues
  171. orgSlug={organization.slug}
  172. monitor={monitor}
  173. monitorEnvs={monitor.environments}
  174. />
  175. <MonitorCheckIns
  176. orgSlug={organization.slug}
  177. monitor={monitor}
  178. monitorEnvs={monitor.environments}
  179. />
  180. </Fragment>
  181. )}
  182. </Layout.Main>
  183. <Layout.Side>
  184. <DetailsSidebar
  185. monitorEnv={envsSortedByLastCheck[envsSortedByLastCheck.length - 1]}
  186. monitor={monitor}
  187. showUnknownLegend={showUnknownLegend}
  188. />
  189. </Layout.Side>
  190. </Layout.Body>
  191. </Layout.Page>
  192. </SentryDocumentTitle>
  193. );
  194. }
  195. const StyledPageFilterBar = styled(PageFilterBar)`
  196. margin-bottom: ${space(2)};
  197. `;
  198. export default MonitorDetails;