|
@@ -21,6 +21,7 @@ import AlertLink from 'sentry/components/alertLink';
|
|
|
import {Button} from 'sentry/components/button';
|
|
|
import Checkbox from 'sentry/components/checkbox';
|
|
|
import Confirm from 'sentry/components/confirm';
|
|
|
+import ErrorBoundary from 'sentry/components/errorBoundary';
|
|
|
import SelectControl from 'sentry/components/forms/controls/selectControl';
|
|
|
import FieldGroup from 'sentry/components/forms/fieldGroup';
|
|
|
import FieldHelp from 'sentry/components/forms/fieldGroup/fieldHelp';
|
|
@@ -34,15 +35,12 @@ import ExternalLink from 'sentry/components/links/externalLink';
|
|
|
import List from 'sentry/components/list';
|
|
|
import ListItem from 'sentry/components/list/listItem';
|
|
|
import LoadingMask from 'sentry/components/loadingMask';
|
|
|
-import {CursorHandler} from 'sentry/components/pagination';
|
|
|
import Panel from 'sentry/components/panels/panel';
|
|
|
import PanelBody from 'sentry/components/panels/panelBody';
|
|
|
import TeamSelector from 'sentry/components/teamSelector';
|
|
|
-import {Tooltip} from 'sentry/components/tooltip';
|
|
|
import {ALL_ENVIRONMENTS_KEY} from 'sentry/constants';
|
|
|
import {IconChevron, IconNot} from 'sentry/icons';
|
|
|
import {t, tct, tn} from 'sentry/locale';
|
|
|
-import GroupStore from 'sentry/stores/groupStore';
|
|
|
import {space} from 'sentry/styles/space';
|
|
|
import {
|
|
|
Environment,
|
|
@@ -71,7 +69,7 @@ import routeTitleGen from 'sentry/utils/routeTitle';
|
|
|
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
|
|
|
import withOrganization from 'sentry/utils/withOrganization';
|
|
|
import withProjects from 'sentry/utils/withProjects';
|
|
|
-import PreviewTable from 'sentry/views/alerts/rules/issue/previewTable';
|
|
|
+import {PreviewIssues} from 'sentry/views/alerts/rules/issue/previewIssues';
|
|
|
import {
|
|
|
CHANGE_ALERT_CONDITION_IDS,
|
|
|
CHANGE_ALERT_PLACEHOLDERS_LABELS,
|
|
@@ -121,9 +119,6 @@ const defaultRule: UnsavedIssueAlertRule = {
|
|
|
|
|
|
const POLLING_MAX_TIME_LIMIT = 3 * 60000;
|
|
|
|
|
|
-const SENTRY_ISSUE_ALERT_DOCS_URL =
|
|
|
- 'https://docs.sentry.io/product/alerts/alert-types/#issue-alerts';
|
|
|
-
|
|
|
type ConditionOrActionProperty = 'conditions' | 'actions' | 'filters';
|
|
|
|
|
|
type RuleTaskResponse = {
|
|
@@ -162,13 +157,6 @@ type State = DeprecatedAsyncView['state'] & {
|
|
|
environments: Environment[] | null;
|
|
|
incompatibleConditions: number[] | null;
|
|
|
incompatibleFilters: number[] | null;
|
|
|
- issueCount: number;
|
|
|
- loadingPreview: boolean;
|
|
|
- previewCursor: string | null | undefined;
|
|
|
- previewEndpoint: null | string;
|
|
|
- previewError: null | string;
|
|
|
- previewGroups: string[] | null;
|
|
|
- previewPage: number;
|
|
|
project: Project;
|
|
|
sendingNotification: boolean;
|
|
|
uuid: null | string;
|
|
@@ -201,28 +189,21 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
|
|
|
|
|
|
componentDidMount() {
|
|
|
super.componentDidMount();
|
|
|
- this.fetchPreview();
|
|
|
}
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
super.componentWillUnmount();
|
|
|
this.isUnmounted = true;
|
|
|
- GroupStore.reset();
|
|
|
window.clearTimeout(this.pollingTimeout);
|
|
|
this.checkIncompatibleRuleDebounced.cancel();
|
|
|
- this.fetchPreviewDebounced.cancel();
|
|
|
}
|
|
|
|
|
|
componentDidUpdate(_prevProps: Props, prevState: State) {
|
|
|
- if (prevState.previewCursor !== this.state.previewCursor) {
|
|
|
- this.fetchPreview();
|
|
|
- } else if (this.isRuleStateChange(prevState)) {
|
|
|
+ if (this.isRuleStateChange(prevState)) {
|
|
|
this.setState({
|
|
|
- loadingPreview: true,
|
|
|
incompatibleConditions: null,
|
|
|
incompatibleFilters: null,
|
|
|
});
|
|
|
- this.fetchPreviewDebounced();
|
|
|
this.checkIncompatibleRuleDebounced();
|
|
|
}
|
|
|
if (prevState.project.id === this.state.project.id) {
|
|
@@ -268,16 +249,9 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
|
|
|
environments: [],
|
|
|
uuid: null,
|
|
|
project,
|
|
|
- previewGroups: null,
|
|
|
- previewCursor: null,
|
|
|
- previewError: null,
|
|
|
- issueCount: 0,
|
|
|
- previewPage: 0,
|
|
|
- loadingPreview: false,
|
|
|
sendingNotification: false,
|
|
|
incompatibleConditions: null,
|
|
|
incompatibleFilters: null,
|
|
|
- previewEndpoint: null,
|
|
|
};
|
|
|
|
|
|
const projectTeamIds = new Set(project.teams.map(({id}) => id));
|
|
@@ -409,73 +383,6 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- fetchPreview = (resetCursor = false) => {
|
|
|
- const {organization} = this.props;
|
|
|
- const {project, rule, previewCursor, previewEndpoint} = this.state;
|
|
|
-
|
|
|
- if (!rule) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- this.setState({loadingPreview: true});
|
|
|
- if (resetCursor) {
|
|
|
- this.setState({previewCursor: null, previewPage: 0});
|
|
|
- }
|
|
|
- // we currently don't have a way to parse objects from query params, so this method is POST for now
|
|
|
- this.api
|
|
|
- .requestPromise(`/projects/${organization.slug}/${project.slug}/rules/preview/`, {
|
|
|
- method: 'POST',
|
|
|
- includeAllArgs: true,
|
|
|
- query: {
|
|
|
- cursor: resetCursor ? null : previewCursor,
|
|
|
- per_page: 5,
|
|
|
- },
|
|
|
- data: {
|
|
|
- conditions: rule?.conditions || [],
|
|
|
- filters: rule?.filters || [],
|
|
|
- actionMatch: rule?.actionMatch || 'all',
|
|
|
- filterMatch: rule?.filterMatch || 'all',
|
|
|
- frequency: rule?.frequency || 60,
|
|
|
- endpoint: previewEndpoint,
|
|
|
- },
|
|
|
- })
|
|
|
- .then(([data, _, resp]) => {
|
|
|
- if (this.isUnmounted) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- GroupStore.add(data);
|
|
|
-
|
|
|
- const pageLinks = resp?.getResponseHeader('Link');
|
|
|
- const hits = resp?.getResponseHeader('X-Hits');
|
|
|
- const endpoint = resp?.getResponseHeader('Endpoint');
|
|
|
- const issueCount =
|
|
|
- typeof hits !== 'undefined' && hits ? parseInt(hits, 10) || 0 : 0;
|
|
|
- this.setState({
|
|
|
- previewGroups: data.map(g => g.id),
|
|
|
- previewError: null,
|
|
|
- pageLinks: pageLinks ?? '',
|
|
|
- issueCount,
|
|
|
- loadingPreview: false,
|
|
|
- previewEndpoint: endpoint ?? null,
|
|
|
- });
|
|
|
- })
|
|
|
- .catch(_ => {
|
|
|
- const errorMessage =
|
|
|
- rule?.conditions.length || rule?.filters.length
|
|
|
- ? t('Preview is not supported for these conditions')
|
|
|
- : t('Select a condition to generate a preview');
|
|
|
- this.setState({
|
|
|
- previewError: errorMessage,
|
|
|
- loadingPreview: false,
|
|
|
- });
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- fetchPreviewDebounced = debounce(() => {
|
|
|
- this.fetchPreview(true);
|
|
|
- }, 1000);
|
|
|
-
|
|
|
// As more incompatible combinations are added, we will need a more generic way to check for incompatibility.
|
|
|
checkIncompatibleRuleDebounced = debounce(() => {
|
|
|
const {conditionIndices, filterIndices} = findIncompatibleRules(this.state.rule);
|
|
@@ -494,13 +401,6 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
|
|
|
});
|
|
|
}, 500);
|
|
|
|
|
|
- onPreviewCursor: CursorHandler = (cursor, _1, _2, direction) => {
|
|
|
- this.setState({
|
|
|
- previewCursor: cursor,
|
|
|
- previewPage: this.state.previewPage + direction,
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
fetchEnvironments() {
|
|
|
const {organization} = this.props;
|
|
|
const {project} = this.state;
|
|
@@ -1116,52 +1016,6 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- renderPreviewText() {
|
|
|
- const {issueCount, previewError} = this.state;
|
|
|
- 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: (
|
|
|
- <Tooltip
|
|
|
- title={t('Previews that include issue frequency conditions are approximated')}
|
|
|
- showUnderline
|
|
|
- />
|
|
|
- ),
|
|
|
- link: <ExternalLink href={SENTRY_ISSUE_ALERT_DOCS_URL} />,
|
|
|
- }
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- renderPreviewTable() {
|
|
|
- const {members} = this.props;
|
|
|
- const {
|
|
|
- previewGroups,
|
|
|
- previewError,
|
|
|
- pageLinks,
|
|
|
- issueCount,
|
|
|
- previewPage,
|
|
|
- loadingPreview,
|
|
|
- } = this.state;
|
|
|
- return (
|
|
|
- <PreviewTable
|
|
|
- previewGroups={previewGroups}
|
|
|
- members={members}
|
|
|
- pageLinks={pageLinks}
|
|
|
- onCursor={this.onPreviewCursor}
|
|
|
- issueCount={issueCount}
|
|
|
- page={previewPage}
|
|
|
- loading={loadingPreview}
|
|
|
- error={previewError}
|
|
|
- />
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
renderProjectSelect(disabled: boolean) {
|
|
|
const {project: _selectedProject, projects, organization} = this.props;
|
|
|
const {rule} = this.state;
|
|
@@ -1269,7 +1123,7 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
|
|
|
}
|
|
|
|
|
|
renderBody() {
|
|
|
- const {organization} = this.props;
|
|
|
+ const {organization, members} = this.props;
|
|
|
const {
|
|
|
project,
|
|
|
rule,
|
|
@@ -1570,15 +1424,9 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
|
|
|
</StyledFieldHelp>
|
|
|
</StyledListItem>
|
|
|
<ContentIndent>{this.renderActionInterval(disabled)}</ContentIndent>
|
|
|
- <StyledListItem>
|
|
|
- <StyledListItemSpaced>
|
|
|
- <div>
|
|
|
- <StepHeader>{t('Preview')}</StepHeader>
|
|
|
- <StyledFieldHelp>{this.renderPreviewText()}</StyledFieldHelp>
|
|
|
- </div>
|
|
|
- </StyledListItemSpaced>
|
|
|
- </StyledListItem>
|
|
|
- <ContentIndent>{this.renderPreviewTable()}</ContentIndent>
|
|
|
+ <ErrorBoundary mini>
|
|
|
+ <PreviewIssues members={members} rule={rule} project={project} />
|
|
|
+ </ErrorBoundary>
|
|
|
<StyledListItem>
|
|
|
<StepHeader>{t('Add a name and owner')}</StepHeader>
|
|
|
<StyledFieldHelp>
|
|
@@ -1719,11 +1567,6 @@ const StyledListItem = styled(ListItem)`
|
|
|
font-size: ${p => p.theme.fontSizeExtraLarge};
|
|
|
`;
|
|
|
|
|
|
-const StyledListItemSpaced = styled('div')`
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
-`;
|
|
|
-
|
|
|
const StyledFieldHelp = styled(FieldHelp)`
|
|
|
margin-top: 0;
|
|
|
@media (max-width: ${p => p.theme.breakpoints.small}) {
|