details.tsx 7.3 KB

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