Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions static/app/components/modals/redirectToProject.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Fragment, useEffect, useState} from 'react';
import {useMatches} from 'react-router-dom';

import {LinkButton} from '@sentry/scraps/button';
import {Flex} from '@sentry/scraps/layout';
Expand All @@ -17,13 +18,15 @@ interface Props extends ModalRenderProps {
}

function RedirectToProjectModal({slug, Header, Body}: Props) {
const matches = useMatches();
const routes = useRoutes();
const params = useParams();
const location = useLocation();

const [timer, setTimer] = useState(5);

const newPath = recreateRoute('', {
matches,
routes,
location,
params: {...params, projectId: slug},
Expand Down
143 changes: 103 additions & 40 deletions static/app/utils/recreateRoute.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,66 @@
import type {UIMatch} from 'react-router-dom';
import {LocationFixture} from 'sentry-fixture/locationFixture';

import type {PlainRoute} from 'sentry/types/legacyReactRouter';
import {recreateRoute} from 'sentry/utils/recreateRoute';

jest.unmock('sentry/utils/recreateRoute');

const routes = [
{path: '/', childRoutes: []},
{childRoutes: []},
{path: '/settings/', name: 'Settings'},
{name: 'Organizations', path: ':orgId/', childRoutes: []},
{childRoutes: []},
{path: 'api-keys/', name: 'API Key'},
function matchesToRoutes(ms: UIMatch[]): PlainRoute[] {
return ms.map(m => ({...(m.handle as any)}));
}

function makeMatch(
pathname: string,
matchParams: Record<string, string>,
handle: Record<string, unknown>
): UIMatch {
return {id: pathname, pathname, params: matchParams, data: undefined, handle};
}

const matches: UIMatch[] = [
makeMatch('/', {}, {path: '/', childRoutes: []}),
makeMatch('/', {}, {childRoutes: []}),
makeMatch('/settings/', {}, {path: '/settings/', name: 'Settings'}),
makeMatch(
'/settings/org-slug/',
{orgId: 'org-slug'},
{name: 'Organizations', path: ':orgId/', childRoutes: []}
),
makeMatch('/settings/org-slug/', {orgId: 'org-slug'}, {childRoutes: []}),
makeMatch(
'/settings/org-slug/api-keys/',
{orgId: 'org-slug'},
{path: 'api-keys/', name: 'API Key'}
),
];
const routes = matchesToRoutes(matches);

const projectRoutes = [
{path: '/', childRoutes: []},
{childRoutes: []},
{path: '/settings/', name: 'Settings', indexRoute: {}, childRoutes: []},
{name: 'Organizations', path: ':orgId/', childRoutes: []},
{name: 'Projects', path: ':projectId', childRoutes: []},
{name: 'Alerts', path: 'alerts/'},
const projectMatches: UIMatch[] = [
makeMatch('/', {}, {path: '/', childRoutes: []}),
makeMatch('/', {}, {childRoutes: []}),
makeMatch(
'/settings/',
{},
{path: '/settings/', name: 'Settings', indexRoute: {}, childRoutes: []}
),
makeMatch(
'/settings/org-slug/',
{orgId: 'org-slug'},
{name: 'Organizations', path: ':orgId/', childRoutes: []}
),
makeMatch(
'/settings/org-slug/project-slug',
{orgId: 'org-slug', projectId: 'project-slug'},
{name: 'Projects', path: ':projectId', childRoutes: []}
),
makeMatch(
'/settings/org-slug/project-slug/alerts/',
{orgId: 'org-slug', projectId: 'project-slug'},
{name: 'Alerts', path: 'alerts/'}
),
];
const projectRoutes = matchesToRoutes(projectMatches);

const params = {
orgId: 'org-slug',
Expand All @@ -34,51 +74,74 @@ const location = {

describe('recreateRoute', () => {
it('returns correct path to a route object', () => {
expect(recreateRoute(routes[0]!, {routes, params})).toBe('/');
expect(recreateRoute(routes[1]!, {routes, params})).toBe('/');
expect(recreateRoute(routes[2]!, {routes, params})).toBe('/settings/');
expect(recreateRoute(routes[3]!, {routes, params})).toBe('/settings/org-slug/');
expect(recreateRoute(routes[4]!, {routes, params})).toBe('/settings/org-slug/');
expect(recreateRoute(routes[5]!, {routes, params})).toBe(
expect(recreateRoute(routes[0]!, {routes, matches, params})).toBe('/');
expect(recreateRoute(routes[1]!, {routes, matches, params})).toBe('/');
expect(recreateRoute(routes[2]!, {routes, matches, params})).toBe('/settings/');
expect(recreateRoute(routes[3]!, {routes, matches, params})).toBe(
'/settings/org-slug/'
);
expect(recreateRoute(routes[4]!, {routes, matches, params})).toBe(
'/settings/org-slug/'
);
expect(recreateRoute(routes[5]!, {routes, matches, params})).toBe(
'/settings/org-slug/api-keys/'
);

expect(
recreateRoute(projectRoutes[5]!, {routes: projectRoutes, location, params})
recreateRoute(projectRoutes[5]!, {
routes: projectRoutes,
matches: projectMatches,
location,
params,
})
).toBe('/settings/org-slug/project-slug/alerts/');
});

it('has correct path with route object with many roots (starts with "/")', () => {
const r = [
{path: '/', childRoutes: []},
{childRoutes: []},
{path: '/foo/', childRoutes: []},
{childRoutes: []},
{path: 'bar', childRoutes: []},
{path: '/settings/', name: 'Settings'},
{name: 'Organizations', path: ':orgId/', childRoutes: []},
{childRoutes: []},
{path: 'api-keys/', name: 'API Key'},
const m: UIMatch[] = [
makeMatch('/', {}, {path: '/', childRoutes: []}),
makeMatch('/', {}, {childRoutes: []}),
makeMatch('/foo/', {}, {path: '/foo/', childRoutes: []}),
makeMatch('/foo/', {}, {childRoutes: []}),
makeMatch('/foo/bar', {}, {path: 'bar', childRoutes: []}),
makeMatch('/settings/', {}, {path: '/settings/', name: 'Settings'}),
makeMatch(
'/settings/org-slug/',
{orgId: 'org-slug'},
{name: 'Organizations', path: ':orgId/', childRoutes: []}
),
makeMatch('/settings/org-slug/', {orgId: 'org-slug'}, {childRoutes: []}),
makeMatch(
'/settings/org-slug/api-keys/',
{orgId: 'org-slug'},
{path: 'api-keys/', name: 'API Key'}
),
];
const r = matchesToRoutes(m);

expect(recreateRoute(r[4]!, {routes: r, params})).toBe('/foo/bar/');
expect(recreateRoute(r[4]!, {routes: r, matches: m, params})).toBe('/foo/bar/');
});

it('returns correct path to a string (at the end of the routes)', () => {
expect(recreateRoute('test/', {routes, location, params})).toBe(
expect(recreateRoute('test/', {routes, matches, location, params})).toBe(
'/settings/org-slug/api-keys/test/'
);
});

it('returns correct path to a string after the 2nd to last route', () => {
expect(recreateRoute('test/', {routes, location, params, stepBack: -2})).toBe(
'/settings/org-slug/test/'
);
expect(
recreateRoute('test/', {routes, matches, location, params, stepBack: -2})
).toBe('/settings/org-slug/test/');
});

it('switches to new org but keeps current route', () => {
expect(
recreateRoute(routes[5]!, {routes, location, params: {orgId: 'new-org'}})
recreateRoute(routes[5]!, {
routes,
matches,
location,
params: {orgId: 'new-org'},
})
).toBe('/settings/new-org/api-keys/');
});

Expand All @@ -88,8 +151,8 @@ describe('recreateRoute', () => {
search: '?key1=foo&key2=bar',
};

expect(recreateRoute(routes[5]!, {routes, params, location: withSearch})).toBe(
'/settings/org-slug/api-keys/?key1=foo&key2=bar'
);
expect(
recreateRoute(routes[5]!, {routes, matches, params, location: withSearch})
).toBe('/settings/org-slug/api-keys/?key1=foo&key2=bar');
});
});
3 changes: 3 additions & 0 deletions static/app/utils/recreateRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type {UIMatch} from 'react-router-dom';
import type {Location} from 'history';

import type {PlainRoute} from 'sentry/types/legacyReactRouter';
import {replaceRouterParams} from 'sentry/utils/replaceRouterParams';

type Options = {
matches: UIMatch[];

// parameters to replace any route string parameters (e.g. if route is `:orgId`,
// params should have `{orgId: slug}`
params: Record<string, string | undefined>;
Expand Down
5 changes: 3 additions & 2 deletions static/app/utils/withDomainRedirect.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {generatePath} from 'react-router-dom';
import {generatePath, useMatches} from 'react-router-dom';
import trim from 'lodash/trim';
import trimEnd from 'lodash/trimEnd';
import trimStart from 'lodash/trimStart';
Expand Down Expand Up @@ -39,6 +39,7 @@ export function withDomainRedirect(WrappedComponent: React.ComponentType<any>) {
const {sentryUrl} = links;
const currentOrganization = useOrganization({allowNull: true});
const params = useParams();
const matches = useMatches();
const routes = useRoutes();

if (customerDomain) {
Expand All @@ -63,7 +64,7 @@ export function withDomainRedirect(WrappedComponent: React.ComponentType<any>) {
Object.keys(params).forEach(param => {
newParams[param] = `:${param}`;
});
const fullRoute = recreateRoute('', {routes, params: newParams});
const fullRoute = recreateRoute('', {matches, routes, params: newParams});
const orglessSlugRoute = normalizeUrl(fullRoute, {forceCustomerDomain: true});

if (orglessSlugRoute === fullRoute) {
Expand Down
3 changes: 3 additions & 0 deletions static/app/views/alerts/create.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {useEffect, Fragment, useMemo, useRef} from 'react';
import {useMatches} from 'react-router-dom';

import {Stack} from '@sentry/scraps/layout';

Expand Down Expand Up @@ -51,6 +52,7 @@ export default function Create() {
const location = useLocation();
const params = useParams<RouteParams>();
const navigate = useNavigate();
const matches = useMatches();
const routes = useRoutes();
const {project, members} = useAlertBuilderOutlet();
const hasMetricAlerts = organization.features.includes('incidents');
Expand Down Expand Up @@ -202,6 +204,7 @@ export default function Create() {
/>
) : !hasMetricAlerts || alertType === AlertRuleType.ISSUE ? (
<IssueRuleEditor
matches={matches}
location={location}
params={params}
router={router}
Expand Down
3 changes: 3 additions & 0 deletions static/app/views/alerts/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {useMemo, useState, Fragment} from 'react';
import {useMatches} from 'react-router-dom';

import {Stack} from '@sentry/scraps/layout';

Expand Down Expand Up @@ -34,6 +35,7 @@ export default function ProjectAlertsEditor() {
const location = useLocation();
const params = useParams<RouteParams>();
const navigate = useNavigate();
const matches = useMatches();
const routes = useRoutes();
const {project, members} = useAlertBuilderOutlet();

Expand Down Expand Up @@ -106,6 +108,7 @@ export default function ProjectAlertsEditor() {
<Fragment>
{alertType === CombinedAlertType.ISSUE && (
<IssueEditor
matches={matches}
location={location}
params={params}
router={router}
Expand Down
77 changes: 49 additions & 28 deletions static/app/views/alerts/rules/issue/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {UIMatch} from 'react-router-dom';
import moment from 'moment-timezone';
import {EnvironmentsFixture} from 'sentry-fixture/environments';
import {GitHubIntegrationProviderFixture} from 'sentry-fixture/githubIntegrationProvider';
Expand Down Expand Up @@ -49,35 +50,54 @@ jest.mock('sentry/utils/analytics', () => ({
trackAnalytics: jest.fn(),
}));

const projectAlertRuleDetailsRoutes: PlainRoute[] = [
{
path: '/',
},
{
path: '/settings/',
indexRoute: {},
},
{
path: ':orgId/',
},
{
path: 'projects/:projectId/',
},
{},
{
indexRoute: {},
},
{
path: 'alerts/',
indexRoute: {},
},
{
path: 'rules/',
indexRoute: {},
childRoutes: [{path: 'new/'}, {path: ':ruleId/'}],
},
{path: ':ruleId/'},
function makeMatch(
pathname: string,
matchParams: Record<string, string>,
handle: Record<string, unknown>
): UIMatch {
return {id: pathname, pathname, params: matchParams, data: undefined, handle};
}

function matchesToRoutes(ms: UIMatch[]): PlainRoute[] {
return ms.map(m => ({...(m.handle as any)}));
}

const matches: UIMatch[] = [
makeMatch('/', {}, {path: '/'}),
makeMatch('/settings/', {}, {path: '/settings/', indexRoute: {}}),
makeMatch('/settings/org-slug/', {orgId: 'org-slug'}, {path: ':orgId/'}),
makeMatch(
'/settings/org-slug/projects/project-slug/',
{orgId: 'org-slug', projectId: 'project-slug'},
{path: 'projects/:projectId/'}
),
makeMatch(
'/settings/org-slug/projects/project-slug/',
{orgId: 'org-slug', projectId: 'project-slug'},
{}
),
makeMatch(
'/settings/org-slug/projects/project-slug/',
{orgId: 'org-slug', projectId: 'project-slug'},
{indexRoute: {}}
),
makeMatch(
'/settings/org-slug/projects/project-slug/alerts/',
{orgId: 'org-slug', projectId: 'project-slug'},
{path: 'alerts/', indexRoute: {}}
),
makeMatch(
'/settings/org-slug/projects/project-slug/alerts/rules/',
{orgId: 'org-slug', projectId: 'project-slug'},
{path: 'rules/', indexRoute: {}, childRoutes: [{path: 'new/'}, {path: ':ruleId/'}]}
),
makeMatch(
'/settings/org-slug/projects/project-slug/alerts/rules/1/',
{orgId: 'org-slug', projectId: 'project-slug', ruleId: '1'},
{path: ':ruleId/'}
),
];
const projectAlertRuleDetailsRoutes: PlainRoute[] = matchesToRoutes(matches);

const createWrapper = (props: Parameters<typeof initializeOrg>[0] = {}) => {
const {organization, project} = initializeOrg(props);
Expand All @@ -103,6 +123,7 @@ const createWrapper = (props: Parameters<typeof initializeOrg>[0] = {}) => {
const onChangeTitleMock = jest.fn();
const wrapper = render(
<IssueRuleEditor
matches={matches}
route={RouteComponentPropsFixture().route}
routeParams={RouteComponentPropsFixture().routeParams}
params={params}
Expand Down
Loading
Loading