Browse Source

feat(alerts): Alert wizard v3 pre-project selector projectId route parameter (#33307)

* feat(alerts): Alert wizard v3 pre-project selector projectId route parameter

* browser history to router
Taylan Gocmen 2 years ago
parent
commit
c50e25d279

+ 12 - 0
static/app/routes.tsx

@@ -1081,6 +1081,18 @@ function buildRoutes() {
           />
         </Route>
       </Route>
+      <Route
+        path="new/"
+        name={t('New Alert Rule')}
+        component={SafeLazyLoad}
+        componentPromise={() => import('sentry/views/alerts/builder/projectProvider')}
+      >
+        <Route
+          path=":alertType/"
+          component={SafeLazyLoad}
+          componentPromise={() => import('sentry/views/alerts/create')}
+        />
+      </Route>
       <Route
         path=":alertId/"
         componentPromise={() => import('sentry/views/alerts/incidentRedirect')}

+ 14 - 7
static/app/views/alerts/builder/builderBreadCrumbs.tsx

@@ -6,6 +6,7 @@ import Breadcrumbs, {Crumb, CrumbDropdown} from 'sentry/components/breadcrumbs';
 import IdBadge from 'sentry/components/idBadge';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
+import {Organization} from 'sentry/types';
 import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
 import recreateRoute from 'sentry/utils/recreateRoute';
 import useProjects from 'sentry/utils/useProjects';
@@ -14,43 +15,49 @@ import type {RouteWithName} from 'sentry/views/settings/components/settingsBread
 
 interface Props {
   location: Location;
-  orgSlug: string;
+  organization: Organization;
   projectSlug: string;
   routes: RouteWithName[];
   title: string;
   alertName?: string;
+  alertType?: string;
   canChangeProject?: boolean;
 }
 
 function BuilderBreadCrumbs({
-  orgSlug,
   title,
   alertName,
   projectSlug,
   routes,
   canChangeProject,
   location,
+  organization,
+  alertType,
 }: Props) {
   const {projects} = useProjects();
   const isSuperuser = isActiveSuperuser();
   const project = projects.find(({slug}) => projectSlug === slug);
+  const hasAlertWizardV3 = organization.features.includes('alert-wizard-v3');
 
   const label = (
     <IdBadge project={project ?? {slug: projectSlug}} avatarSize={18} disableLink />
   );
 
   const projectCrumbLink: Crumb = {
-    to: `/organizations/${orgSlug}/alerts/rules/?project=${project?.id}`,
+    to: `/organizations/${organization.slug}/alerts/rules/?project=${project?.id}`,
     label,
   };
 
   function getProjectDropdownCrumb(): CrumbDropdown {
     return {
-      onSelect: ({value}) => {
+      onSelect: ({value: projectId}) => {
+        // TODO(taylangocmen): recreating route doesn't update query, don't edit recreateRoute will add project selector for alert-wizard-v3
         browserHistory.push(
           recreateRoute('', {
             routes,
-            params: {orgId: orgSlug, projectId: value},
+            params: hasAlertWizardV3
+              ? {orgId: organization.slug, alertType}
+              : {orgId: organization.slug, projectId},
             location,
           })
         );
@@ -80,7 +87,7 @@ function BuilderBreadCrumbs({
 
   const crumbs: (Crumb | CrumbDropdown)[] = [
     {
-      to: `/organizations/${orgSlug}/alerts/rules/`,
+      to: `/organizations/${organization.slug}/alerts/rules/`,
       label: t('Alerts'),
       preservePageFilters: true,
     },
@@ -89,7 +96,7 @@ function BuilderBreadCrumbs({
       label: title,
       ...(alertName
         ? {
-            to: `/organizations/${orgSlug}/alerts/${projectSlug}/wizard`,
+            to: `/organizations/${organization.slug}/alerts/${projectSlug}/wizard`,
             preservePageFilters: true,
           }
         : {}),

+ 2 - 2
static/app/views/alerts/builder/projectProvider.tsx

@@ -17,7 +17,7 @@ type Props = RouteComponentProps<RouteParams, {}> & {
 };
 
 type RouteParams = {
-  projectId: string;
+  projectId?: string;
 };
 
 function AlertBuilderProjectProvider(props: Props) {
@@ -25,7 +25,7 @@ function AlertBuilderProjectProvider(props: Props) {
   useScrollToTop({location: props.location});
 
   const {children, params, organization, ...other} = props;
-  const {projectId} = params;
+  const projectId = params.projectId || props.location.query.project;
   const {projects, initiallyLoaded, fetching, fetchError} = useProjects({
     slugs: [projectId],
   });

+ 51 - 32
static/app/views/alerts/create.tsx

@@ -1,5 +1,5 @@
 import {Component, Fragment} from 'react';
-import {browserHistory, RouteComponentProps} from 'react-router';
+import {RouteComponentProps} from 'react-router';
 import styled from '@emotion/styled';
 
 import * as Layout from 'sentry/components/layouts/thirds';
@@ -14,16 +14,19 @@ import Teams from 'sentry/utils/teams';
 import BuilderBreadCrumbs from 'sentry/views/alerts/builder/builderBreadCrumbs';
 import IncidentRulesCreate from 'sentry/views/alerts/incidentRules/create';
 import IssueRuleEditor from 'sentry/views/alerts/issueRuleEditor';
+import {AlertRuleType} from 'sentry/views/alerts/types';
 import {
   AlertType as WizardAlertType,
   AlertWizardAlertNames,
+  DEFAULT_WIZARD_TEMPLATE,
   WizardRuleTemplate,
 } from 'sentry/views/alerts/wizard/options';
 import {getAlertTypeFromAggregateDataset} from 'sentry/views/alerts/wizard/utils';
 
 type RouteParams = {
   orgId: string;
-  projectId: string;
+  alertType?: AlertRuleType;
+  projectId?: string;
 };
 
 type Props = RouteComponentProps<RouteParams, {}> & {
@@ -32,35 +35,53 @@ type Props = RouteComponentProps<RouteParams, {}> & {
   project: Project;
 };
 
-type AlertType = 'metric' | 'issue';
-
 type State = {
-  alertType: AlertType;
+  alertType: AlertRuleType;
 };
 
 class Create extends Component<Props, State> {
   state = this.getInitialState();
 
   getInitialState(): State {
-    const {organization, location, project} = this.props;
-    const {createFromDiscover, createFromWizard, aggregate, dataset, eventTypes} =
-      location?.query ?? {};
-    let alertType: AlertType = 'issue';
-
-    // Alerts can only be created via create from discover or alert wizard
-    if (createFromDiscover) {
-      alertType = 'metric';
+    const {organization, location, project, params, router} = this.props;
+    const {
+      createFromDiscover,
+      createFromWizard,
+      aggregate,
+      dataset,
+      eventTypes,
+      createFromV3,
+    } = location?.query ?? {};
+    let alertType = AlertRuleType.ISSUE;
+
+    const hasAlertWizardV3 = organization.features.includes('alert-wizard-v3');
+
+    // Alerts can only be created via create from discover or alert wizard, until alert-wizard-v3 is fully implemented
+    if (hasAlertWizardV3 && createFromV3) {
+      alertType = params.alertType || AlertRuleType.METRIC;
+
+      if (alertType === AlertRuleType.METRIC && !(aggregate && dataset && eventTypes)) {
+        router.replace({
+          ...location,
+          pathname: `/organizations/${organization.slug}/alerts/new/${alertType}`,
+          query: {
+            ...location.query,
+            ...DEFAULT_WIZARD_TEMPLATE,
+            project: project.slug,
+          },
+        });
+      }
+    } else if (createFromDiscover) {
+      alertType = AlertRuleType.METRIC;
     } else if (createFromWizard) {
       if (aggregate && dataset && eventTypes) {
-        alertType = 'metric';
+        alertType = AlertRuleType.METRIC;
       } else {
         // Just to be explicit
-        alertType = 'issue';
+        alertType = AlertRuleType.ISSUE;
       }
     } else {
-      browserHistory.replace(
-        `/organizations/${organization.slug}/alerts/${project.slug}/wizard`
-      );
+      router.replace(`/organizations/${organization.slug}/alerts/${project.slug}/wizard`);
     }
 
     return {alertType};
@@ -80,22 +101,19 @@ class Create extends Component<Props, State> {
   sessionId = uniqueId();
 
   render() {
-    const {
-      hasMetricAlerts,
-      organization,
-      project,
-      params: {projectId},
-      location,
-      routes,
-    } = this.props;
+    const {hasMetricAlerts, organization, project, location, routes} = this.props;
     const {alertType} = this.state;
     const {aggregate, dataset, eventTypes, createFromWizard, createFromDiscover} =
       location?.query ?? {};
-    const wizardTemplate: WizardRuleTemplate = {aggregate, dataset, eventTypes};
+    const wizardTemplate: WizardRuleTemplate = {
+      aggregate: aggregate ?? DEFAULT_WIZARD_TEMPLATE.aggregate,
+      dataset: dataset ?? DEFAULT_WIZARD_TEMPLATE.dataset,
+      eventTypes: eventTypes ?? DEFAULT_WIZARD_TEMPLATE.eventTypes,
+    };
     const eventView = createFromDiscover ? EventView.fromLocation(location) : undefined;
 
     let wizardAlertType: undefined | WizardAlertType;
-    if (createFromWizard && alertType === 'metric') {
+    if (createFromWizard && alertType === AlertRuleType.METRIC) {
       wizardAlertType = wizardTemplate
         ? getAlertTypeFromAggregateDataset(wizardTemplate)
         : 'issues';
@@ -105,15 +123,16 @@ class Create extends Component<Props, State> {
 
     return (
       <Fragment>
-        <SentryDocumentTitle title={title} projectSlug={projectId} />
+        <SentryDocumentTitle title={title} projectSlug={project.slug} />
 
         <Layout.Header>
           <StyledHeaderContent>
             <BuilderBreadCrumbs
-              orgSlug={organization.slug}
+              organization={organization}
               alertName={t('Set Conditions')}
               title={wizardAlertType ? t('Select Alert') : title}
-              projectSlug={projectId}
+              projectSlug={project.slug}
+              alertType={alertType}
               routes={routes}
               location={location}
               canChangeProject
@@ -139,7 +158,7 @@ class Create extends Component<Props, State> {
                       />
                     )}
 
-                    {hasMetricAlerts && alertType === 'metric' && (
+                    {hasMetricAlerts && alertType === AlertRuleType.METRIC && (
                       <IncidentRulesCreate
                         {...this.props}
                         eventView={eventView}

+ 7 - 4
static/app/views/alerts/edit.tsx

@@ -12,6 +12,7 @@ import Teams from 'sentry/utils/teams';
 import BuilderBreadCrumbs from 'sentry/views/alerts/builder/builderBreadCrumbs';
 import IncidentRulesDetails from 'sentry/views/alerts/incidentRules/details';
 import IssueEditor from 'sentry/views/alerts/issueRuleEditor';
+import {AlertRuleType} from 'sentry/views/alerts/types';
 
 type RouteParams = {
   orgId: string;
@@ -52,8 +53,10 @@ class ProjectAlertsEditor extends Component<Props, State> {
     return `${ruleName}`;
   }
 
-  getAlertType(): 'metric' | 'issue' {
-    return location.pathname.includes('/alerts/metric-rules/') ? 'metric' : 'issue';
+  getAlertType(): AlertRuleType {
+    return location.pathname.includes('/alerts/metric-rules/')
+      ? AlertRuleType.METRIC
+      : AlertRuleType.ISSUE;
   }
 
   render() {
@@ -70,7 +73,7 @@ class ProjectAlertsEditor extends Component<Props, State> {
         <Layout.Header>
           <Layout.HeaderContent>
             <BuilderBreadCrumbs
-              orgSlug={organization.slug}
+              organization={organization}
               title={t('Edit Alert Rule')}
               projectSlug={project.slug}
               routes={routes}
@@ -93,7 +96,7 @@ class ProjectAlertsEditor extends Component<Props, State> {
                         userTeamIds={teams.map(({id}) => id)}
                       />
                     )}
-                    {hasMetricAlerts && alertType === 'metric' && (
+                    {hasMetricAlerts && alertType === AlertRuleType.METRIC && (
                       <IncidentRulesDetails
                         {...this.props}
                         project={project}

+ 1 - 1
static/app/views/alerts/incidentRules/create.tsx

@@ -14,7 +14,7 @@ import RuleForm from './ruleForm';
 
 type RouteParams = {
   orgId: string;
-  projectId: string;
+  projectId?: string;
   ruleId?: string;
 };
 

+ 15 - 8
static/app/views/alerts/incidentRules/ruleForm/index.tsx

@@ -33,6 +33,7 @@ import Triggers from 'sentry/views/alerts/incidentRules/triggers';
 import TriggersChart from 'sentry/views/alerts/incidentRules/triggers/chart';
 import {getEventTypeFilter} from 'sentry/views/alerts/incidentRules/utils/getEventTypeFilter';
 import hasThresholdValue from 'sentry/views/alerts/incidentRules/utils/hasThresholdValue';
+import {AlertRuleType} from 'sentry/views/alerts/types';
 import {AlertWizardAlertNames} from 'sentry/views/alerts/wizard/options';
 import {getAlertTypeFromAggregateDataset} from 'sentry/views/alerts/wizard/utils';
 
@@ -74,7 +75,7 @@ type Props = {
   isCustomMetric?: boolean;
   ruleId?: string;
   sessionId?: string;
-} & RouteComponentProps<{orgId: string; projectId: string; ruleId?: string}, {}> & {
+} & RouteComponentProps<{orgId: string; projectId?: string; ruleId?: string}, {}> & {
     onSubmitSuccess?: Form['props']['onSubmitSuccess'];
   } & AsyncComponent['props'];
 
@@ -462,8 +463,15 @@ class RuleFormContainer extends AsyncComponent<Props, State> {
       return;
     }
 
-    const {organization, params, rule, onSubmitSuccess, location, sessionId} = this.props;
-    const {ruleId} = this.props.params;
+    const {
+      organization,
+      project,
+      rule,
+      onSubmitSuccess,
+      location,
+      sessionId,
+      params: {ruleId},
+    } = this.props;
     const {
       aggregate,
       resolveThreshold,
@@ -488,7 +496,7 @@ class RuleFormContainer extends AsyncComponent<Props, State> {
     );
     try {
       const transaction = metric.startTransaction({name: 'saveAlertRule'});
-      transaction.setTag('type', 'metric');
+      transaction.setTag('type', AlertRuleType.METRIC);
       transaction.setTag('operation', !rule.id ? 'create' : 'edit');
       for (const trigger of sanitizedTriggers) {
         for (const action of trigger.actions) {
@@ -503,7 +511,7 @@ class RuleFormContainer extends AsyncComponent<Props, State> {
       const [data, , resp] = await addOrUpdateRule(
         this.api,
         organization.slug,
-        params.projectId,
+        project.slug,
         {
           ...rule,
           ...model.getTransformedData(),
@@ -651,7 +659,6 @@ class RuleFormContainer extends AsyncComponent<Props, State> {
       organization,
       ruleId,
       rule,
-      params,
       onSubmitSuccess,
       project,
       userTeamIds,
@@ -726,7 +733,7 @@ class RuleFormContainer extends AsyncComponent<Props, State> {
         thresholdPeriod={thresholdPeriod}
         thresholdType={thresholdType}
         comparisonType={comparisonType}
-        currentProject={params.projectId}
+        currentProject={project.slug}
         organization={organization}
         ruleId={ruleId}
         availableActions={this.state.availableActions}
@@ -806,7 +813,7 @@ class RuleFormContainer extends AsyncComponent<Props, State> {
             <List symbol="colored-numeric">
               <RuleConditionsForm
                 api={this.api}
-                projectSlug={params.projectId}
+                projectSlug={project.slug}
                 organization={organization}
                 disabled={!hasAccess || !canEdit}
                 thresholdChart={wizardBuilderChart}

+ 10 - 5
static/app/views/alerts/issueRuleEditor/index.tsx

@@ -98,12 +98,14 @@ type RuleTaskResponse = {
   rule?: IssueAlertRule;
 };
 
+type RouteParams = {orgId: string; projectId?: string; ruleId?: string};
+
 type Props = {
   organization: Organization;
   project: Project;
   userTeamIds: string[];
   onChangeTitle?: (data: string) => void;
-} & RouteComponentProps<{orgId: string; projectId: string; ruleId?: string}, {}>;
+} & RouteComponentProps<RouteParams, {}>;
 
 type State = AsyncView['state'] & {
   configs: {
@@ -162,15 +164,18 @@ class IssueRuleEditor extends AsyncView<Props, State> {
   }
 
   getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
-    const {ruleId, projectId, orgId} = this.props.params;
+    const {
+      project,
+      params: {ruleId, orgId},
+    } = this.props;
 
     const endpoints = [
-      ['environments', `/projects/${orgId}/${projectId}/environments/`],
-      ['configs', `/projects/${orgId}/${projectId}/rules/configuration/`],
+      ['environments', `/projects/${orgId}/${project.slug}/environments/`],
+      ['configs', `/projects/${orgId}/${project.slug}/rules/configuration/`],
     ];
 
     if (ruleId) {
-      endpoints.push(['rule', `/projects/${orgId}/${projectId}/rules/${ruleId}/`]);
+      endpoints.push(['rule', `/projects/${orgId}/${project.slug}/rules/${ruleId}/`]);
     }
 
     return endpoints as [string, string][];

+ 4 - 2
static/app/views/alerts/rules/index.tsx

@@ -22,7 +22,7 @@ import withPageFilters from 'sentry/utils/withPageFilters';
 
 import FilterBar from '../filterBar';
 import AlertHeader from '../list/header';
-import {CombinedMetricIssueAlerts} from '../types';
+import {AlertRuleType, CombinedMetricIssueAlerts} from '../types';
 import {getTeamParams, isIssueAlert} from '../utils';
 
 import RuleListRow from './row';
@@ -231,7 +231,9 @@ class AlertRulesList extends AsyncComponent<Props, State & AsyncComponent['state
                     ruleList.map(rule => (
                       <RuleListRow
                         // Metric and issue alerts can have the same id
-                        key={`${isIssueAlert(rule) ? 'metric' : 'issue'}-${rule.id}`}
+                        key={`${
+                          isIssueAlert(rule) ? AlertRuleType.METRIC : AlertRuleType.ISSUE
+                        }-${rule.id}`}
                         projectsLoaded={initiallyLoaded}
                         projects={projects as Project[]}
                         rule={rule}

+ 5 - 0
static/app/views/alerts/types.tsx

@@ -4,6 +4,11 @@ import {IncidentRule} from 'sentry/views/alerts/incidentRules/types';
 
 type Data = [number, {count: number}[]][];
 
+export enum AlertRuleType {
+  METRIC = 'metric',
+  ISSUE = 'issue',
+}
+
 export type Incident = {
   alertRule: IncidentRule;
   dateClosed: string | null;

Some files were not shown because too many files changed in this diff