timelineTableRow.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import {useState} from 'react';
  2. import {Link} from 'react-router';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {Button} from 'sentry/components/button';
  6. import {tct} from 'sentry/locale';
  7. import {fadeIn} from 'sentry/styles/animations';
  8. import {space} from 'sentry/styles/space';
  9. import useOrganization from 'sentry/utils/useOrganization';
  10. import {Monitor, MonitorStatus} from 'sentry/views/monitors/types';
  11. import {scheduleAsText} from 'sentry/views/monitors/utils';
  12. import {statusIconColorMap} from 'sentry/views/monitors/utils/constants';
  13. import {CheckInTimeline, CheckInTimelineProps} from './checkInTimeline';
  14. import {TimelinePlaceholder} from './timelinePlaceholder';
  15. import {MonitorBucket} from './types';
  16. interface Props extends Omit<CheckInTimelineProps, 'bucketedData' | 'environment'> {
  17. monitor: Monitor;
  18. bucketedData?: MonitorBucket[];
  19. /**
  20. * Whether only one monitor is being rendered in a larger view with this component
  21. * turns off things like zebra striping, hover effect, and showing monitor name
  22. */
  23. singleMonitorView?: boolean;
  24. }
  25. const MAX_SHOWN_ENVIRONMENTS = 4;
  26. export function TimelineTableRow({
  27. monitor,
  28. bucketedData,
  29. singleMonitorView,
  30. ...timelineProps
  31. }: Props) {
  32. const [isExpanded, setExpanded] = useState(
  33. monitor.environments.length <= MAX_SHOWN_ENVIRONMENTS
  34. );
  35. const environments = isExpanded
  36. ? monitor.environments
  37. : monitor.environments.slice(0, MAX_SHOWN_ENVIRONMENTS);
  38. return (
  39. <TimelineRow key={monitor.id} singleMonitorView={singleMonitorView}>
  40. {!singleMonitorView && <MonitorDetails monitor={monitor} />}
  41. <MonitorEnvContainer>
  42. {environments.map(({name, status}) => {
  43. const envStatus =
  44. monitor.status === MonitorStatus.DISABLED ? MonitorStatus.DISABLED : status;
  45. return (
  46. <EnvWithStatus key={name}>
  47. <MonitorEnvLabel status={envStatus}>{name}</MonitorEnvLabel>
  48. {statusIconColorMap[envStatus].icon}
  49. </EnvWithStatus>
  50. );
  51. })}
  52. {!isExpanded && (
  53. <Button size="xs" onClick={() => setExpanded(true)}>
  54. {tct('Show [num] More', {
  55. num: monitor.environments.length - MAX_SHOWN_ENVIRONMENTS,
  56. })}
  57. </Button>
  58. )}
  59. </MonitorEnvContainer>
  60. <TimelineContainer>
  61. {environments.map(({name}) => {
  62. return (
  63. <TimelineEnvOuterContainer key={name}>
  64. {!bucketedData ? (
  65. <TimelinePlaceholder />
  66. ) : (
  67. <TimelineEnvContainer>
  68. <CheckInTimeline
  69. {...timelineProps}
  70. bucketedData={bucketedData}
  71. environment={name}
  72. />
  73. </TimelineEnvContainer>
  74. )}
  75. </TimelineEnvOuterContainer>
  76. );
  77. })}
  78. </TimelineContainer>
  79. </TimelineRow>
  80. );
  81. }
  82. function MonitorDetails({monitor}: {monitor: Monitor}) {
  83. const organization = useOrganization();
  84. const schedule = scheduleAsText(monitor.config);
  85. const monitorDetailUrl = `/organizations/${organization.slug}/crons/${monitor.slug}/`;
  86. return (
  87. <DetailsContainer to={monitorDetailUrl}>
  88. <Name>{monitor.name}</Name>
  89. <Schedule>{schedule}</Schedule>
  90. </DetailsContainer>
  91. );
  92. }
  93. const TimelineRow = styled('div')<{singleMonitorView?: boolean}>`
  94. display: contents;
  95. ${p =>
  96. !p.singleMonitorView &&
  97. css`
  98. &:nth-child(odd) > * {
  99. background: ${p.theme.backgroundSecondary};
  100. }
  101. &:hover > * {
  102. background: ${p.theme.backgroundTertiary};
  103. }
  104. `}
  105. > * {
  106. transition: background 50ms ease-in-out;
  107. }
  108. `;
  109. const DetailsContainer = styled(Link)`
  110. color: ${p => p.theme.textColor};
  111. padding: ${space(3)};
  112. border-right: 1px solid ${p => p.theme.border};
  113. border-radius: 0;
  114. `;
  115. const Name = styled('h3')`
  116. font-size: ${p => p.theme.fontSizeLarge};
  117. margin-bottom: ${space(0.25)};
  118. `;
  119. const Schedule = styled('small')`
  120. color: ${p => p.theme.subText};
  121. font-size: ${p => p.theme.fontSizeSmall};
  122. `;
  123. const MonitorEnvContainer = styled('div')`
  124. display: flex;
  125. padding: ${space(3)} ${space(2)};
  126. flex-direction: column;
  127. gap: ${space(4)};
  128. border-right: 1px solid ${p => p.theme.innerBorder};
  129. text-align: right;
  130. `;
  131. const EnvWithStatus = styled('div')`
  132. display: flex;
  133. gap: ${space(1)};
  134. align-items: center;
  135. `;
  136. const MonitorEnvLabel = styled('div')<{status: MonitorStatus}>`
  137. text-overflow: ellipsis;
  138. overflow: hidden;
  139. white-space: nowrap;
  140. flex: 1;
  141. color: ${p => p.theme[statusIconColorMap[p.status].color]};
  142. `;
  143. const TimelineContainer = styled('div')`
  144. display: flex;
  145. padding: ${space(3)} 0;
  146. flex-direction: column;
  147. gap: ${space(4)};
  148. contain: content;
  149. `;
  150. const TimelineEnvOuterContainer = styled('div')`
  151. position: relative;
  152. height: calc(${p => p.theme.fontSizeLarge} * ${p => p.theme.text.lineHeightHeading});
  153. `;
  154. const TimelineEnvContainer = styled('div')`
  155. position: absolute;
  156. inset: 0;
  157. opacity: 0;
  158. animation: ${fadeIn} 1.5s ease-out forwards;
  159. contain: content;
  160. `;