projectLatestAlerts.tsx 6.9 KB

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