monitors.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import {Fragment} from 'react';
  2. // eslint-disable-next-line no-restricted-imports
  3. import {withRouter, WithRouterProps} from 'react-router';
  4. import styled from '@emotion/styled';
  5. import * as qs from 'query-string';
  6. import onboardingImg from 'sentry-images/spot/onboarding-preview.svg';
  7. import Access from 'sentry/components/acl/access';
  8. import Button, {ButtonProps} from 'sentry/components/button';
  9. import FeatureBadge from 'sentry/components/featureBadge';
  10. import Link from 'sentry/components/links/link';
  11. import OnboardingPanel from 'sentry/components/onboardingPanel';
  12. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  13. import PageHeading from 'sentry/components/pageHeading';
  14. import Pagination from 'sentry/components/pagination';
  15. import {Panel, PanelBody, PanelItem} from 'sentry/components/panels';
  16. import ProjectPageFilter from 'sentry/components/projectPageFilter';
  17. import SearchBar from 'sentry/components/searchBar';
  18. import TimeSince from 'sentry/components/timeSince';
  19. import {t} from 'sentry/locale';
  20. import {PageHeader} from 'sentry/styles/organization';
  21. import space from 'sentry/styles/space';
  22. import {Organization} from 'sentry/types';
  23. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  24. import {decodeScalar} from 'sentry/utils/queryString';
  25. import useOrganization from 'sentry/utils/useOrganization';
  26. import withOrganization from 'sentry/utils/withOrganization';
  27. import AsyncView from 'sentry/views/asyncView';
  28. import MonitorIcon from './monitorIcon';
  29. import {Monitor} from './types';
  30. type Props = AsyncView['props'] &
  31. WithRouterProps<{orgId: string}> & {
  32. organization: Organization;
  33. };
  34. type State = AsyncView['state'] & {
  35. monitorList: Monitor[] | null;
  36. };
  37. function NewMonitorButton(props: ButtonProps) {
  38. const organization = useOrganization();
  39. return (
  40. <Access organization={organization} access={['project:write']}>
  41. {({hasAccess}) => (
  42. <Button
  43. to={`/organizations/${organization.slug}/monitors/create/`}
  44. priority="primary"
  45. disabled={!hasAccess}
  46. tooltipProps={{
  47. disabled: hasAccess,
  48. }}
  49. title={t(
  50. 'You must be an organization owner, manager, or admin to create a new monitor'
  51. )}
  52. {...props}
  53. >
  54. {props.children}
  55. </Button>
  56. )}
  57. </Access>
  58. );
  59. }
  60. class Monitors extends AsyncView<Props, State> {
  61. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  62. const {params, location} = this.props;
  63. return [
  64. [
  65. 'monitorList',
  66. `/organizations/${params.orgId}/monitors/`,
  67. {
  68. query: location.query,
  69. },
  70. ],
  71. ];
  72. }
  73. getTitle() {
  74. return `Monitors - ${this.props.params.orgId}`;
  75. }
  76. componentDidMount() {
  77. trackAdvancedAnalyticsEvent('monitors.page_viewed', {
  78. organization: this.props.organization.id,
  79. });
  80. }
  81. handleSearch = (query: string) => {
  82. const {location, router} = this.props;
  83. router.push({
  84. pathname: location.pathname,
  85. query: normalizeDateTimeParams({
  86. ...(location.query || {}),
  87. query,
  88. }),
  89. });
  90. };
  91. renderBody() {
  92. const {monitorList, monitorListPageLinks} = this.state;
  93. const {organization} = this.props;
  94. return (
  95. <Fragment>
  96. <PageHeader>
  97. <HeaderTitle>
  98. <div>
  99. {t('Monitors')} <FeatureBadge type="beta" />
  100. </div>
  101. <NewMonitorButton size="sm">{t('New Monitor')}</NewMonitorButton>
  102. </HeaderTitle>
  103. </PageHeader>
  104. <Filters>
  105. <ProjectPageFilter resetParamsOnChange={['cursor']} />
  106. <SearchBar
  107. query={decodeScalar(qs.parse(location.search)?.query, '')}
  108. placeholder={t('Search for monitors.')}
  109. onSearch={this.handleSearch}
  110. />
  111. </Filters>
  112. {monitorList?.length ? (
  113. <Fragment>
  114. <Panel>
  115. <PanelBody>
  116. {monitorList?.map(monitor => (
  117. <PanelItemCentered key={monitor.id}>
  118. <MonitorIcon status={monitor.status} size={16} />
  119. <StyledLink
  120. to={`/organizations/${organization.slug}/monitors/${monitor.id}/`}
  121. >
  122. {monitor.name}
  123. </StyledLink>
  124. {monitor.nextCheckIn ? (
  125. <StyledTimeSince date={monitor.lastCheckIn} />
  126. ) : (
  127. t('n/a')
  128. )}
  129. </PanelItemCentered>
  130. ))}
  131. </PanelBody>
  132. </Panel>
  133. {monitorListPageLinks && (
  134. <Pagination pageLinks={monitorListPageLinks} {...this.props} />
  135. )}
  136. </Fragment>
  137. ) : (
  138. <OnboardingPanel image={<img src={onboardingImg} />}>
  139. <h3>{t('Monitor your recurring jobs')}</h3>
  140. <p>
  141. {t(
  142. 'Stop worrying about the status of your cron jobs. Let us notify you when your jobs take too long or do not execute on schedule.'
  143. )}
  144. </p>
  145. <NewMonitorButton>{t('Create a Monitor')}</NewMonitorButton>
  146. </OnboardingPanel>
  147. )}
  148. </Fragment>
  149. );
  150. }
  151. }
  152. const HeaderTitle = styled(PageHeading)`
  153. display: flex;
  154. align-items: center;
  155. justify-content: space-between;
  156. flex: 1;
  157. `;
  158. const PanelItemCentered = styled(PanelItem)`
  159. align-items: center;
  160. padding: 0;
  161. padding-left: ${space(2)};
  162. padding-right: ${space(2)};
  163. `;
  164. const StyledLink = styled(Link)`
  165. flex: 1;
  166. padding: ${space(2)};
  167. `;
  168. const StyledTimeSince = styled(TimeSince)`
  169. font-variant-numeric: tabular-nums;
  170. `;
  171. const Filters = styled('div')`
  172. display: grid;
  173. grid-template-columns: minmax(auto, 300px) 1fr;
  174. gap: ${space(1.5)};
  175. margin-bottom: ${space(2)};
  176. `;
  177. export default withRouter(withOrganization(Monitors));