Browse Source

fix(uptime): Correct API used to create alerts (#78168)

When creating a new uptime alert the API endpoint used is a project
endpoint, which includes the project slug. When creating a uptime alert
you can specify the project as part of the form. We did not take this
into account correctly and the form would always POST to the project
endpoint of the project that was originally selected when landing on the
uptime form.

The fix here is a little complicated, since we need to reactively update
the form API endpoint when the project slug changes, we can do this by
setting up a mobx observer on the projectSlug value and updating the
form options accordingly.

Fixes GH-76622
Evan Purkhiser 5 months ago
parent
commit
a310ac9dfa

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

@@ -143,18 +143,7 @@ function Create(props: Props) {
         ) : (
           <Fragment>
             {alertType === AlertRuleType.UPTIME ? (
-              <UptimeAlertForm
-                apiMethod="POST"
-                apiUrl={`/projects/${organization.slug}/${project.slug}/uptime/`}
-                project={project}
-                onSubmitSuccess={response => {
-                  router.push(
-                    normalizeUrl(
-                      `/organizations/${organization.slug}/alerts/rules/uptime/${project.slug}/${response.id}/details`
-                    )
-                  );
-                }}
-              />
+              <UptimeAlertForm {...props} />
             ) : !hasMetricAlerts || alertType === AlertRuleType.ISSUE ? (
               <IssueRuleEditor
                 {...props}

+ 1 - 9
static/app/views/alerts/rules/uptime/edit.tsx

@@ -77,18 +77,10 @@ export function UptimeRulesEdit({params, onChangeTitle, organization, project}:
 
   return (
     <UptimeAlertForm
-      apiMethod="PUT"
-      apiUrl={apiUrl}
+      organization={organization}
       project={project}
       rule={rule}
       handleDelete={handleDelete}
-      onSubmitSuccess={() => {
-        navigate(
-          normalizeUrl(
-            `/organizations/${organization.slug}/alerts/rules/uptime/${params.projectId}/${params.ruleId}/details`
-          )
-        );
-      }}
     />
   );
 }

+ 8 - 32
static/app/views/alerts/rules/uptime/uptimeAlertForm.spec.tsx

@@ -30,15 +30,9 @@ describe('Uptime Alert Form', function () {
     const {organization, project} = initializeOrg();
     OrganizationStore.onUpdate(organization);
 
-    render(
-      <UptimeAlertForm
-        apiMethod="POST"
-        apiUrl={'/update-rule'}
-        project={project}
-        onSubmitSuccess={() => {}}
-      />,
-      {organization}
-    );
+    render(<UptimeAlertForm organization={organization} project={project} />, {
+      organization,
+    });
     await screen.findByText('Configure Request');
 
     await userEvent.clear(input('URL'));
@@ -60,7 +54,7 @@ describe('Uptime Alert Form', function () {
     await selectEvent.select(screen.getByRole('textbox', {name: 'Owner'}), 'Foo Bar');
 
     const updateMock = MockApiClient.addMockResponse({
-      url: '/update-rule',
+      url: `/projects/${organization.slug}/${project.slug}/uptime/`,
       method: 'POST',
     });
 
@@ -99,13 +93,7 @@ describe('Uptime Alert Form', function () {
       owner: ActorFixture(),
     });
     render(
-      <UptimeAlertForm
-        apiMethod="PUT"
-        apiUrl={''}
-        project={project}
-        onSubmitSuccess={() => {}}
-        rule={rule}
-      />,
+      <UptimeAlertForm organization={organization} project={project} rule={rule} />,
       {organization}
     );
     await screen.findByText('Configure Request');
@@ -132,13 +120,7 @@ describe('Uptime Alert Form', function () {
       owner: ActorFixture(),
     });
     render(
-      <UptimeAlertForm
-        apiMethod="PUT"
-        apiUrl={'/update-rule'}
-        project={project}
-        onSubmitSuccess={() => {}}
-        rule={rule}
-      />,
+      <UptimeAlertForm organization={organization} project={project} rule={rule} />,
       {organization}
     );
     await screen.findByText('Configure Request');
@@ -165,7 +147,7 @@ describe('Uptime Alert Form', function () {
     await selectEvent.select(screen.getByRole('textbox', {name: 'Owner'}), 'Foo Bar');
 
     const updateMock = MockApiClient.addMockResponse({
-      url: '/update-rule',
+      url: `/projects/${organization.slug}/${project.slug}/uptime/${rule.id}/`,
       method: 'PUT',
     });
 
@@ -199,13 +181,7 @@ describe('Uptime Alert Form', function () {
       owner: ActorFixture(),
     });
     render(
-      <UptimeAlertForm
-        apiMethod="PUT"
-        apiUrl={'/update-rule'}
-        project={project}
-        onSubmitSuccess={() => {}}
-        rule={rule}
-      />,
+      <UptimeAlertForm organization={organization} project={project} rule={rule} />,
       {organization}
     );
     await screen.findByText('Configure Request');

+ 38 - 23
static/app/views/alerts/rules/uptime/uptimeAlertForm.tsx

@@ -1,8 +1,8 @@
-import {useState} from 'react';
+import {useEffect, useState} from 'react';
 import styled from '@emotion/styled';
+import {observe} from 'mobx';
 import {Observer} from 'mobx-react';
 
-import type {APIRequestMethod} from 'sentry/api';
 import {Button} from 'sentry/components/button';
 import Confirm from 'sentry/components/confirm';
 import FieldWrapper from 'sentry/components/forms/fieldGroup/fieldWrapper';
@@ -12,14 +12,18 @@ import SentryMemberTeamSelectorField from 'sentry/components/forms/fields/sentry
 import SentryProjectSelectorField from 'sentry/components/forms/fields/sentryProjectSelectorField';
 import TextareaField from 'sentry/components/forms/fields/textareaField';
 import TextField from 'sentry/components/forms/fields/textField';
-import Form, {type FormProps} from 'sentry/components/forms/form';
+import Form from 'sentry/components/forms/form';
 import FormModel from 'sentry/components/forms/model';
 import List from 'sentry/components/list';
 import ListItem from 'sentry/components/list/listItem';
 import Panel from 'sentry/components/panels/panel';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
+import type {Organization} from 'sentry/types/organization';
 import type {Project} from 'sentry/types/project';
+import normalizeUrl from 'sentry/utils/url/normalizeUrl';
+import {useNavigate} from 'sentry/utils/useNavigate';
+import useOrganization from 'sentry/utils/useOrganization';
 import useProjects from 'sentry/utils/useProjects';
 import type {UptimeRule} from 'sentry/views/alerts/rules/uptime/types';
 
@@ -27,9 +31,7 @@ import {HTTPSnippet} from './httpSnippet';
 import {UptimeHeadersField} from './uptimeHeadersField';
 
 interface Props {
-  apiMethod: APIRequestMethod;
-  apiUrl: string;
-  onSubmitSuccess: FormProps['onSubmitSuccess'];
+  organization: Organization;
   project: Project;
   handleDelete?: () => void;
   rule?: UptimeRule;
@@ -50,36 +52,49 @@ function getFormDataFromRule(rule: UptimeRule) {
   };
 }
 
-export function UptimeAlertForm({
-  apiMethod,
-  apiUrl,
-  project,
-  onSubmitSuccess,
-  handleDelete,
-  rule,
-}: Props) {
+export function UptimeAlertForm({project, handleDelete, rule}: Props) {
+  const navigate = useNavigate();
+  const organization = useOrganization();
   const {projects} = useProjects();
 
   const initialData = rule
     ? getFormDataFromRule(rule)
     : {projectSlug: project.slug, method: 'GET', headers: []};
 
-  const submitLabel = {
-    POST: t('Create Rule'),
-    PUT: t('Save Rule'),
-  };
-
   const [formModel] = useState(() => new FormModel());
 
+  // XXX(epurkhiser): The forms API endpoint is derived from the selcted
+  // project. We don't have an easy way to interpolate this into the <Form />
+  // components `apiEndpoint` prop, so instead we setup a mobx observer on
+  // value of the project slug and use that to update the endpoint of the form
+  // model
+  useEffect(
+    () =>
+      observe(formModel, () => {
+        const projectSlug = formModel.getValue('projectSlug');
+        const apiEndpoint = rule
+          ? `/projects/${organization.slug}/${projectSlug}/uptime/${rule.id}/`
+          : `/projects/${organization.slug}/${projectSlug}/uptime/`;
+
+        function onSubmitSuccess(response: any) {
+          navigate(
+            normalizeUrl(
+              `/organizations/${organization.slug}/alerts/rules/uptime/${projectSlug}/${response.id}/details/`
+            )
+          );
+        }
+        formModel.setFormOptions({apiEndpoint, onSubmitSuccess});
+      }),
+    [formModel, navigate, organization.slug, rule]
+  );
+
   return (
     <Form
       model={formModel}
-      apiMethod={apiMethod}
-      apiEndpoint={apiUrl}
+      apiMethod={rule ? 'PUT' : 'POST'}
       saveOnBlur={false}
       initialData={initialData}
-      onSubmitSuccess={onSubmitSuccess}
-      submitLabel={submitLabel[apiMethod]}
+      submitLabel={rule ? t('Save Rule') : t('Create Rule')}
       extraButton={
         rule && handleDelete ? (
           <Confirm