projectLatestAlerts.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import styled from '@emotion/styled';
  2. import type {Location} from 'history';
  3. import pick from 'lodash/pick';
  4. import AlertBadge from 'sentry/components/alertBadge';
  5. import {SectionHeading} from 'sentry/components/charts/styles';
  6. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  7. import Link from 'sentry/components/links/link';
  8. import LoadingError from 'sentry/components/loadingError';
  9. import Placeholder from 'sentry/components/placeholder';
  10. import TimeSince from 'sentry/components/timeSince';
  11. import {URL_PARAM} from 'sentry/constants/pageFilters';
  12. import {IconCheckmark, IconExclamation, IconFire, IconOpen} from 'sentry/icons';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import type {Organization} from 'sentry/types';
  16. import {useApiQuery} from 'sentry/utils/queryClient';
  17. import type {Incident} from 'sentry/views/alerts/types';
  18. import {IncidentStatus} from 'sentry/views/alerts/types';
  19. import MissingAlertsButtons from './missingFeatureButtons/missingAlertsButtons';
  20. import {SectionHeadingLink, SectionHeadingWrapper, SidebarSection} from './styles';
  21. const PLACEHOLDER_AND_EMPTY_HEIGHT = '172px';
  22. interface AlertRowProps {
  23. alert: Incident;
  24. orgSlug: string;
  25. }
  26. function AlertRow({alert, orgSlug}: AlertRowProps) {
  27. const {status, identifier, title, dateClosed, dateStarted} = alert;
  28. const isResolved = status === IncidentStatus.CLOSED;
  29. const isWarning = status === IncidentStatus.WARNING;
  30. const Icon = isResolved ? IconCheckmark : isWarning ? IconExclamation : IconFire;
  31. const statusProps = {isResolved, isWarning};
  32. return (
  33. <AlertRowLink
  34. aria-label={title}
  35. to={`/organizations/${orgSlug}/alerts/${identifier}/`}
  36. >
  37. <AlertBadgeWrapper {...statusProps} icon={<Icon />}>
  38. <AlertBadge status={status} />
  39. </AlertBadgeWrapper>
  40. <AlertDetails>
  41. <AlertTitle>{title}</AlertTitle>
  42. <AlertDate {...statusProps}>
  43. {isResolved ? t('Resolved') : t('Triggered')}{' '}
  44. {isResolved ? (
  45. dateClosed ? (
  46. <TimeSince date={dateClosed} />
  47. ) : null
  48. ) : (
  49. <TimeSince
  50. date={dateStarted}
  51. tooltipUnderlineColor={getStatusColor(statusProps)}
  52. />
  53. )}
  54. </AlertDate>
  55. </AlertDetails>
  56. </AlertRowLink>
  57. );
  58. }
  59. interface ProjectLatestAlertsProps {
  60. isProjectStabilized: boolean;
  61. location: Location;
  62. organization: Organization;
  63. projectSlug: string;
  64. }
  65. function ProjectLatestAlerts({
  66. location,
  67. organization,
  68. isProjectStabilized,
  69. projectSlug,
  70. }: ProjectLatestAlertsProps) {
  71. const query = {
  72. ...pick(location.query, Object.values(URL_PARAM)),
  73. per_page: 3,
  74. };
  75. const {
  76. data: unresolvedAlerts = [],
  77. isLoading: unresolvedAlertsIsLoading,
  78. isError: unresolvedAlertsIsError,
  79. } = useApiQuery<Incident[]>(
  80. [
  81. `/organizations/${organization.slug}/incidents/`,
  82. {query: {...query, status: 'open'}},
  83. ],
  84. {staleTime: 0, enabled: isProjectStabilized}
  85. );
  86. const {
  87. data: resolvedAlerts = [],
  88. isLoading: resolvedAlertsIsLoading,
  89. isError: resolvedAlertsIsError,
  90. } = useApiQuery<Incident[]>(
  91. [
  92. `/organizations/${organization.slug}/incidents/`,
  93. {query: {...query, status: 'closed'}},
  94. ],
  95. {staleTime: 0, enabled: isProjectStabilized}
  96. );
  97. const alertsUnresolvedAndResolved = [...unresolvedAlerts, ...resolvedAlerts];
  98. const shouldLoadAlertRules =
  99. alertsUnresolvedAndResolved.length === 0 &&
  100. !unresolvedAlertsIsLoading &&
  101. !resolvedAlertsIsLoading;
  102. // This is only used to determine if we should show the "Create Alert" button
  103. const {data: alertRules = [], isLoading: alertRulesLoading} = useApiQuery<any[]>(
  104. [
  105. `/organizations/${organization.slug}/alert-rules/`,
  106. {
  107. query: {
  108. ...pick(location.query, Object.values(URL_PARAM)),
  109. // Sort by name
  110. asc: 1,
  111. per_page: 1,
  112. },
  113. },
  114. ],
  115. {
  116. staleTime: 0,
  117. enabled: shouldLoadAlertRules,
  118. }
  119. );
  120. function renderAlertRules() {
  121. if (unresolvedAlertsIsError || resolvedAlertsIsError) {
  122. return <LoadingError message={t('Unable to load latest alerts')} />;
  123. }
  124. const isLoading = unresolvedAlertsIsLoading || resolvedAlertsIsLoading;
  125. if (isLoading || (shouldLoadAlertRules && alertRulesLoading)) {
  126. return <Placeholder height={PLACEHOLDER_AND_EMPTY_HEIGHT} />;
  127. }
  128. const hasAlertRule = alertsUnresolvedAndResolved.length > 0 || alertRules?.length > 0;
  129. if (!hasAlertRule) {
  130. return (
  131. <MissingAlertsButtons organization={organization} projectSlug={projectSlug} />
  132. );
  133. }
  134. if (alertsUnresolvedAndResolved.length === 0) {
  135. return (
  136. <StyledEmptyStateWarning small>{t('No alerts found')}</StyledEmptyStateWarning>
  137. );
  138. }
  139. return alertsUnresolvedAndResolved
  140. .slice(0, 3)
  141. .map(alert => (
  142. <AlertRow key={alert.id} alert={alert} orgSlug={organization.slug} />
  143. ));
  144. }
  145. return (
  146. <SidebarSection>
  147. <SectionHeadingWrapper>
  148. <SectionHeading>{t('Latest Alerts')}</SectionHeading>
  149. {/* as this is a link to latest alerts, we want to only preserve project and environment */}
  150. <SectionHeadingLink
  151. to={{
  152. pathname: `/organizations/${organization.slug}/alerts/`,
  153. query: {
  154. statsPeriod: undefined,
  155. start: undefined,
  156. end: undefined,
  157. utc: undefined,
  158. },
  159. }}
  160. >
  161. <IconOpen aria-label={t('Metric Alert History')} />
  162. </SectionHeadingLink>
  163. </SectionHeadingWrapper>
  164. <div>{renderAlertRules()}</div>
  165. </SidebarSection>
  166. );
  167. }
  168. const AlertRowLink = styled(Link)`
  169. display: flex;
  170. align-items: center;
  171. height: 40px;
  172. margin-bottom: ${space(3)};
  173. margin-left: ${space(0.5)};
  174. &,
  175. &:hover,
  176. &:focus {
  177. color: inherit;
  178. }
  179. &:first-child {
  180. margin-top: ${space(1)};
  181. }
  182. `;
  183. type StatusColorProps = {
  184. isResolved: boolean;
  185. isWarning: boolean;
  186. };
  187. const getStatusColor = ({isResolved, isWarning}: StatusColorProps) =>
  188. isResolved ? 'successText' : isWarning ? 'warningText' : 'errorText';
  189. const AlertBadgeWrapper = styled('div')<{icon: React.ReactNode} & StatusColorProps>`
  190. display: flex;
  191. align-items: center;
  192. justify-content: center;
  193. flex-shrink: 0;
  194. /* icon warning needs to be treated differently to look visually centered */
  195. line-height: ${p => (p.icon === IconExclamation ? undefined : 1)};
  196. `;
  197. const AlertDetails = styled('div')`
  198. font-size: ${p => p.theme.fontSizeMedium};
  199. margin-left: ${space(1.5)};
  200. ${p => p.theme.overflowEllipsis}
  201. line-height: 1.35;
  202. `;
  203. const AlertTitle = styled('div')`
  204. font-weight: 400;
  205. overflow: hidden;
  206. text-overflow: ellipsis;
  207. `;
  208. const AlertDate = styled('span')<StatusColorProps>`
  209. color: ${p => p.theme[getStatusColor(p)]};
  210. `;
  211. const StyledEmptyStateWarning = styled(EmptyStateWarning)`
  212. height: ${PLACEHOLDER_AND_EMPTY_HEIGHT};
  213. justify-content: center;
  214. `;
  215. export default ProjectLatestAlerts;