row.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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} 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 {scheduleAsText} from 'sentry/views/monitors/utils';
  20. import {Monitor, MonitorEnvironment, MonitorStatus} from '../types';
  21. import {MonitorBadge} from './monitorBadge';
  22. interface MonitorRowProps {
  23. monitor: Monitor;
  24. onDelete: () => void;
  25. organization: Organization;
  26. monitorEnv?: MonitorEnvironment;
  27. }
  28. function MonitorRow({monitor, monitorEnv, organization, onDelete}: MonitorRowProps) {
  29. const api = useApi();
  30. const lastCheckin = monitorEnv?.lastCheckIn ? (
  31. <TimeSince unitStyle="regular" date={monitorEnv.lastCheckIn} />
  32. ) : null;
  33. const deletionModalMessage = (
  34. <Fragment>
  35. <Text>
  36. {tct('Are you sure you want to permanently delete "[name]"?', {
  37. name: monitor.name,
  38. })}
  39. </Text>
  40. {monitor.environments.length > 1 && (
  41. <AdditionalEnvironmentWarning>
  42. <Text>
  43. {t(
  44. `This will delete check-in data for this monitor associated with these environments:`
  45. )}
  46. </Text>
  47. <List symbol="bullet">
  48. {monitor.environments.map(environment => (
  49. <ListItem key={environment.name}>{environment.name}</ListItem>
  50. ))}
  51. </List>
  52. </AdditionalEnvironmentWarning>
  53. )}
  54. </Fragment>
  55. );
  56. const actions: MenuItemProps[] = [
  57. {
  58. key: 'edit',
  59. label: t('Edit'),
  60. // TODO(davidenwang): Right now we have to pass the environment
  61. // through the URL so that when we save the monitor and are
  62. // redirected back to the details page it queries the backend
  63. // for a monitor environment with check-in data
  64. to: normalizeUrl({
  65. pathname: `/organizations/${organization.slug}/crons/${monitor.slug}/edit/`,
  66. query: {environment: monitorEnv?.name},
  67. }),
  68. },
  69. {
  70. key: 'delete',
  71. label: t('Delete'),
  72. priority: 'danger',
  73. onAction: () => {
  74. openConfirmModal({
  75. onConfirm: async () => {
  76. await deleteMonitor(api, organization.slug, monitor.slug);
  77. onDelete();
  78. },
  79. header: t('Delete Monitor?'),
  80. message: deletionModalMessage,
  81. confirmText: t('Delete Monitor'),
  82. priority: 'danger',
  83. });
  84. },
  85. },
  86. ];
  87. const monitorDetailUrl = `/organizations/${organization.slug}/crons/${monitor.slug}/${
  88. monitorEnv ? `?environment=${monitorEnv.name}` : ''
  89. }`;
  90. // TODO(davidenwang): Change accordingly when we have ObjectStatus on monitor
  91. const monitorStatus =
  92. monitor.status !== 'disabled' && monitorEnv ? monitorEnv.status : monitor.status;
  93. return (
  94. <Fragment>
  95. <MonitorName>
  96. <MonitorBadge status={monitorStatus} />
  97. <NameAndSlug>
  98. <Link to={monitorDetailUrl}>{monitor.name}</Link>
  99. <MonitorSlug>{monitor.slug}</MonitorSlug>
  100. </NameAndSlug>
  101. </MonitorName>
  102. <MonitorColumn>
  103. <TextOverflow>
  104. {monitorStatus === MonitorStatus.DISABLED
  105. ? t('Paused')
  106. : monitorStatus === MonitorStatus.ACTIVE || !lastCheckin
  107. ? t('Waiting for first check-in')
  108. : monitorStatus === MonitorStatus.OK
  109. ? tct('Check-in [lastCheckin]', {lastCheckin})
  110. : monitorStatus === MonitorStatus.MISSED_CHECKIN
  111. ? tct('Missed [lastCheckin]', {lastCheckin})
  112. : monitorStatus === MonitorStatus.ERROR
  113. ? tct('Failed [lastCheckin]', {lastCheckin})
  114. : monitorStatus === MonitorStatus.TIMEOUT
  115. ? t('Timed out')
  116. : null}
  117. </TextOverflow>
  118. </MonitorColumn>
  119. <MonitorColumn>{scheduleAsText(monitor.config)}</MonitorColumn>
  120. <MonitorColumn>
  121. {monitorEnv?.nextCheckIn &&
  122. monitorEnv.status !== MonitorStatus.DISABLED &&
  123. monitorEnv.status !== MonitorStatus.ACTIVE ? (
  124. <TimeSince unitStyle="regular" date={monitorEnv.nextCheckIn} />
  125. ) : (
  126. '\u2014'
  127. )}
  128. </MonitorColumn>
  129. <MonitorColumn>
  130. <IdBadge
  131. project={monitor.project}
  132. avatarSize={18}
  133. avatarProps={{hasTooltip: true, tooltip: monitor.project.slug}}
  134. />
  135. </MonitorColumn>
  136. <MonitorColumn>{monitorEnv?.name ?? '\u2014'}</MonitorColumn>
  137. <ActionsColumn>
  138. <DropdownMenu
  139. items={actions}
  140. position="bottom-end"
  141. triggerProps={{
  142. 'aria-label': t('Actions'),
  143. size: 'xs',
  144. icon: <IconEllipsis size="xs" />,
  145. showChevron: false,
  146. }}
  147. />
  148. </ActionsColumn>
  149. </Fragment>
  150. );
  151. }
  152. export {MonitorRow};
  153. const MonitorName = styled('div')`
  154. display: flex;
  155. align-items: center;
  156. gap: ${space(2)};
  157. font-size: ${p => p.theme.fontSizeLarge};
  158. `;
  159. const NameAndSlug = styled('div')`
  160. display: flex;
  161. flex-direction: column;
  162. gap: ${space(0.25)};
  163. `;
  164. const MonitorSlug = styled('div')`
  165. font-size: ${p => p.theme.fontSizeSmall};
  166. color: ${p => p.theme.subText};
  167. `;
  168. const MonitorColumn = styled('div')`
  169. display: flex;
  170. align-items: center;
  171. `;
  172. const ActionsColumn = styled('div')`
  173. display: flex;
  174. align-items: center;
  175. justify-content: center;
  176. `;
  177. const AdditionalEnvironmentWarning = styled('div')`
  178. margin: ${space(1)} 0;
  179. `;