import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import styled from '@emotion/styled'; import debounce from 'lodash/debounce'; import FieldHelp from 'sentry/components/forms/fieldGroup/fieldHelp'; import ExternalLink from 'sentry/components/links/externalLink'; import ListItem from 'sentry/components/list/listItem'; import type {CursorHandler} from 'sentry/components/pagination'; import {Tooltip} from 'sentry/components/tooltip'; import {t, tct} from 'sentry/locale'; import GroupStore from 'sentry/stores/groupStore'; import {space} from 'sentry/styles/space'; import type {IssueAlertRule, UnsavedIssueAlertRule} from 'sentry/types/alerts'; import type {Member} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import useApi from 'sentry/utils/useApi'; import {useIsMountedRef} from 'sentry/utils/useIsMountedRef'; import useOrganization from 'sentry/utils/useOrganization'; import PreviewTable from './previewTable'; const SENTRY_ISSUE_ALERT_DOCS_URL = 'https://docs.sentry.io/product/alerts/alert-types/#issue-alerts'; function PreviewText({issueCount, previewError}) { if (previewError) { return ( {t("Select a condition above to see which issues would've triggered this alert")} ); } return tct( "[issueCount] issues would have triggered this rule in the past 14 days [approximately:approximately]. If you're looking to reduce noise then make sure to [link:read the docs].", { issueCount, approximately: ( ), link: , } ); } interface PreviewIssuesProps { members: Member[] | undefined; project: Project; rule?: UnsavedIssueAlertRule | IssueAlertRule | null; } export function PreviewIssues({members, rule, project}: PreviewIssuesProps) { const api = useApi(); const organization = useOrganization(); const isMounted = useIsMountedRef(); const [isLoading, setIsLoading] = useState(false); const [previewError, setPreviewError] = useState(false); const [previewGroups, setPreviewGroups] = useState([]); const [previewPage, setPreviewPage] = useState(0); const [pageLinks, setPageLinks] = useState(''); const [issueCount, setIssueCount] = useState(0); const endDateRef = useRef(null); /** * If any of this data changes we'll need to re-fetch the preview */ const relevantRuleData = useMemo( () => rule ? { conditions: rule.conditions || [], filters: rule.filters || [], actionMatch: rule.actionMatch || 'all', filterMatch: rule.filterMatch || 'all', frequency: rule.frequency || 60, } : {}, [rule] ); /** * Not using useApiQuery because it makes a post request */ const fetchApiData = useCallback( (ruleFields: any, cursor?: string | null, resetCursor?: boolean) => { setIsLoading(true); if (resetCursor) { setPreviewPage(0); } // we currently don't have a way to parse objects from query params, so this method is POST for now api .requestPromise(`/projects/${organization.slug}/${project.slug}/rules/preview/`, { method: 'POST', includeAllArgs: true, query: { cursor, per_page: 5, }, data: { ...ruleFields, // so the end date doesn't change? Not sure. endpoint: endDateRef.current, }, }) .then(([data, _, resp]) => { if (!isMounted.current) { return; } GroupStore.add(data); const hits = resp?.getResponseHeader('X-Hits'); const count = typeof hits !== 'undefined' && hits ? parseInt(hits, 10) : 0; setPreviewGroups(data.map(g => g.id)); setPreviewError(false); setPageLinks(resp?.getResponseHeader('Link') ?? ''); setIssueCount(count); setIsLoading(false); endDateRef.current = resp?.getResponseHeader('Endpoint') ?? null; }) .catch(_ => { setPreviewError(true); setIsLoading(false); }); }, [ setIsLoading, setPreviewError, setPreviewGroups, setIssueCount, api, project.slug, organization.slug, isMounted, ] ); const debouncedFetchApiData = useMemo( () => debounce(fetchApiData, 500), [fetchApiData] ); useEffect(() => { debouncedFetchApiData(relevantRuleData, null, true); // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(relevantRuleData), debouncedFetchApiData]); useEffect(() => { return () => { debouncedFetchApiData.cancel(); // Reset the group store when leaving GroupStore.reset(); }; }, [debouncedFetchApiData]); const onPreviewCursor: CursorHandler = (cursor, _1, _2, direction) => { setPreviewPage(previewPage + direction); debouncedFetchApiData.cancel(); fetchApiData(relevantRuleData, cursor); }; const errorMessage = previewError ? rule?.conditions.length || rule?.filters.length ? t('Preview is not supported for these conditions') : t('Select a condition to generate a preview') : null; return ( {t('Preview')} ); } const StyledListItem = styled(ListItem)` margin: ${space(2)} 0 ${space(1)} 0; font-size: ${p => p.theme.fontSizeExtraLarge}; `; const StepHeader = styled('h5')` margin-bottom: ${space(1)}; `; const StyledFieldHelp = styled(FieldHelp)` margin-top: 0; @media (max-width: ${p => p.theme.breakpoints.small}) { margin-left: -${space(4)}; } `; const ContentIndent = styled('div')` @media (min-width: ${p => p.theme.breakpoints.small}) { margin-left: ${space(4)}; } `;