overview.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as qs from 'query-string';
  4. import onboardingImg from 'sentry-images/spot/onboarding-preview.svg';
  5. import {Button, ButtonProps} from 'sentry/components/button';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
  8. import FeatureBadge from 'sentry/components/featureBadge';
  9. import * as Layout from 'sentry/components/layouts/thirds';
  10. import LoadingIndicator from 'sentry/components/loadingIndicator';
  11. import OnboardingPanel from 'sentry/components/onboardingPanel';
  12. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  13. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  14. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  15. import Pagination from 'sentry/components/pagination';
  16. import ProjectPageFilter from 'sentry/components/projectPageFilter';
  17. import SearchBar from 'sentry/components/searchBar';
  18. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  19. import {IconAdd} from 'sentry/icons';
  20. import {t} from 'sentry/locale';
  21. import {space} from 'sentry/styles/space';
  22. import {useApiQuery} from 'sentry/utils/queryClient';
  23. import {decodeScalar} from 'sentry/utils/queryString';
  24. import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
  25. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  26. import useOrganization from 'sentry/utils/useOrganization';
  27. import usePageFilters from 'sentry/utils/usePageFilters';
  28. import useRouter from 'sentry/utils/useRouter';
  29. import CronsFeedbackButton from './components/cronsFeedbackButton';
  30. import {OverviewTable} from './components/overviewTable';
  31. import {OverviewTimeline} from './components/overviewTimeline';
  32. import {Monitor} from './types';
  33. import {makeMonitorListQueryKey} from './utils';
  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() {
  51. const organization = useOrganization();
  52. const router = useRouter();
  53. const queryKey = makeMonitorListQueryKey(organization, router.location);
  54. const {
  55. data: monitorList,
  56. getResponseHeader: monitorListHeaders,
  57. isLoading,
  58. } = useApiQuery<Monitor[]>(queryKey, {
  59. staleTime: 0,
  60. });
  61. useRouteAnalyticsEventNames('monitors.page_viewed', 'Monitors: Page Viewed');
  62. useRouteAnalyticsParams({empty_state: !monitorList || monitorList.length === 0});
  63. const monitorListPageLinks = monitorListHeaders?.('Link');
  64. const handleSearch = (query: string) => {
  65. const currentQuery = router.location.query ?? {};
  66. router.push({
  67. pathname: location.pathname,
  68. query: normalizeDateTimeParams({...currentQuery, query}),
  69. });
  70. };
  71. const monitorsTimelineView = organization.features.includes(
  72. 'crons-timeline-listing-page'
  73. );
  74. return (
  75. <SentryDocumentTitle title={`Crons — ${organization.slug}`}>
  76. <Layout.Page>
  77. <Layout.Header>
  78. <Layout.HeaderContent>
  79. <Layout.Title>
  80. {t('Cron Monitors')}
  81. <PageHeadingQuestionTooltip
  82. title={t(
  83. 'Scheduled monitors that check in on recurring jobs and tell you if they’re running on schedule, failing, or succeeding.'
  84. )}
  85. docsUrl="https://docs.sentry.io/product/crons/"
  86. />
  87. <FeatureBadge type="beta" />
  88. </Layout.Title>
  89. </Layout.HeaderContent>
  90. <Layout.HeaderActions>
  91. <ButtonBar gap={1}>
  92. <CronsFeedbackButton />
  93. <NewMonitorButton size="sm" icon={<IconAdd isCircled size="xs" />}>
  94. {t('Add Monitor')}
  95. </NewMonitorButton>
  96. </ButtonBar>
  97. </Layout.HeaderActions>
  98. </Layout.Header>
  99. <Layout.Body>
  100. <Layout.Main fullWidth>
  101. <Filters>
  102. <PageFilterBar>
  103. <ProjectPageFilter resetParamsOnChange={['cursor']} />
  104. <EnvironmentPageFilter resetParamsOnChange={['cursor']} />
  105. </PageFilterBar>
  106. <SearchBar
  107. query={decodeScalar(qs.parse(location.search)?.query, '')}
  108. placeholder={t('Search by name or slug')}
  109. onSearch={handleSearch}
  110. />
  111. </Filters>
  112. {isLoading ? (
  113. <LoadingIndicator />
  114. ) : monitorList?.length ? (
  115. <Fragment>
  116. {monitorsTimelineView ? (
  117. <OverviewTimeline monitorList={monitorList} />
  118. ) : (
  119. <OverviewTable monitorList={monitorList} />
  120. )}
  121. {monitorListPageLinks && <Pagination pageLinks={monitorListPageLinks} />}
  122. </Fragment>
  123. ) : (
  124. <OnboardingPanel image={<img src={onboardingImg} />}>
  125. <h3>{t('Let Sentry monitor your recurring jobs')}</h3>
  126. <p>
  127. {t(
  128. "We'll tell you if your recurring jobs are running on schedule, failing, or succeeding."
  129. )}
  130. </p>
  131. <OnboardingActions gap={1}>
  132. <NewMonitorButton>{t('Set up first cron monitor')}</NewMonitorButton>
  133. <Button href="https://docs.sentry.io/product/crons" external>
  134. {t('Read docs')}
  135. </Button>
  136. </OnboardingActions>
  137. </OnboardingPanel>
  138. )}
  139. </Layout.Main>
  140. </Layout.Body>
  141. </Layout.Page>
  142. </SentryDocumentTitle>
  143. );
  144. }
  145. const Filters = styled('div')`
  146. display: grid;
  147. grid-template-columns: max-content 1fr;
  148. gap: ${space(1.5)};
  149. margin-bottom: ${space(2)};
  150. `;
  151. const OnboardingActions = styled(ButtonBar)`
  152. grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
  153. `;