overviewRow.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import {css} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import pick from 'lodash/pick';
  4. import Tag from 'sentry/components/badge/tag';
  5. import {CheckInPlaceholder} from 'sentry/components/checkInTimeline/checkInPlaceholder';
  6. import {CheckInTimeline} from 'sentry/components/checkInTimeline/checkInTimeline';
  7. import type {TimeWindowConfig} from 'sentry/components/checkInTimeline/types';
  8. import ActorBadge from 'sentry/components/idBadge/actorBadge';
  9. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  10. import Link from 'sentry/components/links/link';
  11. import {IconTimer, IconUser} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import getDuration from 'sentry/utils/duration/getDuration';
  15. import {useLocation} from 'sentry/utils/useLocation';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import useProjectFromSlug from 'sentry/utils/useProjectFromSlug';
  18. import {makeAlertsPathname} from 'sentry/views/alerts/pathnames';
  19. import type {UptimeRule} from 'sentry/views/alerts/rules/uptime/types';
  20. import {checkStatusPrecedent, statusToText, tickStyle} from '../../timelineConfig';
  21. import {useUptimeMonitorStats} from '../../utils/useUptimeMonitorStats';
  22. interface Props {
  23. timeWindowConfig: TimeWindowConfig;
  24. uptimeRule: UptimeRule;
  25. /**
  26. * Whether only one uptime rule is being rendered in a larger view with this
  27. * component. turns off things like zebra striping, hover effect, and showing
  28. * rule name.
  29. */
  30. singleRuleView?: boolean;
  31. }
  32. export function OverviewRow({uptimeRule, timeWindowConfig, singleRuleView}: Props) {
  33. const organization = useOrganization();
  34. const project = useProjectFromSlug({
  35. organization,
  36. projectSlug: uptimeRule.projectSlug,
  37. });
  38. const location = useLocation();
  39. const query = pick(location.query, ['start', 'end', 'statsPeriod', 'environment']);
  40. const {data: uptimeStats, isPending} = useUptimeMonitorStats({
  41. ruleIds: [uptimeRule.id],
  42. timeWindowConfig,
  43. });
  44. const ruleDetails = singleRuleView ? null : (
  45. <DetailsArea>
  46. <DetailsLink
  47. to={{
  48. pathname: makeAlertsPathname({
  49. path: `/rules/uptime/${uptimeRule.projectSlug}/${uptimeRule.id}/details/`,
  50. organization,
  51. }),
  52. query,
  53. }}
  54. >
  55. <DetailsHeadline>
  56. <Name>{uptimeRule.name}</Name>
  57. </DetailsHeadline>
  58. <DetailsContainer>
  59. <OwnershipDetails>
  60. {project && <ProjectBadge project={project} avatarSize={12} disableLink />}
  61. {uptimeRule.owner ? (
  62. <ActorBadge actor={uptimeRule.owner} avatarSize={12} />
  63. ) : (
  64. <UnassignedLabel>
  65. <IconUser size="xs" />
  66. {t('Unassigned')}
  67. </UnassignedLabel>
  68. )}
  69. </OwnershipDetails>
  70. <ScheduleDetails>
  71. <IconTimer size="xs" />
  72. {t('Checked every %s', getDuration(uptimeRule.intervalSeconds))}
  73. </ScheduleDetails>
  74. <MonitorStatuses>
  75. {uptimeRule.status === 'disabled' && <Tag>{t('Disabled')}</Tag>}
  76. </MonitorStatuses>
  77. </DetailsContainer>
  78. </DetailsLink>
  79. </DetailsArea>
  80. );
  81. return (
  82. <TimelineRow
  83. key={uptimeRule.id}
  84. singleRuleView={singleRuleView}
  85. as={singleRuleView ? 'div' : 'li'}
  86. >
  87. {ruleDetails}
  88. <TimelineContainer>
  89. {isPending ? (
  90. <CheckInPlaceholder />
  91. ) : (
  92. <CheckInTimeline
  93. bucketedData={uptimeStats?.[uptimeRule.id] ?? []}
  94. statusLabel={statusToText}
  95. statusStyle={tickStyle}
  96. statusPrecedent={checkStatusPrecedent}
  97. timeWindowConfig={timeWindowConfig}
  98. />
  99. )}
  100. </TimelineContainer>
  101. </TimelineRow>
  102. );
  103. }
  104. const DetailsLink = styled(Link)`
  105. display: block;
  106. padding: ${space(3)};
  107. color: ${p => p.theme.textColor};
  108. &:focus-visible {
  109. outline: none;
  110. }
  111. `;
  112. const DetailsArea = styled('div')`
  113. border-right: 1px solid ${p => p.theme.border};
  114. border-radius: 0;
  115. position: relative;
  116. `;
  117. const DetailsHeadline = styled('div')`
  118. display: grid;
  119. gap: ${space(1)};
  120. grid-template-columns: 1fr minmax(30px, max-content);
  121. `;
  122. const DetailsContainer = styled('div')`
  123. display: flex;
  124. flex-direction: column;
  125. gap: ${space(0.5)};
  126. `;
  127. const OwnershipDetails = styled('div')`
  128. display: flex;
  129. gap: ${space(0.75)};
  130. align-items: center;
  131. color: ${p => p.theme.subText};
  132. font-size: ${p => p.theme.fontSizeSmall};
  133. `;
  134. const UnassignedLabel = styled('div')`
  135. display: flex;
  136. gap: ${space(0.5)};
  137. align-items: center;
  138. `;
  139. const Name = styled('h3')`
  140. font-size: ${p => p.theme.fontSizeLarge};
  141. word-break: break-word;
  142. margin-bottom: ${space(0.5)};
  143. `;
  144. const ScheduleDetails = styled('small')`
  145. display: flex;
  146. gap: ${space(0.5)};
  147. align-items: center;
  148. color: ${p => p.theme.subText};
  149. font-size: ${p => p.theme.fontSizeSmall};
  150. `;
  151. const MonitorStatuses = styled('div')`
  152. display: flex;
  153. gap: ${space(0.5)};
  154. `;
  155. interface TimelineRowProps {
  156. isDisabled?: boolean;
  157. singleRuleView?: boolean;
  158. }
  159. const TimelineRow = styled('li')<TimelineRowProps>`
  160. grid-column: 1/-1;
  161. display: grid;
  162. grid-template-columns: subgrid;
  163. ${p =>
  164. !p.singleRuleView &&
  165. css`
  166. transition: background 50ms ease-in-out;
  167. &:nth-child(odd) {
  168. background: ${p.theme.backgroundSecondary};
  169. }
  170. &:hover {
  171. background: ${p.theme.backgroundTertiary};
  172. }
  173. &:has(*:focus-visible) {
  174. background: ${p.theme.backgroundTertiary};
  175. }
  176. `}
  177. /* Disabled monitors become more opaque */
  178. --disabled-opacity: ${p => (p.isDisabled ? '0.6' : 'unset')};
  179. &:last-child {
  180. border-bottom-left-radius: ${p => p.theme.borderRadius};
  181. border-bottom-right-radius: ${p => p.theme.borderRadius};
  182. }
  183. `;
  184. const TimelineContainer = styled('div')`
  185. display: flex;
  186. align-items: center;
  187. padding: ${space(3)} 0;
  188. grid-column: 2/-1;
  189. opacity: var(--disabled-opacity);
  190. `;