monitors.tsx 6.5 KB

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