index.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import {useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import {
  4. deleteMonitorEnvironment,
  5. setEnvironmentIsMuted,
  6. updateMonitor,
  7. } from 'sentry/actionCreators/monitors';
  8. import {DateNavigator} from 'sentry/components/checkInTimeline/dateNavigator';
  9. import {
  10. GridLineLabels,
  11. GridLineOverlay,
  12. } from 'sentry/components/checkInTimeline/gridLines';
  13. import {useDateNavigation} from 'sentry/components/checkInTimeline/hooks/useDateNavigation';
  14. import {useTimeWindowConfig} from 'sentry/components/checkInTimeline/hooks/useTimeWindowConfig';
  15. import Panel from 'sentry/components/panels/panel';
  16. import {Sticky} from 'sentry/components/sticky';
  17. import {space} from 'sentry/styles/space';
  18. import {setApiQueryData, useQueryClient} from 'sentry/utils/queryClient';
  19. import useApi from 'sentry/utils/useApi';
  20. import {useDimensions} from 'sentry/utils/useDimensions';
  21. import {useLocation} from 'sentry/utils/useLocation';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. import type {Monitor} from 'sentry/views/monitors/types';
  24. import {makeMonitorListQueryKey} from 'sentry/views/monitors/utils';
  25. import {CronServiceIncidents} from '../serviceIncidents';
  26. import {OverviewRow} from './overviewRow';
  27. import {SortSelector} from './sortSelector';
  28. interface Props {
  29. monitorList: Monitor[];
  30. linkToAlerts?: boolean;
  31. }
  32. export function OverviewTimeline({monitorList, linkToAlerts}: Props) {
  33. const organization = useOrganization();
  34. const api = useApi();
  35. const queryClient = useQueryClient();
  36. const location = useLocation();
  37. const elementRef = useRef<HTMLDivElement>(null);
  38. const {width: timelineWidth} = useDimensions<HTMLDivElement>({elementRef});
  39. const timeWindowConfig = useTimeWindowConfig({timelineWidth});
  40. const dateNavigation = useDateNavigation();
  41. const handleDeleteEnvironment = async (monitor: Monitor, env: string) => {
  42. const success = await deleteMonitorEnvironment(api, organization.slug, monitor, env);
  43. if (!success) {
  44. return;
  45. }
  46. const queryKey = makeMonitorListQueryKey(organization, location.query);
  47. setApiQueryData(queryClient, queryKey, (oldMonitorList: Monitor[]) => {
  48. const oldMonitorIdx = oldMonitorList.findIndex(m => m.slug === monitor.slug);
  49. if (oldMonitorIdx < 0) {
  50. return oldMonitorList;
  51. }
  52. const oldMonitor = oldMonitorList[oldMonitorIdx]!;
  53. const newEnvList = oldMonitor.environments.filter(e => e.name !== env);
  54. const updatedMonitor = {
  55. ...oldMonitor,
  56. environments: newEnvList,
  57. };
  58. const left = oldMonitorList.slice(0, oldMonitorIdx);
  59. const right = oldMonitorList.slice(oldMonitorIdx + 1);
  60. if (newEnvList.length === 0) {
  61. return [...left, ...right];
  62. }
  63. return [...left, updatedMonitor, ...right];
  64. });
  65. };
  66. const handleToggleMuteEnvironment = async (
  67. monitor: Monitor,
  68. env: string,
  69. isMuted: boolean
  70. ) => {
  71. const resp = await setEnvironmentIsMuted(
  72. api,
  73. organization.slug,
  74. monitor,
  75. env,
  76. isMuted
  77. );
  78. if (resp === null) {
  79. return;
  80. }
  81. const queryKey = makeMonitorListQueryKey(organization, location.query);
  82. setApiQueryData(queryClient, queryKey, (oldMonitorList: Monitor[]) => {
  83. const monitorIdx = oldMonitorList.findIndex(m => m.slug === monitor.slug);
  84. // TODO(davidenwang): in future only change the specifically modified environment for optimistic updates
  85. oldMonitorList[monitorIdx] = resp;
  86. return oldMonitorList;
  87. });
  88. };
  89. const handleToggleStatus = async (monitor: Monitor) => {
  90. const status = monitor.status === 'active' ? 'disabled' : 'active';
  91. const resp = await updateMonitor(api, organization.slug, monitor, {status});
  92. if (resp === null) {
  93. return;
  94. }
  95. const queryKey = makeMonitorListQueryKey(organization, location.query);
  96. setApiQueryData(queryClient, queryKey, (oldMonitorList: Monitor[]) => {
  97. const monitorIdx = oldMonitorList.findIndex(m => m.slug === monitor.slug);
  98. oldMonitorList[monitorIdx] = {...oldMonitorList[monitorIdx]!, status: resp.status};
  99. return oldMonitorList;
  100. });
  101. };
  102. return (
  103. <MonitorListPanel role="region">
  104. <TimelineWidthTracker ref={elementRef} />
  105. <Header>
  106. <HeaderControlsLeft>
  107. <SortSelector size="xs" />
  108. <DateNavigator
  109. dateNavigation={dateNavigation}
  110. direction="back"
  111. size="xs"
  112. borderless
  113. />
  114. </HeaderControlsLeft>
  115. <AlignedGridLineLabels timeWindowConfig={timeWindowConfig} />
  116. <HeaderControlsRight>
  117. <DateNavigator
  118. dateNavigation={dateNavigation}
  119. direction="forward"
  120. size="xs"
  121. borderless
  122. />
  123. </HeaderControlsRight>
  124. </Header>
  125. <AlignedGridLineOverlay
  126. stickyCursor
  127. allowZoom
  128. showCursor
  129. additionalUi={<CronServiceIncidents timeWindowConfig={timeWindowConfig} />}
  130. timeWindowConfig={timeWindowConfig}
  131. />
  132. <MonitorRows>
  133. {monitorList.map(monitor => (
  134. <OverviewRow
  135. key={monitor.id}
  136. monitor={monitor}
  137. timeWindowConfig={timeWindowConfig}
  138. onDeleteEnvironment={env => handleDeleteEnvironment(monitor, env)}
  139. onToggleMuteEnvironment={(env, isMuted) =>
  140. handleToggleMuteEnvironment(monitor, env, isMuted)
  141. }
  142. onToggleStatus={handleToggleStatus}
  143. linkToAlerts={linkToAlerts}
  144. />
  145. ))}
  146. </MonitorRows>
  147. </MonitorListPanel>
  148. );
  149. }
  150. const Header = styled(Sticky)`
  151. display: grid;
  152. grid-column: 1/-1;
  153. grid-template-columns: subgrid;
  154. z-index: 1;
  155. background: ${p => p.theme.background};
  156. border-top-left-radius: ${p => p.theme.panelBorderRadius};
  157. border-top-right-radius: ${p => p.theme.panelBorderRadius};
  158. box-shadow: 0 1px ${p => p.theme.translucentBorder};
  159. &[data-stuck] {
  160. border-radius: 0;
  161. border-left: 1px solid ${p => p.theme.border};
  162. border-right: 1px solid ${p => p.theme.border};
  163. margin: 0 -1px;
  164. }
  165. `;
  166. const TimelineWidthTracker = styled('div')`
  167. position: absolute;
  168. width: 100%;
  169. grid-row: 1;
  170. grid-column: 3/-1;
  171. `;
  172. const AlignedGridLineOverlay = styled(GridLineOverlay)`
  173. grid-row: 1;
  174. grid-column: 3/-1;
  175. `;
  176. const AlignedGridLineLabels = styled(GridLineLabels)`
  177. grid-row: 1;
  178. grid-column: 3/-1;
  179. `;
  180. const MonitorListPanel = styled(Panel)`
  181. display: grid;
  182. grid-template-columns: 350px 135px 1fr max-content;
  183. `;
  184. const MonitorRows = styled('ul')`
  185. display: grid;
  186. grid-template-columns: subgrid;
  187. grid-column: 1 / -1;
  188. list-style: none;
  189. padding: 0;
  190. margin: 0;
  191. `;
  192. const HeaderControlsLeft = styled('div')`
  193. grid-column: 1/3;
  194. display: flex;
  195. justify-content: space-between;
  196. gap: ${space(0.5)};
  197. padding: ${space(1.5)} ${space(2)};
  198. `;
  199. const HeaderControlsRight = styled('div')`
  200. grid-row: 1;
  201. grid-column: -1;
  202. padding: ${space(1.5)} ${space(2)};
  203. `;