monitors.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {Fragment} from 'react';
  2. import {WithRouterProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import * as qs from 'query-string';
  5. import onboardingImg from 'sentry-images/spot/onboarding-preview.svg';
  6. import {Button, ButtonProps} from 'sentry/components/button';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
  9. import FeatureBadge from 'sentry/components/featureBadge';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  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 {PanelTable} from 'sentry/components/panels';
  17. import ProjectPageFilter from 'sentry/components/projectPageFilter';
  18. import SearchBar from 'sentry/components/searchBar';
  19. import {IconAdd} from 'sentry/icons';
  20. import {t} from 'sentry/locale';
  21. import {space} from 'sentry/styles/space';
  22. import {Organization} from 'sentry/types';
  23. import {decodeScalar} from 'sentry/utils/queryString';
  24. import withRouteAnalytics, {
  25. WithRouteAnalyticsProps,
  26. } from 'sentry/utils/routeAnalytics/withRouteAnalytics';
  27. import useOrganization from 'sentry/utils/useOrganization';
  28. import withOrganization from 'sentry/utils/withOrganization';
  29. // eslint-disable-next-line no-restricted-imports
  30. import withSentryRouter from 'sentry/utils/withSentryRouter';
  31. import AsyncView from 'sentry/views/asyncView';
  32. import CronsFeedbackButton from './components/cronsFeedbackButton';
  33. import {MonitorRow} from './components/row';
  34. import {Monitor, MonitorEnvironment} from './types';
  35. type Props = AsyncView['props'] &
  36. WithRouteAnalyticsProps &
  37. WithRouterProps<{}> & {
  38. organization: Organization;
  39. };
  40. type State = AsyncView['state'] & {
  41. monitorList: Monitor[] | null;
  42. };
  43. function NewMonitorButton(props: ButtonProps) {
  44. const organization = useOrganization();
  45. return (
  46. <Button
  47. to={`/organizations/${organization.slug}/crons/create/`}
  48. priority="primary"
  49. {...props}
  50. >
  51. {props.children}
  52. </Button>
  53. );
  54. }
  55. class Monitors extends AsyncView<Props, State> {
  56. get orgSlug() {
  57. return this.props.organization.slug;
  58. }
  59. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  60. const {location} = this.props;
  61. return [
  62. [
  63. 'monitorList',
  64. `/organizations/${this.orgSlug}/monitors/?includeNew`,
  65. {
  66. query: location.query,
  67. },
  68. ],
  69. ];
  70. }
  71. getTitle() {
  72. return `Crons - ${this.orgSlug}`;
  73. }
  74. onRequestSuccess(response): void {
  75. this.props.setEventNames('monitors.page_viewed', 'Monitors: Page Viewed');
  76. this.props.setRouteAnalyticsParams({
  77. empty_state: response.data.length === 0,
  78. });
  79. }
  80. handleSearch = (query: string) => {
  81. const {location, router} = this.props;
  82. router.push({
  83. pathname: location.pathname,
  84. query: normalizeDateTimeParams({
  85. ...(location.query || {}),
  86. query,
  87. }),
  88. });
  89. };
  90. renderBody() {
  91. const {monitorList, monitorListPageLinks} = this.state;
  92. const {organization} = this.props;
  93. const renderMonitorRow = (monitor: Monitor, monitorEnv?: MonitorEnvironment) => (
  94. <MonitorRow
  95. key={monitor.slug}
  96. monitor={monitor}
  97. monitorEnv={monitorEnv}
  98. onDelete={() => {
  99. if (monitorList) {
  100. this.setState({
  101. monitorList: monitorList.filter(m => m.slug !== monitor.slug),
  102. });
  103. }
  104. }}
  105. organization={organization}
  106. />
  107. );
  108. return (
  109. <Layout.Page>
  110. <Layout.Header>
  111. <Layout.HeaderContent>
  112. <Layout.Title>
  113. {t('Cron Monitors')}
  114. <PageHeadingQuestionTooltip
  115. title={t(
  116. 'Scheduled monitors that check in on recurring jobs and tell you if they’re running on schedule, failing, or succeeding.'
  117. )}
  118. docsUrl="https://docs.sentry.io/product/crons/"
  119. />
  120. <FeatureBadge type="beta" />
  121. </Layout.Title>
  122. </Layout.HeaderContent>
  123. <Layout.HeaderActions>
  124. <ButtonBar gap={1}>
  125. <CronsFeedbackButton />
  126. <NewMonitorButton size="sm" icon={<IconAdd isCircled size="xs" />}>
  127. {t('Add Monitor')}
  128. </NewMonitorButton>
  129. </ButtonBar>
  130. </Layout.HeaderActions>
  131. </Layout.Header>
  132. <Layout.Body>
  133. <Layout.Main fullWidth>
  134. <Filters>
  135. <PageFilterBar>
  136. <ProjectPageFilter resetParamsOnChange={['cursor']} />
  137. <EnvironmentPageFilter resetParamsOnChange={['cursor']} />
  138. </PageFilterBar>
  139. <SearchBar
  140. query={decodeScalar(qs.parse(location.search)?.query, '')}
  141. placeholder={t('Search by name')}
  142. onSearch={this.handleSearch}
  143. />
  144. </Filters>
  145. {monitorList?.length ? (
  146. <Fragment>
  147. <StyledPanelTable
  148. headers={[
  149. t('Monitor Name'),
  150. t('Status'),
  151. t('Schedule'),
  152. t('Next Checkin'),
  153. t('Project'),
  154. t('Environment'),
  155. t('Actions'),
  156. ]}
  157. >
  158. {monitorList
  159. ?.map(monitor =>
  160. monitor.environments.length > 0
  161. ? monitor.environments.map(monitorEnv =>
  162. renderMonitorRow(monitor, monitorEnv)
  163. )
  164. : renderMonitorRow(monitor)
  165. )
  166. .flat()}
  167. </StyledPanelTable>
  168. {monitorListPageLinks && (
  169. <Pagination pageLinks={monitorListPageLinks} {...this.props} />
  170. )}
  171. </Fragment>
  172. ) : (
  173. <OnboardingPanel image={<img src={onboardingImg} />}>
  174. <h3>{t('Let Sentry monitor your recurring jobs')}</h3>
  175. <p>
  176. {t(
  177. "We'll tell you if your recurring jobs are running on schedule, failing, or succeeding."
  178. )}
  179. </p>
  180. <ButtonList gap={1}>
  181. <NewMonitorButton>{t('Set up first cron monitor')}</NewMonitorButton>
  182. <Button href="https://docs.sentry.io/product/crons" external>
  183. {t('Read docs')}
  184. </Button>
  185. </ButtonList>
  186. </OnboardingPanel>
  187. )}
  188. </Layout.Main>
  189. </Layout.Body>
  190. </Layout.Page>
  191. );
  192. }
  193. }
  194. const Filters = styled('div')`
  195. display: grid;
  196. grid-template-columns: max-content 1fr;
  197. gap: ${space(1.5)};
  198. margin-bottom: ${space(2)};
  199. `;
  200. const StyledPanelTable = styled(PanelTable)`
  201. grid-template-columns: 1fr max-content 1fr max-content max-content max-content max-content;
  202. `;
  203. const ButtonList = styled(ButtonBar)`
  204. grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
  205. `;
  206. export default withRouteAnalytics(withSentryRouter(withOrganization(Monitors)));