overview.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  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 {OverviewTimeline} from './components/overviewTimeline';
  31. import {Monitor} from './types';
  32. import {makeMonitorListQueryKey} from './utils';
  33. function NewMonitorButton(props: ButtonProps) {
  34. const organization = useOrganization();
  35. const {selection} = usePageFilters();
  36. return (
  37. <Button
  38. to={{
  39. pathname: `/organizations/${organization.slug}/crons/create/`,
  40. query: {project: selection.projects},
  41. }}
  42. priority="primary"
  43. {...props}
  44. >
  45. {props.children}
  46. </Button>
  47. );
  48. }
  49. export default function Monitors() {
  50. const organization = useOrganization();
  51. const router = useRouter();
  52. const queryKey = makeMonitorListQueryKey(organization, router.location);
  53. const {
  54. data: monitorList,
  55. getResponseHeader: monitorListHeaders,
  56. isLoading,
  57. } = useApiQuery<Monitor[]>(queryKey, {
  58. staleTime: 0,
  59. });
  60. useRouteAnalyticsEventNames('monitors.page_viewed', 'Monitors: Page Viewed');
  61. useRouteAnalyticsParams({empty_state: !monitorList || monitorList.length === 0});
  62. const monitorListPageLinks = monitorListHeaders?.('Link');
  63. const handleSearch = (query: string) => {
  64. const currentQuery = router.location.query ?? {};
  65. router.push({
  66. pathname: location.pathname,
  67. query: normalizeDateTimeParams({...currentQuery, query}),
  68. });
  69. };
  70. return (
  71. <SentryDocumentTitle title={`Crons — ${organization.slug}`}>
  72. <Layout.Page>
  73. <Layout.Header>
  74. <Layout.HeaderContent>
  75. <Layout.Title>
  76. {t('Cron Monitors')}
  77. <PageHeadingQuestionTooltip
  78. title={t(
  79. 'Scheduled monitors that check in on recurring jobs and tell you if they’re running on schedule, failing, or succeeding.'
  80. )}
  81. docsUrl="https://docs.sentry.io/product/crons/"
  82. />
  83. <FeatureBadge type="beta" />
  84. </Layout.Title>
  85. </Layout.HeaderContent>
  86. <Layout.HeaderActions>
  87. <ButtonBar gap={1}>
  88. <CronsFeedbackButton />
  89. <NewMonitorButton size="sm" icon={<IconAdd isCircled size="xs" />}>
  90. {t('Add Monitor')}
  91. </NewMonitorButton>
  92. </ButtonBar>
  93. </Layout.HeaderActions>
  94. </Layout.Header>
  95. <Layout.Body>
  96. <Layout.Main fullWidth>
  97. <Filters>
  98. <PageFilterBar>
  99. <ProjectPageFilter resetParamsOnChange={['cursor']} />
  100. <EnvironmentPageFilter resetParamsOnChange={['cursor']} />
  101. </PageFilterBar>
  102. <SearchBar
  103. query={decodeScalar(qs.parse(location.search)?.query, '')}
  104. placeholder={t('Search by name or slug')}
  105. onSearch={handleSearch}
  106. />
  107. </Filters>
  108. {isLoading ? (
  109. <LoadingIndicator />
  110. ) : monitorList?.length ? (
  111. <Fragment>
  112. <OverviewTimeline monitorList={monitorList} />
  113. {monitorListPageLinks && <Pagination pageLinks={monitorListPageLinks} />}
  114. </Fragment>
  115. ) : (
  116. <OnboardingPanel image={<img src={onboardingImg} />}>
  117. <h3>{t('Let Sentry monitor your recurring jobs')}</h3>
  118. <p>
  119. {t(
  120. "We'll tell you if your recurring jobs are running on schedule, failing, or succeeding."
  121. )}
  122. </p>
  123. <OnboardingActions gap={1}>
  124. <NewMonitorButton>{t('Set up first cron monitor')}</NewMonitorButton>
  125. <Button href="https://docs.sentry.io/product/crons" external>
  126. {t('Read docs')}
  127. </Button>
  128. </OnboardingActions>
  129. </OnboardingPanel>
  130. )}
  131. </Layout.Main>
  132. </Layout.Body>
  133. </Layout.Page>
  134. </SentryDocumentTitle>
  135. );
  136. }
  137. const Filters = styled('div')`
  138. display: grid;
  139. grid-template-columns: max-content 1fr;
  140. gap: ${space(1.5)};
  141. margin-bottom: ${space(2)};
  142. `;
  143. const OnboardingActions = styled(ButtonBar)`
  144. grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
  145. `;