|
@@ -15,6 +15,7 @@ import {
|
|
|
} from 'sentry/actionCreators/indicator';
|
|
|
import {updateOnboardingTask} from 'sentry/actionCreators/onboardingTasks';
|
|
|
import Access from 'sentry/components/acl/access';
|
|
|
+import Feature from 'sentry/components/acl/feature';
|
|
|
import Alert from 'sentry/components/alert';
|
|
|
import Button from 'sentry/components/button';
|
|
|
import Confirm from 'sentry/components/confirm';
|
|
@@ -49,6 +50,7 @@ import {
|
|
|
IssueAlertRuleAction,
|
|
|
IssueAlertRuleActionTemplate,
|
|
|
IssueAlertRuleConditionTemplate,
|
|
|
+ ProjectAlertRuleStats,
|
|
|
UnsavedIssueAlertRule,
|
|
|
} from 'sentry/types/alerts';
|
|
|
import {metric} from 'sentry/utils/analytics';
|
|
@@ -59,6 +61,7 @@ import recreateRoute from 'sentry/utils/recreateRoute';
|
|
|
import routeTitleGen from 'sentry/utils/routeTitle';
|
|
|
import withOrganization from 'sentry/utils/withOrganization';
|
|
|
import withProjects from 'sentry/utils/withProjects';
|
|
|
+import PreviewChart from 'sentry/views/alerts/rules/issue/previewChart';
|
|
|
import {
|
|
|
CHANGE_ALERT_CONDITION_IDS,
|
|
|
CHANGE_ALERT_PLACEHOLDERS_LABELS,
|
|
@@ -134,7 +137,9 @@ type State = AsyncView['state'] & {
|
|
|
[key: string]: string[];
|
|
|
};
|
|
|
environments: Environment[] | null;
|
|
|
+ previewError: null | string;
|
|
|
project: Project;
|
|
|
+ ruleFireHistory: ProjectAlertRuleStats[] | null;
|
|
|
uuid: null | string;
|
|
|
duplicateTargetRule?: UnsavedIssueAlertRule | IssueAlertRule | null;
|
|
|
ownership?: null | IssueOwnership;
|
|
@@ -189,6 +194,7 @@ class IssueRuleEditor extends AsyncView<Props, State> {
|
|
|
environments: [],
|
|
|
uuid: null,
|
|
|
project,
|
|
|
+ ruleFireHistory: null,
|
|
|
};
|
|
|
|
|
|
const projectTeamIds = new Set(project.teams.map(({id}) => id));
|
|
@@ -301,6 +307,42 @@ class IssueRuleEditor extends AsyncView<Props, State> {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ fetchPreview = async () => {
|
|
|
+ const {organization} = this.props;
|
|
|
+ const {project, rule} = this.state;
|
|
|
+
|
|
|
+ if (!rule) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.setState({loadingPreview: true});
|
|
|
+ try {
|
|
|
+ const response = await this.api.requestPromise(
|
|
|
+ `/projects/${organization.slug}/${project.slug}/rules/preview`,
|
|
|
+ {
|
|
|
+ method: 'POST',
|
|
|
+ data: {
|
|
|
+ conditions: rule?.conditions || [],
|
|
|
+ filters: rule?.filters || [],
|
|
|
+ actionMatch: rule?.actionMatch || 'all',
|
|
|
+ filterMatch: rule?.filterMatch || 'all',
|
|
|
+ frequency: rule?.frequency || 60,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ );
|
|
|
+ this.setState({
|
|
|
+ ruleFireHistory: response,
|
|
|
+ previewError: null,
|
|
|
+ loadingPreview: false,
|
|
|
+ });
|
|
|
+ } catch (err) {
|
|
|
+ this.setState({
|
|
|
+ previewError:
|
|
|
+ 'Previews are unavailable for this combination of conditions and filters',
|
|
|
+ loadingPreview: false,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
fetchEnvironments() {
|
|
|
const {
|
|
|
params: {orgId},
|
|
@@ -759,6 +801,21 @@ class IssueRuleEditor extends AsyncView<Props, State> {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ renderPreviewGraph() {
|
|
|
+ const {ruleFireHistory, previewError} = this.state;
|
|
|
+ if (ruleFireHistory && !previewError) {
|
|
|
+ return <PreviewChart ruleFireHistory={ruleFireHistory} />;
|
|
|
+ }
|
|
|
+ if (previewError) {
|
|
|
+ return (
|
|
|
+ <Alert type="error" showIcon>
|
|
|
+ {previewError}
|
|
|
+ </Alert>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
renderProjectSelect(disabled: boolean) {
|
|
|
const {project: _selectedProject, projects, organization} = this.props;
|
|
|
const {rule} = this.state;
|
|
@@ -892,7 +949,7 @@ class IssueRuleEditor extends AsyncView<Props, State> {
|
|
|
|
|
|
renderBody() {
|
|
|
const {organization} = this.props;
|
|
|
- const {project, rule, detailedError, loading, ownership} = this.state;
|
|
|
+ const {project, rule, detailedError, loading, ownership, loadingPreview} = this.state;
|
|
|
const {actions, filters, conditions, frequency} = rule || {};
|
|
|
|
|
|
const environment =
|
|
@@ -1148,6 +1205,28 @@ class IssueRuleEditor extends AsyncView<Props, State> {
|
|
|
</StyledFieldHelp>
|
|
|
</StyledListItem>
|
|
|
{this.renderActionInterval(disabled)}
|
|
|
+ <Feature organization={organization} features={['issue-alert-preview']}>
|
|
|
+ <StyledListItem>
|
|
|
+ <StyledListItemSpaced>
|
|
|
+ <div>
|
|
|
+ {t('Preview history graph')}
|
|
|
+ <StyledFieldHelp>
|
|
|
+ {t(
|
|
|
+ 'Shows when this rule would have fired in the past 2 weeks'
|
|
|
+ )}
|
|
|
+ </StyledFieldHelp>
|
|
|
+ </div>
|
|
|
+ <Button
|
|
|
+ onClick={this.fetchPreview}
|
|
|
+ type="button"
|
|
|
+ disabled={loadingPreview}
|
|
|
+ >
|
|
|
+ Generate Preview
|
|
|
+ </Button>
|
|
|
+ </StyledListItemSpaced>
|
|
|
+ </StyledListItem>
|
|
|
+ {this.renderPreviewGraph()}
|
|
|
+ </Feature>
|
|
|
<StyledListItem>{t('Establish ownership')}</StyledListItem>
|
|
|
{this.renderRuleName(disabled)}
|
|
|
{this.renderTeamSelect(disabled)}
|
|
@@ -1182,6 +1261,11 @@ 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;
|
|
|
`;
|