row.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {deleteMonitor} from 'sentry/actionCreators/monitors';
  4. import {openConfirmModal} from 'sentry/components/confirm';
  5. import {DropdownMenu, MenuItemProps} from 'sentry/components/dropdownMenu';
  6. import IdBadge from 'sentry/components/idBadge';
  7. import Link from 'sentry/components/links/link';
  8. import List from 'sentry/components/list';
  9. import ListItem from 'sentry/components/list/listItem';
  10. import Text from 'sentry/components/text';
  11. import TextOverflow from 'sentry/components/textOverflow';
  12. import TimeSince from 'sentry/components/timeSince';
  13. import {IconEllipsis} from 'sentry/icons';
  14. import {t, tct, tn} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import {Organization} from 'sentry/types';
  17. import useApi from 'sentry/utils/useApi';
  18. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  19. import {crontabAsText} from 'sentry/views/monitors/utils';
  20. import {
  21. Monitor,
  22. MonitorConfig,
  23. MonitorEnvironment,
  24. MonitorStatus,
  25. ScheduleType,
  26. } from '../types';
  27. import {MonitorBadge} from './monitorBadge';
  28. interface MonitorRowProps {
  29. monitor: Monitor;
  30. onDelete: () => void;
  31. organization: Organization;
  32. monitorEnv?: MonitorEnvironment;
  33. }
  34. function scheduleAsText(config: MonitorConfig) {
  35. // Crontab format uses cronstrue
  36. if (config.schedule_type === ScheduleType.CRONTAB) {
  37. const parsedSchedule = crontabAsText(config.schedule);
  38. return parsedSchedule ?? t('Unknown schedule');
  39. }
  40. // Interval format is simpler
  41. const [value, timeUnit] = config.schedule;
  42. if (timeUnit === 'minute') {
  43. return tn('Every minute', 'Every %s minutes', value);
  44. }
  45. if (timeUnit === 'hour') {
  46. return tn('Every hour', 'Every %s hours', value);
  47. }
  48. if (timeUnit === 'day') {
  49. return tn('Every day', 'Every %s days', value);
  50. }
  51. if (timeUnit === 'week') {
  52. return tn('Every week', 'Every %s weeks', value);
  53. }
  54. if (timeUnit === 'month') {
  55. return tn('Every month', 'Every %s months', value);
  56. }
  57. return t('Unknown schedule');
  58. }
  59. function MonitorRow({monitor, monitorEnv, organization, onDelete}: MonitorRowProps) {
  60. const api = useApi();
  61. const lastCheckin = monitorEnv?.lastCheckIn ? (
  62. <TimeSince unitStyle="regular" date={monitorEnv.lastCheckIn} />
  63. ) : null;
  64. const deletionModalMessage = (
  65. <Fragment>
  66. <Text>
  67. {tct('Are you sure you want to permanently delete "[name]"?', {
  68. name: monitor.name,
  69. })}
  70. </Text>
  71. {monitor.environments.length > 1 && (
  72. <AdditionalEnvironmentWarning>
  73. <Text>
  74. {t(
  75. `This will delete check-in data for this monitor associated with these environments:`
  76. )}
  77. </Text>
  78. <List symbol="bullet">
  79. {monitor.environments.map(environment => (
  80. <ListItem key={environment.name}>{environment.name}</ListItem>
  81. ))}
  82. </List>
  83. </AdditionalEnvironmentWarning>
  84. )}
  85. </Fragment>
  86. );
  87. const actions: MenuItemProps[] = [
  88. {
  89. key: 'edit',
  90. label: t('Edit'),
  91. // TODO(davidenwang): Right now we have to pass the environment
  92. // through the URL so that when we save the monitor and are
  93. // redirected back to the details page it queries the backend
  94. // for a monitor environment with check-in data
  95. to: normalizeUrl({
  96. pathname: `/organizations/${organization.slug}/crons/${monitor.slug}/edit/`,
  97. query: {environment: monitorEnv?.name},
  98. }),
  99. },
  100. {
  101. key: 'delete',
  102. label: t('Delete'),
  103. priority: 'danger',
  104. onAction: () => {
  105. openConfirmModal({
  106. onConfirm: async () => {
  107. await deleteMonitor(api, organization.slug, monitor.slug);
  108. onDelete();
  109. },
  110. header: t('Delete Monitor?'),
  111. message: deletionModalMessage,
  112. confirmText: t('Delete Monitor'),
  113. priority: 'danger',
  114. });
  115. },
  116. },
  117. ];
  118. const monitorDetailUrl = `/organizations/${organization.slug}/crons/${monitor.slug}/${
  119. monitorEnv ? `?environment=${monitorEnv.name}` : ''
  120. }`;
  121. // TODO(davidenwang): Change accordingly when we have ObjectStatus on monitor
  122. const monitorStatus =
  123. monitor.status !== 'disabled' && monitorEnv ? monitorEnv.status : monitor.status;
  124. return (
  125. <Fragment>
  126. <MonitorName>
  127. <MonitorBadge status={monitorStatus} />
  128. <NameAndSlug>
  129. <Link to={monitorDetailUrl}>{monitor.name}</Link>
  130. <MonitorSlug>{monitor.slug}</MonitorSlug>
  131. </NameAndSlug>
  132. </MonitorName>
  133. <MonitorColumn>
  134. <TextOverflow>
  135. {monitorStatus === MonitorStatus.DISABLED
  136. ? t('Paused')
  137. : monitorStatus === MonitorStatus.ACTIVE || !lastCheckin
  138. ? t('Waiting for first check-in')
  139. : monitorStatus === MonitorStatus.OK
  140. ? tct('Check-in [lastCheckin]', {lastCheckin})
  141. : monitorStatus === MonitorStatus.MISSED_CHECKIN
  142. ? tct('Missed [lastCheckin]', {lastCheckin})
  143. : monitorStatus === MonitorStatus.ERROR
  144. ? tct('Failed [lastCheckin]', {lastCheckin})
  145. : monitorStatus === MonitorStatus.TIMEOUT
  146. ? t('Timed out')
  147. : null}
  148. </TextOverflow>
  149. </MonitorColumn>
  150. <MonitorColumn>{scheduleAsText(monitor.config)}</MonitorColumn>
  151. <MonitorColumn>
  152. {monitorEnv?.nextCheckIn &&
  153. monitorEnv.status !== MonitorStatus.DISABLED &&
  154. monitorEnv.status !== MonitorStatus.ACTIVE ? (
  155. <TimeSince unitStyle="regular" date={monitorEnv.nextCheckIn} />
  156. ) : (
  157. '\u2014'
  158. )}
  159. </MonitorColumn>
  160. <MonitorColumn>
  161. <IdBadge
  162. project={monitor.project}
  163. avatarSize={18}
  164. avatarProps={{hasTooltip: true, tooltip: monitor.project.slug}}
  165. />
  166. </MonitorColumn>
  167. <MonitorColumn>{monitorEnv?.name ?? '\u2014'}</MonitorColumn>
  168. <ActionsColumn>
  169. <DropdownMenu
  170. items={actions}
  171. position="bottom-end"
  172. triggerProps={{
  173. 'aria-label': t('Actions'),
  174. size: 'xs',
  175. icon: <IconEllipsis size="xs" />,
  176. showChevron: false,
  177. }}
  178. />
  179. </ActionsColumn>
  180. </Fragment>
  181. );
  182. }
  183. export {MonitorRow};
  184. const MonitorName = styled('div')`
  185. display: flex;
  186. align-items: center;
  187. gap: ${space(2)};
  188. font-size: ${p => p.theme.fontSizeLarge};
  189. `;
  190. const NameAndSlug = styled('div')`
  191. display: flex;
  192. flex-direction: column;
  193. gap: ${space(0.25)};
  194. `;
  195. const MonitorSlug = styled('div')`
  196. font-size: ${p => p.theme.fontSizeSmall};
  197. color: ${p => p.theme.subText};
  198. `;
  199. const MonitorColumn = styled('div')`
  200. display: flex;
  201. align-items: center;
  202. `;
  203. const ActionsColumn = styled('div')`
  204. display: flex;
  205. align-items: center;
  206. justify-content: center;
  207. `;
  208. const AdditionalEnvironmentWarning = styled('div')`
  209. margin: ${space(1)} 0;
  210. `;