overview.tsx 5.7 KB

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