mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
216 lines
6.4 KiB
TypeScript
216 lines
6.4 KiB
TypeScript
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 {Member, Project} from 'sentry/types';
|
|
import type {IssueAlertRule, UnsavedIssueAlertRule} from 'sentry/types/alerts';
|
|
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 (
|
|
<Fragment>
|
|
{t("Select a condition above to see which issues would've triggered this alert")}
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
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: (
|
|
<Tooltip
|
|
title={t('Previews that include issue frequency conditions are approximated')}
|
|
showUnderline
|
|
/>
|
|
),
|
|
link: <ExternalLink href={SENTRY_ISSUE_ALERT_DOCS_URL} />,
|
|
}
|
|
);
|
|
}
|
|
|
|
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<boolean>(false);
|
|
const [previewError, setPreviewError] = useState<boolean>(false);
|
|
const [previewGroups, setPreviewGroups] = useState<string[]>([]);
|
|
const [previewPage, setPreviewPage] = useState<number>(0);
|
|
const [pageLinks, setPageLinks] = useState<string>('');
|
|
const [issueCount, setIssueCount] = useState<number>(0);
|
|
const endDateRef = useRef<string | null>(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 (
|
|
<Fragment>
|
|
<StyledListItem>
|
|
<StepHeader>{t('Preview')}</StepHeader>
|
|
<StyledFieldHelp>
|
|
<PreviewText issueCount={issueCount} previewError={previewError} />
|
|
</StyledFieldHelp>
|
|
</StyledListItem>
|
|
<ContentIndent>
|
|
<PreviewTable
|
|
previewGroups={previewGroups}
|
|
members={members}
|
|
pageLinks={pageLinks}
|
|
onCursor={onPreviewCursor}
|
|
issueCount={issueCount}
|
|
page={previewPage}
|
|
isLoading={isLoading}
|
|
error={errorMessage}
|
|
/>
|
|
</ContentIndent>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
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)};
|
|
}
|
|
`;
|