import type {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import pick from 'lodash/pick'; import moment from 'moment'; import Access from 'sentry/components/acl/access'; import {Alert} from 'sentry/components/alert'; import SnoozeAlert from 'sentry/components/alerts/snoozeAlert'; import Breadcrumbs from 'sentry/components/breadcrumbs'; import {Button} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import type {DateTimeObject} from 'sentry/components/charts/utils'; import ErrorBoundary from 'sentry/components/errorBoundary'; import IdBadge from 'sentry/components/idBadge'; import * as Layout from 'sentry/components/layouts/thirds'; import Link from 'sentry/components/links/link'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import {ChangeData} from 'sentry/components/organizations/timeRangeSelector'; import PageTimeRangeSelector from 'sentry/components/pageTimeRangeSelector'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {IconCopy, IconEdit} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {DateString} from 'sentry/types'; import type {IssueAlertRule} from 'sentry/types/alerts'; import {trackAnalytics} from 'sentry/utils/analytics'; import { ApiQueryKey, setApiQueryData, useApiQuery, useQueryClient, } from 'sentry/utils/queryClient'; import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames'; import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; import {findIncompatibleRules} from 'sentry/views/alerts/rules/issue'; import {ALERT_DEFAULT_CHART_PERIOD} from 'sentry/views/alerts/rules/metric/details/constants'; import {IssueAlertDetailsChart} from './alertChart'; import AlertRuleIssuesList from './issuesList'; import Sidebar from './sidebar'; interface AlertRuleDetailsProps extends RouteComponentProps<{projectId: string; ruleId: string}, {}> {} const PAGE_QUERY_PARAMS = [ 'pageStatsPeriod', 'pageStart', 'pageEnd', 'pageUtc', 'cursor', ]; const getIssueAlertDetailsQueryKey = ({ orgSlug, projectSlug, ruleId, }: { orgSlug: string; projectSlug: string; ruleId: string; }): ApiQueryKey => [ `/projects/${orgSlug}/${projectSlug}/rules/${ruleId}/`, {query: {expand: 'lastTriggered'}}, ]; function AlertRuleDetails({params, location, router}: AlertRuleDetailsProps) { const queryClient = useQueryClient(); const organization = useOrganization(); const {projects, fetching: projectIsLoading} = useProjects(); const project = projects.find(({slug}) => slug === params.projectId); const {projectId: projectSlug, ruleId} = params; const { data: rule, isLoading, isError, } = useApiQuery( getIssueAlertDetailsQueryKey({orgSlug: organization.slug, projectSlug, ruleId}), {staleTime: 0} ); useRouteAnalyticsEventNames( 'issue_alert_rule_details.viewed', 'Issue Alert Rule Details: Viewed' ); useRouteAnalyticsParams({rule_id: parseInt(params.ruleId, 10)}); function getDataDatetime(): DateTimeObject { const query = location?.query ?? {}; const { start, end, statsPeriod, utc: utcString, } = normalizeDateTimeParams(query, { allowEmptyPeriod: true, allowAbsoluteDatetime: true, allowAbsolutePageDatetime: true, }); if (!statsPeriod && !start && !end) { return {period: ALERT_DEFAULT_CHART_PERIOD}; } // Following getParams, statsPeriod will take priority over start/end if (statsPeriod) { return {period: statsPeriod}; } const utc = utcString === 'true'; if (start && end) { return utc ? { start: moment.utc(start).format(), end: moment.utc(end).format(), utc, } : { start: moment(start).utc().format(), end: moment(end).utc().format(), utc, }; } return {period: ALERT_DEFAULT_CHART_PERIOD}; } function setStateOnUrl(nextState: { cursor?: string; pageEnd?: DateString; pageStart?: DateString; pageStatsPeriod?: string | null; pageUtc?: boolean | null; team?: string; }) { return router.push({ ...location, query: { ...location.query, ...pick(nextState, PAGE_QUERY_PARAMS), }, }); } function onSnooze({ snooze, snoozeCreatedBy, snoozeForEveryone, }: { snooze: boolean; snoozeCreatedBy?: string; snoozeForEveryone?: boolean; }) { setApiQueryData( queryClient, getIssueAlertDetailsQueryKey({orgSlug: organization.slug, projectSlug, ruleId}), alertRule => ({...alertRule, snooze, snoozeCreatedBy, snoozeForEveryone}) ); } function handleUpdateDatetime(datetime: ChangeData) { const {start, end, relative, utc} = datetime; if (start && end) { const parser = utc ? moment.utc : moment; return setStateOnUrl({ pageStatsPeriod: undefined, pageStart: parser(start).format(), pageEnd: parser(end).format(), pageUtc: utc ?? undefined, cursor: undefined, }); } return setStateOnUrl({ pageStatsPeriod: relative || undefined, pageStart: undefined, pageEnd: undefined, pageUtc: undefined, cursor: undefined, }); } if (isLoading || projectIsLoading) { return ( ); } if (!rule || isError) { return ( ); } if (!project) { return ( ); } const hasSnoozeFeature = organization.features.includes('mute-alerts'); const isSnoozed = rule.snooze; const duplicateLink = { pathname: `/organizations/${organization.slug}/alerts/new/issue/`, query: { project: project.slug, duplicateRuleId: rule.id, createFromDuplicate: true, referrer: 'issue_rule_details', }, }; function renderIncompatibleAlert() { const incompatibleRule = findIncompatibleRules(rule); if ( (incompatibleRule.conditionIndices || incompatibleRule.filterIndices) && organization.features.includes('issue-alert-incompatible-rules') ) { return ( {tct( 'The conditions in this alert rule conflict and might not be working properly. [link:Edit alert rule]', { link: ( ), } )} ); } return null; } const {period, start, end, utc} = getDataDatetime(); const {cursor} = location.query; return ( {rule.name} {hasSnoozeFeature && ( {({hasAccess}) => ( )} )} {renderIncompatibleAlert()} {hasSnoozeFeature && isSnoozed && ( {tct( "[creator] muted this alert[forEveryone]so you won't get these notifications in the future.", { creator: rule.snoozeCreatedBy, forEveryone: rule.snoozeForEveryone ? ' for everyone ' : ' ', } )} )} ); } export default AlertRuleDetails; const StyledPageTimeRangeSelector = styled(PageTimeRangeSelector)` margin-bottom: ${space(2)}; `; const StyledLoadingError = styled(LoadingError)` margin: ${space(2)}; `;