import styled from '@emotion/styled'; import {Location} from 'history'; import pick from 'lodash/pick'; import AlertBadge from 'sentry/components/alertBadge'; import AsyncComponent from 'sentry/components/asyncComponent'; import {SectionHeading} from 'sentry/components/charts/styles'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; import Link from 'sentry/components/links/link'; import Placeholder from 'sentry/components/placeholder'; import TimeSince from 'sentry/components/timeSince'; import {URL_PARAM} from 'sentry/constants/pageFilters'; import {IconCheckmark, IconExclamation, IconFire, IconOpen} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import space from 'sentry/styles/space'; import {Organization} from 'sentry/types'; import {Incident, IncidentStatus} from '../alerts/types'; import MissingAlertsButtons from './missingFeatureButtons/missingAlertsButtons'; import {SectionHeadingLink, SectionHeadingWrapper, SidebarSection} from './styles'; import {didProjectOrEnvironmentChange} from './utils'; const PLACEHOLDER_AND_EMPTY_HEIGHT = '172px'; type Props = AsyncComponent['props'] & { isProjectStabilized: boolean; location: Location; organization: Organization; projectSlug: string; }; type State = { resolvedAlerts: Incident[] | null; unresolvedAlerts: Incident[] | null; hasAlertRule?: boolean; } & AsyncComponent['state']; class ProjectLatestAlerts extends AsyncComponent { shouldComponentUpdate(nextProps: Props, nextState: State) { const {location, isProjectStabilized} = this.props; // TODO(project-detail): we temporarily removed refetching based on timeselector if ( this.state !== nextState || didProjectOrEnvironmentChange(location, nextProps.location) || isProjectStabilized !== nextProps.isProjectStabilized ) { return true; } return false; } componentDidUpdate(prevProps: Props) { const {location, isProjectStabilized} = this.props; if ( didProjectOrEnvironmentChange(prevProps.location, location) || prevProps.isProjectStabilized !== isProjectStabilized ) { this.remountComponent(); } } getEndpoints(): ReturnType { const {location, organization, isProjectStabilized} = this.props; if (!isProjectStabilized) { return []; } const query = { ...pick(location.query, Object.values(URL_PARAM)), per_page: 3, }; // we are listing 3 alerts total, first unresolved and then we fill with resolved return [ [ 'unresolvedAlerts', `/organizations/${organization.slug}/incidents/`, {query: {...query, status: 'open'}}, ], [ 'resolvedAlerts', `/organizations/${organization.slug}/incidents/`, {query: {...query, status: 'closed'}}, ], ]; } /** * If our alerts are empty, determine if we've configured alert rules (empty message differs then) */ async onLoadAllEndpointsSuccess() { const {unresolvedAlerts, resolvedAlerts} = this.state; const {location, organization, isProjectStabilized} = this.props; if (!isProjectStabilized) { return; } if ([...(unresolvedAlerts ?? []), ...(resolvedAlerts ?? [])].length !== 0) { this.setState({hasAlertRule: true}); return; } this.setState({loading: true}); const alertRules = await this.api.requestPromise( `/organizations/${organization.slug}/alert-rules/`, { method: 'GET', query: { ...pick(location.query, [...Object.values(URL_PARAM)]), per_page: 1, }, } ); this.setState({hasAlertRule: alertRules.length > 0, loading: false}); } get alertsLink() { const {organization} = this.props; // as this is a link to latest alerts, we want to only preserve project and environment return { pathname: `/organizations/${organization.slug}/alerts/`, query: { statsPeriod: undefined, start: undefined, end: undefined, utc: undefined, }, }; } renderAlertRow = (alert: Incident) => { const {organization} = this.props; const {status, id, identifier, title, dateClosed, dateStarted} = alert; const isResolved = status === IncidentStatus.CLOSED; const isWarning = status === IncidentStatus.WARNING; const Icon = isResolved ? IconCheckmark : isWarning ? IconExclamation : IconFire; const statusProps = {isResolved, isWarning}; return ( {title} {isResolved ? tct('Resolved [date]', { date: dateClosed ? : null, }) : tct('Triggered [date]', { date: ( ), })} ); }; renderInnerBody() { const {organization, projectSlug, isProjectStabilized} = this.props; const {loading, unresolvedAlerts, resolvedAlerts, hasAlertRule} = this.state; const alertsUnresolvedAndResolved = [ ...(unresolvedAlerts ?? []), ...(resolvedAlerts ?? []), ]; const checkingForAlertRules = alertsUnresolvedAndResolved.length === 0 && hasAlertRule === undefined; const showLoadingIndicator = loading || checkingForAlertRules || !isProjectStabilized; if (showLoadingIndicator) { return ; } if (!hasAlertRule) { return ( ); } if (alertsUnresolvedAndResolved.length === 0) { return ( {t('No alerts found')} ); } return alertsUnresolvedAndResolved.slice(0, 3).map(this.renderAlertRow); } renderLoading() { return this.renderBody(); } renderBody() { return ( {t('Latest Alerts')}
{this.renderInnerBody()}
); } } const AlertRowLink = styled(Link)` display: flex; align-items: center; height: 40px; margin-bottom: ${space(3)}; margin-left: ${space(0.5)}; &, &:hover, &:focus { color: inherit; } &:first-child { margin-top: ${space(1)}; } `; type StatusColorProps = { isResolved: boolean; isWarning: boolean; }; const getStatusColor = ({isResolved, isWarning}: StatusColorProps) => isResolved ? 'green300' : isWarning ? 'yellow300' : 'red300'; const AlertBadgeWrapper = styled('div')<{icon: React.ReactNode} & StatusColorProps>` display: flex; align-items: center; justify-content: center; flex-shrink: 0; /* icon warning needs to be treated differently to look visually centered */ line-height: ${p => (p.icon === IconExclamation ? undefined : 1)}; `; const AlertDetails = styled('div')` font-size: ${p => p.theme.fontSizeMedium}; margin-left: ${space(1.5)}; ${p => p.theme.overflowEllipsis} line-height: 1.35; `; const AlertTitle = styled('div')` font-weight: 400; overflow: hidden; text-overflow: ellipsis; `; const AlertDate = styled('span')` color: ${p => p.theme[getStatusColor(p)]}; `; const StyledEmptyStateWarning = styled(EmptyStateWarning)` height: ${PLACEHOLDER_AND_EMPTY_HEIGHT}; justify-content: center; `; export default ProjectLatestAlerts;