monitors.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import {Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import * as qs from 'query-string';
  5. import onboardingImg from 'sentry-images/spot/onboarding-preview.svg';
  6. import {Button, ButtonProps} from 'sentry/components/button';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
  9. import FeatureBadge from 'sentry/components/featureBadge';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import LoadingIndicator from 'sentry/components/loadingIndicator';
  12. import OnboardingPanel from 'sentry/components/onboardingPanel';
  13. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  14. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  15. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  16. import Pagination from 'sentry/components/pagination';
  17. import {PanelTable} from 'sentry/components/panels';
  18. import ProjectPageFilter from 'sentry/components/projectPageFilter';
  19. import SearchBar from 'sentry/components/searchBar';
  20. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  21. import {IconAdd} from 'sentry/icons';
  22. import {t} from 'sentry/locale';
  23. import {space} from 'sentry/styles/space';
  24. import {setApiQueryData, useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
  25. import {decodeScalar} from 'sentry/utils/queryString';
  26. import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
  27. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  28. import useOrganization from 'sentry/utils/useOrganization';
  29. import usePageFilters from 'sentry/utils/usePageFilters';
  30. import useRouter from 'sentry/utils/useRouter';
  31. import CronsFeedbackButton from './components/cronsFeedbackButton';
  32. import {MonitorRow} from './components/row';
  33. import {Monitor, MonitorEnvironment} from './types';
  34. function NewMonitorButton(props: ButtonProps) {
  35. const organization = useOrganization();
  36. const {selection} = usePageFilters();
  37. return (
  38. <Button
  39. to={{
  40. pathname: `/organizations/${organization.slug}/crons/create/`,
  41. query: {project: selection.projects},
  42. }}
  43. priority="primary"
  44. {...props}
  45. >
  46. {props.children}
  47. </Button>
  48. );
  49. }
  50. export default function Monitors({location}: RouteComponentProps<{}, {}>) {
  51. const organization = useOrganization();
  52. const router = useRouter();
  53. const queryClient = useQueryClient();
  54. const monitorListQueryKey = [
  55. `/organizations/${organization.slug}/monitors/`,
  56. {query: {...location.query, includeNew: true}},
  57. ] as const;
  58. const {
  59. data: monitorList,
  60. getResponseHeader: monitorListHeaders,
  61. isLoading,
  62. } = useApiQuery<Monitor[]>(monitorListQueryKey, {
  63. staleTime: 0,
  64. });
  65. useRouteAnalyticsEventNames('monitors.page_viewed', 'Monitors: Page Viewed');
  66. useRouteAnalyticsParams({empty_state: !monitorList || monitorList.length === 0});
  67. const monitorListPageLinks = monitorListHeaders?.('Link');
  68. const handleSearch = (query: string) => {
  69. router.push({
  70. pathname: location.pathname,
  71. query: normalizeDateTimeParams({
  72. ...(location.query || {}),
  73. query,
  74. }),
  75. });
  76. };
  77. const renderMonitorRow = (monitor: Monitor, monitorEnv?: MonitorEnvironment) => (
  78. <MonitorRow
  79. key={`${monitor.slug}-${monitorEnv?.name ?? 'no-env'}`}
  80. monitor={monitor}
  81. monitorEnv={monitorEnv}
  82. onDelete={deletedEnv => {
  83. if (deletedEnv) {
  84. if (!monitorList) {
  85. return;
  86. }
  87. const deletedEnvMonitor = monitorList.find(m => m.slug === monitor.slug);
  88. if (!deletedEnvMonitor) {
  89. return;
  90. }
  91. deletedEnvMonitor.environments = deletedEnvMonitor.environments.filter(
  92. e => e.name !== deletedEnv.name
  93. );
  94. setApiQueryData(queryClient, monitorListQueryKey, monitorList);
  95. } else {
  96. setApiQueryData(
  97. queryClient,
  98. monitorListQueryKey,
  99. monitorList?.filter(m => m.slug !== monitor.slug)
  100. );
  101. }
  102. }}
  103. organization={organization}
  104. />
  105. );
  106. return (
  107. <SentryDocumentTitle title={`Crons - ${organization.slug}`}>
  108. <Layout.Page>
  109. <Layout.Header>
  110. <Layout.HeaderContent>
  111. <Layout.Title>
  112. {t('Cron Monitors')}
  113. <PageHeadingQuestionTooltip
  114. title={t(
  115. 'Scheduled monitors that check in on recurring jobs and tell you if they’re running on schedule, failing, or succeeding.'
  116. )}
  117. docsUrl="https://docs.sentry.io/product/crons/"
  118. />
  119. <FeatureBadge type="beta" />
  120. </Layout.Title>
  121. </Layout.HeaderContent>
  122. <Layout.HeaderActions>
  123. <ButtonBar gap={1}>
  124. <CronsFeedbackButton />
  125. <NewMonitorButton size="sm" icon={<IconAdd isCircled size="xs" />}>
  126. {t('Add Monitor')}
  127. </NewMonitorButton>
  128. </ButtonBar>
  129. </Layout.HeaderActions>
  130. </Layout.Header>
  131. <Layout.Body>
  132. <Layout.Main fullWidth>
  133. <Filters>
  134. <PageFilterBar>
  135. <ProjectPageFilter resetParamsOnChange={['cursor']} />
  136. <EnvironmentPageFilter resetParamsOnChange={['cursor']} />
  137. </PageFilterBar>
  138. <SearchBar
  139. query={decodeScalar(qs.parse(location.search)?.query, '')}
  140. placeholder={t('Search by name or slug')}
  141. onSearch={handleSearch}
  142. />
  143. </Filters>
  144. {isLoading ? (
  145. <LoadingIndicator />
  146. ) : monitorList?.length ? (
  147. <Fragment>
  148. <StyledPanelTable
  149. headers={[
  150. t('Monitor Name'),
  151. t('Status'),
  152. t('Schedule'),
  153. t('Next Checkin'),
  154. t('Project'),
  155. t('Environment'),
  156. t('Actions'),
  157. ]}
  158. >
  159. {monitorList
  160. ?.map(monitor =>
  161. monitor.environments.length > 0
  162. ? monitor.environments.map(monitorEnv =>
  163. renderMonitorRow(monitor, monitorEnv)
  164. )
  165. : renderMonitorRow(monitor)
  166. )
  167. .flat()}
  168. </StyledPanelTable>
  169. {monitorListPageLinks && <Pagination pageLinks={monitorListPageLinks} />}
  170. </Fragment>
  171. ) : (
  172. <OnboardingPanel image={<img src={onboardingImg} />}>
  173. <h3>{t('Let Sentry monitor your recurring jobs')}</h3>
  174. <p>
  175. {t(
  176. "We'll tell you if your recurring jobs are running on schedule, failing, or succeeding."
  177. )}
  178. </p>
  179. <ButtonList gap={1}>
  180. <NewMonitorButton>{t('Set up first cron monitor')}</NewMonitorButton>
  181. <Button href="https://docs.sentry.io/product/crons" external>
  182. {t('Read docs')}
  183. </Button>
  184. </ButtonList>
  185. </OnboardingPanel>
  186. )}
  187. </Layout.Main>
  188. </Layout.Body>
  189. </Layout.Page>
  190. </SentryDocumentTitle>
  191. );
  192. }
  193. const Filters = styled('div')`
  194. display: grid;
  195. grid-template-columns: max-content 1fr;
  196. gap: ${space(1.5)};
  197. margin-bottom: ${space(2)};
  198. `;
  199. const StyledPanelTable = styled(PanelTable)`
  200. grid-template-columns: 1fr max-content 1fr max-content max-content max-content max-content;
  201. `;
  202. const ButtonList = styled(ButtonBar)`
  203. grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
  204. `;