overview.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as qs from 'query-string';
  4. import ButtonBar from 'sentry/components/buttonBar';
  5. import FeatureBadge from 'sentry/components/featureBadge';
  6. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  7. import * as Layout from 'sentry/components/layouts/thirds';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  10. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  11. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  12. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  13. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  14. import Pagination from 'sentry/components/pagination';
  15. import SearchBar from 'sentry/components/searchBar';
  16. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  17. import {IconAdd} from 'sentry/icons';
  18. import {t} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import {useApiQuery} from 'sentry/utils/queryClient';
  21. import {decodeScalar} from 'sentry/utils/queryString';
  22. import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
  23. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  24. import useOrganization from 'sentry/utils/useOrganization';
  25. import useRouter from 'sentry/utils/useRouter';
  26. import {
  27. CronsLandingPanel,
  28. isValidGuide,
  29. isValidPlatform,
  30. } from './components/cronsLandingPanel';
  31. import {NewMonitorButton} from './components/newMonitorButton';
  32. import {OverviewTimeline} from './components/overviewTimeline';
  33. import {Monitor} from './types';
  34. import {makeMonitorListQueryKey} from './utils';
  35. export default function Monitors() {
  36. const organization = useOrganization();
  37. const router = useRouter();
  38. const platform = decodeScalar(router.location.query?.platform) ?? null;
  39. const guide = decodeScalar(router.location.query?.guide);
  40. const queryKey = makeMonitorListQueryKey(organization, router.location);
  41. const {
  42. data: monitorList,
  43. getResponseHeader: monitorListHeaders,
  44. isLoading,
  45. } = useApiQuery<Monitor[]>(queryKey, {
  46. staleTime: 0,
  47. });
  48. useRouteAnalyticsEventNames('monitors.page_viewed', 'Monitors: Page Viewed');
  49. useRouteAnalyticsParams({empty_state: !monitorList || monitorList.length === 0});
  50. const monitorListPageLinks = monitorListHeaders?.('Link');
  51. const handleSearch = (query: string) => {
  52. const currentQuery = {...(router.location.query ?? {}), cursor: undefined};
  53. router.push({
  54. pathname: location.pathname,
  55. query: normalizeDateTimeParams({...currentQuery, query}),
  56. });
  57. };
  58. // Only show the add monitor button if there is no currently displayed guide
  59. const showAddMonitor = !isValidPlatform(platform) || !isValidGuide(guide);
  60. return (
  61. <SentryDocumentTitle title={`Crons — ${organization.slug}`}>
  62. <Layout.Page>
  63. <Layout.Header>
  64. <Layout.HeaderContent>
  65. <Layout.Title>
  66. {t('Cron Monitors')}
  67. <PageHeadingQuestionTooltip
  68. title={t(
  69. 'Scheduled monitors that check in on recurring jobs and tell you if they’re running on schedule, failing, or succeeding.'
  70. )}
  71. docsUrl="https://docs.sentry.io/product/crons/"
  72. />
  73. <FeatureBadge type="beta" />
  74. </Layout.Title>
  75. </Layout.HeaderContent>
  76. <Layout.HeaderActions>
  77. <ButtonBar gap={1}>
  78. <FeedbackWidgetButton />
  79. {showAddMonitor && (
  80. <NewMonitorButton size="sm" icon={<IconAdd isCircled />}>
  81. {t('Add Monitor')}
  82. </NewMonitorButton>
  83. )}
  84. </ButtonBar>
  85. </Layout.HeaderActions>
  86. </Layout.Header>
  87. <Layout.Body>
  88. <Layout.Main fullWidth>
  89. <Filters>
  90. <PageFilterBar>
  91. <ProjectPageFilter resetParamsOnChange={['cursor']} />
  92. <EnvironmentPageFilter resetParamsOnChange={['cursor']} />
  93. </PageFilterBar>
  94. <SearchBar
  95. query={decodeScalar(qs.parse(location.search)?.query, '')}
  96. placeholder={t('Search by name or slug')}
  97. onSearch={handleSearch}
  98. />
  99. </Filters>
  100. {isLoading ? (
  101. <LoadingIndicator />
  102. ) : monitorList?.length ? (
  103. <Fragment>
  104. <OverviewTimeline monitorList={monitorList} />
  105. {monitorListPageLinks && <Pagination pageLinks={monitorListPageLinks} />}
  106. </Fragment>
  107. ) : (
  108. <CronsLandingPanel />
  109. )}
  110. </Layout.Main>
  111. </Layout.Body>
  112. </Layout.Page>
  113. </SentryDocumentTitle>
  114. );
  115. }
  116. const Filters = styled('div')`
  117. display: grid;
  118. grid-template-columns: max-content 1fr;
  119. gap: ${space(1.5)};
  120. margin-bottom: ${space(2)};
  121. `;