123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- import styled from '@emotion/styled';
- import type {Location} from 'history';
- import pick from 'lodash/pick';
- import AlertBadge from 'sentry/components/alertBadge';
- import {SectionHeading} from 'sentry/components/charts/styles';
- import EmptyStateWarning from 'sentry/components/emptyStateWarning';
- import Link from 'sentry/components/links/link';
- import LoadingError from 'sentry/components/loadingError';
- 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} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {Organization} from 'sentry/types';
- import {useApiQuery} from 'sentry/utils/queryClient';
- import {Incident, IncidentStatus} from 'sentry/views/alerts/types';
- import MissingAlertsButtons from './missingFeatureButtons/missingAlertsButtons';
- import {SectionHeadingLink, SectionHeadingWrapper, SidebarSection} from './styles';
- const PLACEHOLDER_AND_EMPTY_HEIGHT = '172px';
- interface AlertRowProps {
- alert: Incident;
- orgSlug: string;
- }
- function AlertRow({alert, orgSlug}: AlertRowProps) {
- const {status, 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 (
- <AlertRowLink
- aria-label={title}
- to={`/organizations/${orgSlug}/alerts/${identifier}/`}
- >
- <AlertBadgeWrapper {...statusProps} icon={Icon}>
- <AlertBadge status={status} />
- </AlertBadgeWrapper>
- <AlertDetails>
- <AlertTitle>{title}</AlertTitle>
- <AlertDate {...statusProps}>
- {isResolved ? t('Resolved') : t('Triggered')}{' '}
- {isResolved ? (
- dateClosed ? (
- <TimeSince date={dateClosed} />
- ) : null
- ) : (
- <TimeSince
- date={dateStarted}
- tooltipUnderlineColor={getStatusColor(statusProps)}
- />
- )}
- </AlertDate>
- </AlertDetails>
- </AlertRowLink>
- );
- }
- interface ProjectLatestAlertsProps {
- isProjectStabilized: boolean;
- location: Location;
- organization: Organization;
- projectSlug: string;
- }
- function ProjectLatestAlerts({
- location,
- organization,
- isProjectStabilized,
- projectSlug,
- }: ProjectLatestAlertsProps) {
- const query = {
- ...pick(location.query, Object.values(URL_PARAM)),
- per_page: 3,
- };
- const {
- data: unresolvedAlerts = [],
- isLoading: unresolvedAlertsIsLoading,
- isError: unresolvedAlertsIsError,
- } = useApiQuery<Incident[]>(
- [
- `/organizations/${organization.slug}/incidents/`,
- {query: {...query, status: 'open'}},
- ],
- {staleTime: 0, enabled: isProjectStabilized}
- );
- const {
- data: resolvedAlerts = [],
- isLoading: resolvedAlertsIsLoading,
- isError: resolvedAlertsIsError,
- } = useApiQuery<Incident[]>(
- [
- `/organizations/${organization.slug}/incidents/`,
- {query: {...query, status: 'closed'}},
- ],
- {staleTime: 0, enabled: isProjectStabilized}
- );
- const alertsUnresolvedAndResolved = [...unresolvedAlerts, ...resolvedAlerts];
- const shouldLoadAlertRules =
- alertsUnresolvedAndResolved.length === 0 &&
- !unresolvedAlertsIsLoading &&
- !resolvedAlertsIsLoading;
- // This is only used to determine if we should show the "Create Alert" button
- const {data: alertRules = [], isLoading: alertRulesLoading} = useApiQuery<any[]>(
- [
- `/organizations/${organization.slug}/alert-rules/`,
- {
- query: {
- ...pick(location.query, Object.values(URL_PARAM)),
- // Sort by name
- asc: 1,
- per_page: 1,
- },
- },
- ],
- {
- staleTime: 0,
- enabled: shouldLoadAlertRules,
- }
- );
- function renderAlertRules() {
- if (unresolvedAlertsIsError || resolvedAlertsIsError) {
- return <LoadingError message={t('Unable to load latest alerts')} />;
- }
- const isLoading = unresolvedAlertsIsLoading || resolvedAlertsIsLoading;
- if (isLoading || (shouldLoadAlertRules && alertRulesLoading)) {
- return <Placeholder height={PLACEHOLDER_AND_EMPTY_HEIGHT} />;
- }
- const hasAlertRule = alertsUnresolvedAndResolved.length > 0 || alertRules?.length > 0;
- if (!hasAlertRule) {
- return (
- <MissingAlertsButtons organization={organization} projectSlug={projectSlug} />
- );
- }
- if (alertsUnresolvedAndResolved.length === 0) {
- return (
- <StyledEmptyStateWarning small>{t('No alerts found')}</StyledEmptyStateWarning>
- );
- }
- return alertsUnresolvedAndResolved
- .slice(0, 3)
- .map(alert => (
- <AlertRow key={alert.id} alert={alert} orgSlug={organization.slug} />
- ));
- }
- return (
- <SidebarSection>
- <SectionHeadingWrapper>
- <SectionHeading>{t('Latest Alerts')}</SectionHeading>
- {/* as this is a link to latest alerts, we want to only preserve project and environment */}
- <SectionHeadingLink
- to={{
- pathname: `/organizations/${organization.slug}/alerts/`,
- query: {
- statsPeriod: undefined,
- start: undefined,
- end: undefined,
- utc: undefined,
- },
- }}
- >
- <IconOpen aria-label={t('Metric Alert History')} />
- </SectionHeadingLink>
- </SectionHeadingWrapper>
- <div>{renderAlertRules()}</div>
- </SidebarSection>
- );
- }
- 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 ? 'successText' : isWarning ? 'warningText' : 'errorText';
- 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')<StatusColorProps>`
- color: ${p => p.theme[getStatusColor(p)]};
- `;
- const StyledEmptyStateWarning = styled(EmptyStateWarning)`
- height: ${PLACEHOLDER_AND_EMPTY_HEIGHT};
- justify-content: center;
- `;
- export default ProjectLatestAlerts;
|