overview.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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 {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  12. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  13. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  14. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  15. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  16. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  17. import Pagination from 'sentry/components/pagination';
  18. import SearchBar from 'sentry/components/searchBar';
  19. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  20. import {IconAdd, IconList} from 'sentry/icons';
  21. import {t} from 'sentry/locale';
  22. import {space} from 'sentry/styles/space';
  23. import {useApiQuery} from 'sentry/utils/queryClient';
  24. import {decodeList, decodeScalar} from 'sentry/utils/queryString';
  25. import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
  26. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  27. import {useLocation} from 'sentry/utils/useLocation';
  28. import {useNavigate} from 'sentry/utils/useNavigate';
  29. import useOrganization from 'sentry/utils/useOrganization';
  30. import MonitorProcessingErrors from 'sentry/views/monitors/components/processingErrors/monitorProcessingErrors';
  31. import {makeMonitorListErrorsQueryKey} from 'sentry/views/monitors/components/processingErrors/utils';
  32. import {
  33. CronsLandingPanel,
  34. isValidGuide,
  35. isValidPlatform,
  36. } from './components/cronsLandingPanel';
  37. import {NewMonitorButton} from './components/newMonitorButton';
  38. import {OverviewTimeline} from './components/overviewTimeline';
  39. import {OwnerFilter} from './components/ownerFilter';
  40. import type {CheckinProcessingError, Monitor} from './types';
  41. import {makeMonitorListQueryKey} from './utils';
  42. const CronsListPageHeader = HookOrDefault({
  43. hookName: 'component:crons-list-page-header',
  44. });
  45. export default function Monitors() {
  46. const organization = useOrganization();
  47. const navigate = useNavigate();
  48. const location = useLocation();
  49. const platform = decodeScalar(location.query?.platform) ?? null;
  50. const guide = decodeScalar(location.query?.guide);
  51. const project = decodeList(location.query?.project);
  52. const queryKey = makeMonitorListQueryKey(organization, location.query);
  53. const {
  54. data: monitorList,
  55. getResponseHeader: monitorListHeaders,
  56. isLoading,
  57. refetch,
  58. } = useApiQuery<Monitor[]>(queryKey, {
  59. staleTime: 0,
  60. });
  61. const processingErrorQueryKey = makeMonitorListErrorsQueryKey(organization, project);
  62. const {data: processingErrors} = useApiQuery<CheckinProcessingError[]>(
  63. processingErrorQueryKey,
  64. {
  65. staleTime: 0,
  66. }
  67. );
  68. useRouteAnalyticsEventNames('monitors.page_viewed', 'Monitors: Page Viewed');
  69. useRouteAnalyticsParams({empty_state: !monitorList || monitorList.length === 0});
  70. const monitorListPageLinks = monitorListHeaders?.('Link');
  71. const handleSearch = (query: string) => {
  72. const currentQuery = {...(location.query ?? {}), cursor: undefined};
  73. navigate({
  74. pathname: location.pathname,
  75. query: normalizeDateTimeParams({...currentQuery, query}),
  76. });
  77. };
  78. const showAddMonitor = !isValidPlatform(platform) || !isValidGuide(guide);
  79. return (
  80. <SentryDocumentTitle title={`Crons — ${organization.slug}`}>
  81. <CronsListPageHeader organization={organization} />
  82. <Layout.Page>
  83. <Layout.Header>
  84. <Layout.HeaderContent>
  85. <Layout.Title>
  86. {t('Cron Monitors')}
  87. <PageHeadingQuestionTooltip
  88. title={t(
  89. 'Scheduled monitors that check in on recurring jobs and tell you if they’re running on schedule, failing, or succeeding.'
  90. )}
  91. docsUrl="https://docs.sentry.io/product/crons/"
  92. />
  93. </Layout.Title>
  94. </Layout.HeaderContent>
  95. <Layout.HeaderActions>
  96. <ButtonBar gap={1}>
  97. <FeedbackWidgetButton />
  98. <Button
  99. icon={<IconList />}
  100. size="sm"
  101. onClick={() =>
  102. openBulkEditMonitorsModal({
  103. onClose: refetch,
  104. })
  105. }
  106. analyticsEventKey="crons.bulk_edit_modal_button_clicked"
  107. analyticsEventName="Crons: Bulk Edit Modal Button Clicked"
  108. >
  109. {t('Manage Monitors')}
  110. </Button>
  111. {showAddMonitor && (
  112. <NewMonitorButton size="sm" icon={<IconAdd isCircled />}>
  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. <OwnerFilter
  123. selectedOwners={decodeList(location.query.owner)}
  124. onChangeFilter={owner => {
  125. navigate(
  126. {
  127. ...location,
  128. query: {...location.query, owner},
  129. },
  130. {replace: true}
  131. );
  132. }}
  133. />
  134. <PageFilterBar>
  135. <ProjectPageFilter resetParamsOnChange={['cursor']} />
  136. <EnvironmentPageFilter resetParamsOnChange={['cursor']} />
  137. <DatePageFilter resetParamsOnChange={['cursor']} />
  138. </PageFilterBar>
  139. <SearchBar
  140. query={decodeScalar(qs.parse(location.search)?.query, '')}
  141. placeholder={t('Search by name or slug')}
  142. onSearch={handleSearch}
  143. />
  144. </Filters>
  145. {!!processingErrors?.length && (
  146. <MonitorProcessingErrors checkinErrors={processingErrors}>
  147. {t(
  148. 'Errors were encountered while ingesting check-ins for the selected projects'
  149. )}
  150. </MonitorProcessingErrors>
  151. )}
  152. {isLoading ? (
  153. <LoadingIndicator />
  154. ) : monitorList?.length ? (
  155. <Fragment>
  156. <OverviewTimeline monitorList={monitorList} />
  157. {monitorListPageLinks && <Pagination pageLinks={monitorListPageLinks} />}
  158. </Fragment>
  159. ) : (
  160. <CronsLandingPanel />
  161. )}
  162. </Layout.Main>
  163. </Layout.Body>
  164. </Layout.Page>
  165. </SentryDocumentTitle>
  166. );
  167. }
  168. const Filters = styled('div')`
  169. display: flex;
  170. gap: ${space(1.5)};
  171. margin-bottom: ${space(2)};
  172. > :last-child {
  173. flex-grow: 1;
  174. }
  175. `;