monitors.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  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 Access from 'sentry/components/acl/access';
  7. import Button, {ButtonProps} from 'sentry/components/button';
  8. import FeatureBadge from 'sentry/components/featureBadge';
  9. import IdBadge from 'sentry/components/idBadge';
  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 {PanelTable} 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 {decodeScalar} from 'sentry/utils/queryString';
  23. import withRouteAnalytics, {
  24. WithRouteAnalyticsProps,
  25. } from 'sentry/utils/routeAnalytics/withRouteAnalytics';
  26. import useOrganization from 'sentry/utils/useOrganization';
  27. import withOrganization from 'sentry/utils/withOrganization';
  28. // eslint-disable-next-line no-restricted-imports
  29. import withSentryRouter from 'sentry/utils/withSentryRouter';
  30. import AsyncView from 'sentry/views/asyncView';
  31. import MonitorIcon from './monitorIcon';
  32. import {Monitor} from './types';
  33. type Props = AsyncView['props'] &
  34. WithRouteAnalyticsProps &
  35. WithRouterProps<{orgId: string}> & {
  36. organization: Organization;
  37. };
  38. type State = AsyncView['state'] & {
  39. monitorList: Monitor[] | null;
  40. };
  41. function NewMonitorButton(props: ButtonProps) {
  42. const organization = useOrganization();
  43. return (
  44. <Access organization={organization} access={['project:write']}>
  45. {({hasAccess}) => (
  46. <Button
  47. to={`/organizations/${organization.slug}/monitors/create/`}
  48. priority="primary"
  49. disabled={!hasAccess}
  50. tooltipProps={{
  51. disabled: hasAccess,
  52. }}
  53. title={t(
  54. 'You must be an organization owner, manager, or admin to create a new monitor'
  55. )}
  56. {...props}
  57. >
  58. {props.children}
  59. </Button>
  60. )}
  61. </Access>
  62. );
  63. }
  64. class Monitors extends AsyncView<Props, State> {
  65. get orgSlug() {
  66. return this.props.organization.slug;
  67. }
  68. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  69. const {location} = this.props;
  70. return [
  71. [
  72. 'monitorList',
  73. `/organizations/${this.orgSlug}/monitors/`,
  74. {
  75. query: location.query,
  76. },
  77. ],
  78. ];
  79. }
  80. getTitle() {
  81. return `Monitors - ${this.orgSlug}`;
  82. }
  83. componentDidMount() {
  84. this.props.setEventNames('monitors.page_viewed', 'Monitors: Page Viewed');
  85. }
  86. handleSearch = (query: string) => {
  87. const {location, router} = this.props;
  88. router.push({
  89. pathname: location.pathname,
  90. query: normalizeDateTimeParams({
  91. ...(location.query || {}),
  92. query,
  93. }),
  94. });
  95. };
  96. renderBody() {
  97. const {monitorList, monitorListPageLinks} = this.state;
  98. const {organization} = this.props;
  99. return (
  100. <Fragment>
  101. <Layout.Header>
  102. <Layout.HeaderContent>
  103. <HeaderTitle>
  104. {t('Monitors')} <FeatureBadge type="beta" />
  105. </HeaderTitle>
  106. </Layout.HeaderContent>
  107. <Layout.HeaderActions>
  108. <NewMonitorButton size="sm">{t('New Monitor')}</NewMonitorButton>
  109. </Layout.HeaderActions>
  110. </Layout.Header>
  111. <Layout.Body>
  112. <Layout.Main fullWidth>
  113. <Filters>
  114. <ProjectPageFilter resetParamsOnChange={['cursor']} />
  115. <SearchBar
  116. query={decodeScalar(qs.parse(location.search)?.query, '')}
  117. placeholder={t('Search for monitors.')}
  118. onSearch={this.handleSearch}
  119. />
  120. </Filters>
  121. {monitorList?.length ? (
  122. <Fragment>
  123. <StyledPanelTable
  124. headers={[t('Monitor Name'), t('Last Check-In'), t('Project')]}
  125. >
  126. {monitorList?.map(monitor => (
  127. <Fragment key={monitor.id}>
  128. <MonitorName>
  129. <MonitorIcon status={monitor.status} size={16} />
  130. <StyledLink
  131. to={`/organizations/${organization.slug}/monitors/${monitor.id}/`}
  132. >
  133. {monitor.name}
  134. </StyledLink>
  135. </MonitorName>
  136. {monitor.nextCheckIn ? (
  137. <StyledTimeSince date={monitor.lastCheckIn} />
  138. ) : (
  139. <div>{t('n/a')}</div>
  140. )}
  141. <IdBadge
  142. project={monitor.project}
  143. avatarSize={18}
  144. avatarProps={{hasTooltip: true, tooltip: monitor.project.slug}}
  145. />
  146. </Fragment>
  147. ))}
  148. </StyledPanelTable>
  149. {monitorListPageLinks && (
  150. <Pagination pageLinks={monitorListPageLinks} {...this.props} />
  151. )}
  152. </Fragment>
  153. ) : (
  154. <OnboardingPanel image={<img src={onboardingImg} />}>
  155. <h3>{t('Monitor your recurring jobs')}</h3>
  156. <p>
  157. {t(
  158. '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.'
  159. )}
  160. </p>
  161. <NewMonitorButton>{t('Create a Monitor')}</NewMonitorButton>
  162. </OnboardingPanel>
  163. )}
  164. </Layout.Main>
  165. </Layout.Body>
  166. </Fragment>
  167. );
  168. }
  169. }
  170. const HeaderTitle = styled(Layout.Title)`
  171. margin-top: 0;
  172. `;
  173. const StyledLink = styled(Link)`
  174. flex: 1;
  175. margin-left: ${space(2)};
  176. `;
  177. const StyledTimeSince = styled(TimeSince)`
  178. font-variant-numeric: tabular-nums;
  179. `;
  180. const Filters = styled('div')`
  181. display: grid;
  182. grid-template-columns: minmax(auto, 300px) 1fr;
  183. gap: ${space(1.5)};
  184. margin-bottom: ${space(2)};
  185. `;
  186. const MonitorName = styled('div')`
  187. display: flex;
  188. align-items: center;
  189. `;
  190. const StyledPanelTable = styled(PanelTable)`
  191. grid-template-columns: 1fr max-content max-content;
  192. `;
  193. export default withRouteAnalytics(withSentryRouter(withOrganization(Monitors)));