overview.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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 Alert from 'sentry/components/alert';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import FeatureBadge from 'sentry/components/featureBadge';
  8. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  9. import HookOrDefault from 'sentry/components/hookOrDefault';
  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 {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  14. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  15. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  16. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  17. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  18. import Pagination from 'sentry/components/pagination';
  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 {useApiQuery} 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 useRouter from 'sentry/utils/useRouter';
  30. import {
  31. CronsLandingPanel,
  32. isValidGuide,
  33. isValidPlatform,
  34. } from './components/cronsLandingPanel';
  35. import {NewMonitorButton} from './components/newMonitorButton';
  36. import {OverviewTimeline} from './components/overviewTimeline';
  37. import {Monitor} from './types';
  38. import {makeMonitorListQueryKey} from './utils';
  39. function DisabledMonitorCreationPanel() {
  40. return (
  41. <OnboardingPanel image={<img src={onboardingImg} />}>
  42. <h3>{t('Monitor Your Cron Jobs')}</h3>
  43. <Alert type="warning" showIcon>
  44. {t(
  45. 'The Crons beta is over. Adding new monitors for projects without existing ones is temporarily disabled until our launch preparations are complete. Please try again after January 11th, 2024.'
  46. )}
  47. </Alert>
  48. </OnboardingPanel>
  49. );
  50. }
  51. const CronsListPageHeader = HookOrDefault({
  52. hookName: 'component:crons-list-page-header',
  53. });
  54. export default function Monitors() {
  55. const organization = useOrganization();
  56. const router = useRouter();
  57. const platform = decodeScalar(router.location.query?.platform) ?? null;
  58. const guide = decodeScalar(router.location.query?.guide);
  59. const queryKey = makeMonitorListQueryKey(organization, router.location);
  60. const {
  61. data: monitorList,
  62. getResponseHeader: monitorListHeaders,
  63. isLoading,
  64. } = useApiQuery<Monitor[]>(queryKey, {
  65. staleTime: 0,
  66. });
  67. useRouteAnalyticsEventNames('monitors.page_viewed', 'Monitors: Page Viewed');
  68. useRouteAnalyticsParams({empty_state: !monitorList || monitorList.length === 0});
  69. const monitorListPageLinks = monitorListHeaders?.('Link');
  70. const handleSearch = (query: string) => {
  71. const currentQuery = {...(router.location.query ?? {}), cursor: undefined};
  72. router.push({
  73. pathname: location.pathname,
  74. query: normalizeDateTimeParams({...currentQuery, query}),
  75. });
  76. };
  77. const disableNewMonitors =
  78. organization.features.includes('crons-disable-new-projects') &&
  79. (isLoading || monitorList?.length === 0);
  80. const showAddMonitor = !isValidPlatform(platform) || !isValidGuide(guide);
  81. return (
  82. <SentryDocumentTitle title={`Crons — ${organization.slug}`}>
  83. <CronsListPageHeader organization={organization} />
  84. <Layout.Page>
  85. <Layout.Header>
  86. <Layout.HeaderContent>
  87. <Layout.Title>
  88. {t('Cron Monitors')}
  89. <PageHeadingQuestionTooltip
  90. title={t(
  91. 'Scheduled monitors that check in on recurring jobs and tell you if they’re running on schedule, failing, or succeeding.'
  92. )}
  93. docsUrl="https://docs.sentry.io/product/crons/"
  94. />
  95. <FeatureBadge type="beta" />
  96. </Layout.Title>
  97. </Layout.HeaderContent>
  98. <Layout.HeaderActions>
  99. <ButtonBar gap={1}>
  100. <FeedbackWidgetButton />
  101. {showAddMonitor && (
  102. <NewMonitorButton
  103. size="sm"
  104. icon={<IconAdd isCircled />}
  105. disabled={disableNewMonitors}
  106. title={
  107. disableNewMonitors &&
  108. t(
  109. 'Adding new monitors for projects without existing ones is temporarily disabled until our launch preparations are complete.'
  110. )
  111. }
  112. >
  113. {t('Add Monitor')}
  114. </NewMonitorButton>
  115. )}
  116. </ButtonBar>
  117. </Layout.HeaderActions>
  118. </Layout.Header>
  119. <Layout.Body>
  120. <Layout.Main fullWidth>
  121. <Filters>
  122. <PageFilterBar>
  123. <ProjectPageFilter resetParamsOnChange={['cursor']} />
  124. <EnvironmentPageFilter resetParamsOnChange={['cursor']} />
  125. </PageFilterBar>
  126. <SearchBar
  127. query={decodeScalar(qs.parse(location.search)?.query, '')}
  128. placeholder={t('Search by name or slug')}
  129. onSearch={handleSearch}
  130. />
  131. </Filters>
  132. {isLoading ? (
  133. <LoadingIndicator />
  134. ) : monitorList?.length ? (
  135. <Fragment>
  136. <OverviewTimeline monitorList={monitorList} />
  137. {monitorListPageLinks && <Pagination pageLinks={monitorListPageLinks} />}
  138. </Fragment>
  139. ) : disableNewMonitors ? (
  140. <DisabledMonitorCreationPanel />
  141. ) : (
  142. <CronsLandingPanel />
  143. )}
  144. </Layout.Main>
  145. </Layout.Body>
  146. </Layout.Page>
  147. </SentryDocumentTitle>
  148. );
  149. }
  150. const Filters = styled('div')`
  151. display: grid;
  152. grid-template-columns: max-content 1fr;
  153. gap: ${space(1.5)};
  154. margin-bottom: ${space(2)};
  155. `;