timelineTableRow.tsx 4.6 KB

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