monitors.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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 * as Layout from 'sentry/components/layouts/thirds';
  11. import Link from 'sentry/components/links/link';
  12. import OnboardingPanel from 'sentry/components/onboardingPanel';
  13. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  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 space from 'sentry/styles/space';
  21. import {Organization} from 'sentry/types';
  22. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  23. import {decodeScalar} from 'sentry/utils/queryString';
  24. import useOrganization from 'sentry/utils/useOrganization';
  25. import withOrganization from 'sentry/utils/withOrganization';
  26. import AsyncView from 'sentry/views/asyncView';
  27. import MonitorIcon from './monitorIcon';
  28. import {Monitor} from './types';
  29. type Props = AsyncView['props'] &
  30. WithRouterProps<{orgId: string}> & {
  31. organization: Organization;
  32. };
  33. type State = AsyncView['state'] & {
  34. monitorList: Monitor[] | null;
  35. };
  36. function NewMonitorButton(props: ButtonProps) {
  37. const organization = useOrganization();
  38. return (
  39. <Access organization={organization} access={['project:write']}>
  40. {({hasAccess}) => (
  41. <Button
  42. to={`/organizations/${organization.slug}/monitors/create/`}
  43. priority="primary"
  44. disabled={!hasAccess}
  45. tooltipProps={{
  46. disabled: hasAccess,
  47. }}
  48. title={t(
  49. 'You must be an organization owner, manager, or admin to create a new monitor'
  50. )}
  51. {...props}
  52. >
  53. {props.children}
  54. </Button>
  55. )}
  56. </Access>
  57. );
  58. }
  59. class Monitors extends AsyncView<Props, State> {
  60. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  61. const {params, location} = this.props;
  62. return [
  63. [
  64. 'monitorList',
  65. `/organizations/${params.orgId}/monitors/`,
  66. {
  67. query: location.query,
  68. },
  69. ],
  70. ];
  71. }
  72. getTitle() {
  73. return `Monitors - ${this.props.params.orgId}`;
  74. }
  75. componentDidMount() {
  76. trackAdvancedAnalyticsEvent('monitors.page_viewed', {
  77. organization: this.props.organization.id,
  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. return (
  94. <Fragment>
  95. <Layout.Header>
  96. <Layout.HeaderContent>
  97. <HeaderTitle>
  98. {t('Monitors')} <FeatureBadge type="beta" />
  99. </HeaderTitle>
  100. </Layout.HeaderContent>
  101. <Layout.HeaderActions>
  102. <NewMonitorButton size="sm">{t('New Monitor')}</NewMonitorButton>
  103. </Layout.HeaderActions>
  104. </Layout.Header>
  105. <Layout.Body>
  106. <Layout.Main fullWidth>
  107. <Filters>
  108. <ProjectPageFilter resetParamsOnChange={['cursor']} />
  109. <SearchBar
  110. query={decodeScalar(qs.parse(location.search)?.query, '')}
  111. placeholder={t('Search for monitors.')}
  112. onSearch={this.handleSearch}
  113. />
  114. </Filters>
  115. {monitorList?.length ? (
  116. <Fragment>
  117. <Panel>
  118. <PanelBody>
  119. {monitorList?.map(monitor => (
  120. <PanelItemCentered key={monitor.id}>
  121. <MonitorIcon status={monitor.status} size={16} />
  122. <StyledLink
  123. to={`/organizations/${organization.slug}/monitors/${monitor.id}/`}
  124. >
  125. {monitor.name}
  126. </StyledLink>
  127. {monitor.nextCheckIn ? (
  128. <StyledTimeSince date={monitor.lastCheckIn} />
  129. ) : (
  130. t('n/a')
  131. )}
  132. </PanelItemCentered>
  133. ))}
  134. </PanelBody>
  135. </Panel>
  136. {monitorListPageLinks && (
  137. <Pagination pageLinks={monitorListPageLinks} {...this.props} />
  138. )}
  139. </Fragment>
  140. ) : (
  141. <OnboardingPanel image={<img src={onboardingImg} />}>
  142. <h3>{t('Monitor your recurring jobs')}</h3>
  143. <p>
  144. {t(
  145. '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.'
  146. )}
  147. </p>
  148. <NewMonitorButton>{t('Create a Monitor')}</NewMonitorButton>
  149. </OnboardingPanel>
  150. )}
  151. </Layout.Main>
  152. </Layout.Body>
  153. </Fragment>
  154. );
  155. }
  156. }
  157. const HeaderTitle = styled(Layout.Title)`
  158. margin-top: 0;
  159. `;
  160. const PanelItemCentered = styled(PanelItem)`
  161. align-items: center;
  162. padding: 0;
  163. padding-left: ${space(2)};
  164. padding-right: ${space(2)};
  165. `;
  166. const StyledLink = styled(Link)`
  167. flex: 1;
  168. padding: ${space(2)};
  169. `;
  170. const StyledTimeSince = styled(TimeSince)`
  171. font-variant-numeric: tabular-nums;
  172. `;
  173. const Filters = styled('div')`
  174. display: grid;
  175. grid-template-columns: minmax(auto, 300px) 1fr;
  176. gap: ${space(1.5)};
  177. margin-bottom: ${space(2)};
  178. `;
  179. export default withRouter(withOrganization(Monitors));