row.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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. to: normalizeUrl(`/organizations/${organization.slug}/crons/${monitor.slug}/edit/`),
  92. },
  93. {
  94. key: 'delete',
  95. label: t('Delete'),
  96. priority: 'danger',
  97. onAction: () => {
  98. openConfirmModal({
  99. onConfirm: async () => {
  100. await deleteMonitor(api, organization.slug, monitor.slug);
  101. onDelete();
  102. },
  103. header: t('Delete Monitor?'),
  104. message: deletionModalMessage,
  105. confirmText: t('Delete Monitor'),
  106. priority: 'danger',
  107. });
  108. },
  109. },
  110. ];
  111. const monitorDetailUrl = `/organizations/${organization.slug}/crons/${monitor.slug}/${
  112. monitorEnv ? `?environment=${monitorEnv.name}` : ''
  113. }`;
  114. // TODO(davidenwang): Change accordingly when we have ObjectStatus on monitor
  115. const monitorStatus = monitorEnv ? monitorEnv.status : monitor.status;
  116. return (
  117. <Fragment>
  118. <MonitorName>
  119. <MonitorBadge status={monitorEnv?.status ?? monitor.status} />
  120. <NameAndSlug>
  121. <Link to={monitorDetailUrl}>{monitor.name}</Link>
  122. <MonitorSlug>{monitor.slug}</MonitorSlug>
  123. </NameAndSlug>
  124. </MonitorName>
  125. <MonitorColumn>
  126. <TextOverflow>
  127. {monitorStatus === MonitorStatus.DISABLED
  128. ? t('Paused')
  129. : monitorStatus === MonitorStatus.ACTIVE || !lastCheckin
  130. ? t('Waiting for first check-in')
  131. : monitorStatus === MonitorStatus.OK
  132. ? tct('Check-in [lastCheckin]', {lastCheckin})
  133. : monitorStatus === MonitorStatus.MISSED_CHECKIN
  134. ? tct('Missed [lastCheckin]', {lastCheckin})
  135. : monitorStatus === MonitorStatus.ERROR
  136. ? tct('Failed [lastCheckin]', {lastCheckin})
  137. : null}
  138. </TextOverflow>
  139. </MonitorColumn>
  140. <MonitorColumn>{scheduleAsText(monitor.config)}</MonitorColumn>
  141. <MonitorColumn>
  142. {monitorEnv?.nextCheckIn &&
  143. monitorEnv.status !== MonitorStatus.DISABLED &&
  144. monitorEnv.status !== MonitorStatus.ACTIVE ? (
  145. <TimeSince unitStyle="regular" date={monitorEnv.nextCheckIn} />
  146. ) : (
  147. '\u2014'
  148. )}
  149. </MonitorColumn>
  150. <MonitorColumn>
  151. <IdBadge
  152. project={monitor.project}
  153. avatarSize={18}
  154. avatarProps={{hasTooltip: true, tooltip: monitor.project.slug}}
  155. />
  156. </MonitorColumn>
  157. <MonitorColumn>{monitorEnv?.name ?? '\u2014'}</MonitorColumn>
  158. <ActionsColumn>
  159. <DropdownMenu
  160. items={actions}
  161. position="bottom-end"
  162. triggerProps={{
  163. 'aria-label': t('Actions'),
  164. size: 'xs',
  165. icon: <IconEllipsis size="xs" />,
  166. showChevron: false,
  167. }}
  168. />
  169. </ActionsColumn>
  170. </Fragment>
  171. );
  172. }
  173. export {MonitorRow};
  174. const MonitorName = styled('div')`
  175. display: flex;
  176. align-items: center;
  177. gap: ${space(2)};
  178. font-size: ${p => p.theme.fontSizeLarge};
  179. `;
  180. const NameAndSlug = styled('div')`
  181. display: flex;
  182. flex-direction: column;
  183. gap: ${space(0.25)};
  184. `;
  185. const MonitorSlug = styled('div')`
  186. font-size: ${p => p.theme.fontSizeSmall};
  187. color: ${p => p.theme.subText};
  188. `;
  189. const MonitorColumn = styled('div')`
  190. display: flex;
  191. align-items: center;
  192. `;
  193. const ActionsColumn = styled('div')`
  194. display: flex;
  195. align-items: center;
  196. justify-content: center;
  197. `;
  198. const AdditionalEnvironmentWarning = styled('div')`
  199. margin: ${space(1)} 0;
  200. `;