Browse Source

ref(ui) Fix more URLs for customer domains (#41715)

* Fix URL generation in monitors. This was a more invasive change as the
routes won't receive `orgId` in the future due to how organization slugs
are propagated to class components.
* Updated URL normalization for Organization general settings.
Mark Story 2 years ago
parent
commit
415bfebb09

+ 6 - 5
static/app/utils/withDomainRequired.spec.tsx

@@ -23,7 +23,8 @@ describe('normalizeUrl', function () {
     const location = TestStubs.location();
     const cases = [
       // input, expected
-      ['/settings/organization', '/settings/organization'],
+      ['/settings/acme/', '/settings/organization/'],
+      ['/settings/organization', '/settings/organization/'],
       ['/settings/sentry/members/', '/settings/members/'],
       ['/settings/sentry/members/3/', '/settings/members/3/'],
       ['/settings/sentry/teams/peeps/', '/settings/teams/peeps/'],
@@ -56,9 +57,9 @@ describe('normalizeUrl', function () {
 
   it('replaces pathname in objects', function () {
     const location = TestStubs.location();
-    result = normalizeUrl({pathname: '/settings/organization'}, location);
+    result = normalizeUrl({pathname: '/settings/acme/'}, location);
     // @ts-ignore
-    expect(result.pathname).toEqual('/settings/organization');
+    expect(result.pathname).toEqual('/settings/organization/');
 
     result = normalizeUrl({pathname: '/settings/sentry/members'}, location);
     // @ts-ignore
@@ -82,11 +83,11 @@ describe('normalizeUrl', function () {
   it('replaces pathname in function callback', function () {
     const location = TestStubs.location();
     function objectCallback(_loc: Location): LocationDescriptorObject {
-      return {pathname: '/settings/organization'};
+      return {pathname: '/settings/'};
     }
     result = normalizeUrl(objectCallback, location);
     // @ts-ignore
-    expect(result.pathname).toEqual('/settings/organization');
+    expect(result.pathname).toEqual('/settings/');
 
     function stringCallback(_loc: Location): LocationDescriptor {
       return '/organizations/a-long-slug/discover/';

+ 8 - 5
static/app/utils/withDomainRequired.tsx

@@ -6,8 +6,10 @@ import trimStart from 'lodash/trimStart';
 const NORMALIZE_PATTERNS: Array<[pattern: RegExp, replacement: string]> = [
   // /organizations/slug/section, but not /organizations/new
   [/\/?organizations\/(?!new)[^\/]+\/(.*)/, '/$1'],
-  // /settings/slug/section but not /settings/organization
-  // and not /settings/projects which is a new URL
+  // For /settings/:orgId/ -> /settings/organization/
+  [/\/?settings\/[^\/]+\/?$/, '/settings/organization/'],
+  // Move /settings/:orgId/:section -> /settings/:section
+  // but not /settings/organization or /settings/projects which is a new URL
   [/\/?settings\/(?!projects)(?!account)[^\/]+\/(.*)/, '/settings/$1'],
   [/\/?join-request\/[^\/]+\/?.*/, '/join-request/'],
   [/\/?onboarding\/[^\/]+\/(.*)/, '/onboarding/$1'],
@@ -46,13 +48,14 @@ export function normalizeUrl(path: LocationTarget, location?: Location): Locatio
     }
     return resolved;
   }
+
   if (!resolved.pathname) {
     return resolved;
   }
   for (const patternData of NORMALIZE_PATTERNS) {
-    resolved.pathname = resolved.pathname.replace(patternData[0], patternData[1]);
-    if (resolved !== path) {
-      return resolved;
+    const replacement = resolved.pathname.replace(patternData[0], patternData[1]);
+    if (replacement !== resolved.pathname) {
+      return {...resolved, pathname: replacement};
     }
   }
 

+ 16 - 3
static/app/views/monitors/create.tsx

@@ -1,8 +1,11 @@
 import {browserHistory, RouteComponentProps} from 'react-router';
 import styled from '@emotion/styled';
+import * as PropTypes from 'prop-types';
 
 import * as Layout from 'sentry/components/layouts/thirds';
 import {t} from 'sentry/locale';
+import SentryTypes from 'sentry/sentryTypes';
+import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import AsyncView from 'sentry/views/asyncView';
 
 import MonitorForm from './monitorForm';
@@ -11,12 +14,22 @@ import {Monitor} from './types';
 type Props = AsyncView['props'] & RouteComponentProps<{orgId: string}, {}>;
 
 export default class CreateMonitor extends AsyncView<Props, AsyncView['state']> {
+  static contextTypes = {
+    router: PropTypes.object,
+    organization: SentryTypes.Organization,
+  };
+
   getTitle() {
-    return `Monitors - ${this.props.params.orgId}`;
+    return `Monitors - ${this.orgSlug}`;
+  }
+
+  get orgSlug() {
+    return this.context.organization.slug;
   }
 
   onSubmitSuccess = (data: Monitor) => {
-    browserHistory.push(`/organizations/${this.props.params.orgId}/monitors/${data.id}/`);
+    const url = normalizeUrl(`/organizations/${this.orgSlug}/monitors/${data.id}/`);
+    browserHistory.push(url);
   };
 
   renderBody() {
@@ -34,7 +47,7 @@ export default class CreateMonitor extends AsyncView<Props, AsyncView['state']>
           </HelpText>
           <MonitorForm
             apiMethod="POST"
-            apiEndpoint={`/organizations/${this.props.params.orgId}/monitors/`}
+            apiEndpoint={`/organizations/${this.orgSlug}/monitors/`}
             onSubmitSuccess={this.onSubmitSuccess}
             submitLabel={t('Next Steps')}
           />

+ 15 - 8
static/app/views/monitors/details.tsx

@@ -1,12 +1,14 @@
 import {Fragment} from 'react';
 import {RouteComponentProps} from 'react-router';
 import styled from '@emotion/styled';
+import * as PropTypes from 'prop-types';
 
 import DatePageFilter from 'sentry/components/datePageFilter';
 import * as Layout from 'sentry/components/layouts/thirds';
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import {Panel, PanelHeader} from 'sentry/components/panels';
 import {t} from 'sentry/locale';
+import SentryTypes from 'sentry/sentryTypes';
 import space from 'sentry/styles/space';
 import AsyncView from 'sentry/views/asyncView';
 
@@ -25,6 +27,15 @@ type State = AsyncView['state'] & {
 };
 
 class MonitorDetails extends AsyncView<Props, State> {
+  static contextTypes = {
+    router: PropTypes.object,
+    organization: SentryTypes.Organization,
+  };
+
+  get orgSlug() {
+    return this.context.organization.slug;
+  }
+
   getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
     const {params, location} = this.props;
     return [['monitor', `/monitors/${params.monitorId}/`, {query: location.query}]];
@@ -32,9 +43,9 @@ class MonitorDetails extends AsyncView<Props, State> {
 
   getTitle() {
     if (this.state.monitor) {
-      return `${this.state.monitor.name} - Monitors - ${this.props.params.orgId}`;
+      return `${this.state.monitor.name} - Monitors - ${this.orgSlug}`;
     }
-    return `Monitors - ${this.props.params.orgId}`;
+    return `Monitors - ${this.orgSlug}`;
   }
 
   onUpdate = (data: Monitor) =>
@@ -49,11 +60,7 @@ class MonitorDetails extends AsyncView<Props, State> {
 
     return (
       <Fragment>
-        <MonitorHeader
-          monitor={monitor}
-          orgId={this.props.params.orgId}
-          onUpdate={this.onUpdate}
-        />
+        <MonitorHeader monitor={monitor} orgId={this.orgSlug} onUpdate={this.onUpdate} />
         <Layout.Body>
           <Layout.Main fullWidth>
             {!monitor.lastCheckIn && <MonitorOnboarding monitor={monitor} />}
@@ -64,7 +71,7 @@ class MonitorDetails extends AsyncView<Props, State> {
 
             <MonitorStats monitor={monitor} />
 
-            <MonitorIssues monitor={monitor} orgId={this.props.params.orgId} />
+            <MonitorIssues monitor={monitor} orgId={this.orgSlug} />
 
             <Panel>
               <PanelHeader>{t('Recent Check-ins')}</PanelHeader>

+ 14 - 3
static/app/views/monitors/edit.tsx

@@ -1,7 +1,9 @@
 import {browserHistory, RouteComponentProps} from 'react-router';
+import * as PropTypes from 'prop-types';
 
 import * as Layout from 'sentry/components/layouts/thirds';
 import {t} from 'sentry/locale';
+import SentryTypes from 'sentry/sentryTypes';
 import AsyncView from 'sentry/views/asyncView';
 
 import MonitorForm from './monitorForm';
@@ -15,6 +17,15 @@ type State = AsyncView['state'] & {
 };
 
 export default class EditMonitor extends AsyncView<Props, State> {
+  static contextTypes = {
+    router: PropTypes.object,
+    organization: SentryTypes.Organization,
+  };
+
+  get orgSlug() {
+    return this.context.organization.slug;
+  }
+
   getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
     const {params} = this.props;
     return [['monitor', `/monitors/${params.monitorId}/`]];
@@ -24,13 +35,13 @@ export default class EditMonitor extends AsyncView<Props, State> {
     this.setState(state => ({monitor: {...state.monitor, ...data}}));
 
   onSubmitSuccess = (data: Monitor) =>
-    browserHistory.push(`/organizations/${this.props.params.orgId}/monitors/${data.id}/`);
+    browserHistory.push(`/organizations/${this.orgSlug}/monitors/${data.id}/`);
 
   getTitle() {
     if (this.state.monitor) {
-      return `${this.state.monitor.name} - Monitors - ${this.props.params.orgId}`;
+      return `${this.state.monitor.name} - Monitors - ${this.orgSlug}`;
     }
-    return `Monitors - ${this.props.params.orgId}`;
+    return `Monitors - ${this.orgSlug}`;
   }
 
   renderBody() {

+ 14 - 3
static/app/views/monitors/monitors.tsx

@@ -2,6 +2,7 @@ import {Fragment} from 'react';
 // eslint-disable-next-line no-restricted-imports
 import {withRouter, WithRouterProps} from 'react-router';
 import styled from '@emotion/styled';
+import * as PropTypes from 'prop-types';
 import * as qs from 'query-string';
 
 import onboardingImg from 'sentry-images/spot/onboarding-preview.svg';
@@ -20,6 +21,7 @@ import ProjectPageFilter from 'sentry/components/projectPageFilter';
 import SearchBar from 'sentry/components/searchBar';
 import TimeSince from 'sentry/components/timeSince';
 import {t} from 'sentry/locale';
+import SentryTypes from 'sentry/sentryTypes';
 import space from 'sentry/styles/space';
 import {Organization} from 'sentry/types';
 import {decodeScalar} from 'sentry/utils/queryString';
@@ -68,12 +70,21 @@ function NewMonitorButton(props: ButtonProps) {
 }
 
 class Monitors extends AsyncView<Props, State> {
+  static contextTypes = {
+    router: PropTypes.object,
+    organization: SentryTypes.Organization,
+  };
+
+  get orgSlug() {
+    return this.context.organization.slug;
+  }
+
   getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
-    const {params, location} = this.props;
+    const {location} = this.props;
     return [
       [
         'monitorList',
-        `/organizations/${params.orgId}/monitors/`,
+        `/organizations/${this.orgSlug}/monitors/`,
         {
           query: location.query,
         },
@@ -82,7 +93,7 @@ class Monitors extends AsyncView<Props, State> {
   }
 
   getTitle() {
-    return `Monitors - ${this.props.params.orgId}`;
+    return `Monitors - ${this.orgSlug}`;
   }
 
   componentDidMount() {