projectLatestAlerts.tsx 7.1 KB

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