diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2022-06-13 11:39:21 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-06-28 20:02:53 +0000 |
commit | 54732569670fc345367062d5b20fcca83d9f7692 (patch) | |
tree | 8a3d86a9b76fbc056b74ac68ff8b38db9cee2cb1 /server/sonar-web/src/main/js/app | |
parent | 26675093303e38f1973f3ee9da5750aeeb2a5a5f (diff) | |
download | sonarqube-54732569670fc345367062d5b20fcca83d9f7692.tar.gz sonarqube-54732569670fc345367062d5b20fcca83d9f7692.zip |
SONAR-16045 Migrate react-router to 6.3.0
Co-authored-by: Jeremy Davis <jeremy.davis@sonarsource.com>
Co-authored-by: Guillaume Péoc'h <guillaume.peoch@sonarsource.com>
Diffstat (limited to 'server/sonar-web/src/main/js/app')
88 files changed, 1376 insertions, 2114 deletions
diff --git a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx index f5ef383db99..de4a84124e4 100644 --- a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx @@ -19,11 +19,13 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; +import { Outlet } from 'react-router-dom'; import { getSettingsNavigation } from '../../api/nav'; import { getPendingPlugins } from '../../api/plugins'; import { getSystemStatus, waitSystemUPStatus } from '../../api/system'; import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; import { translate } from '../../helpers/l10n'; +import { AdminPagesContext } from '../../types/admin'; import { AppState } from '../../types/appstate'; import { PendingPluginResult } from '../../types/plugins'; import { Extension, SysStatus } from '../../types/types'; @@ -33,7 +35,6 @@ import SettingsNav from './nav/settings/SettingsNav'; export interface AdminContainerProps { appState: AppState; - children: React.ReactElement; } interface State { @@ -120,6 +121,8 @@ export class AdminContainer extends React.PureComponent<AdminContainerProps, Sta const { pendingPlugins, systemStatus } = this.state; const defaultTitle = translate('layout.settings'); + const adminPagesContext: AdminPagesContext = { adminPages }; + return ( <div> <Helmet defaultTitle={defaultTitle} defer={false} titleTemplate={`%s - ${defaultTitle}`} /> @@ -137,9 +140,7 @@ export class AdminContainer extends React.PureComponent<AdminContainerProps, Sta pendingPlugins, systemStatus }}> - {React.cloneElement(this.props.children, { - adminPages - })} + <Outlet context={adminPagesContext} /> </AdminContext.Provider> </div> ); diff --git a/server/sonar-web/src/main/js/app/components/App.tsx b/server/sonar-web/src/main/js/app/components/App.tsx index e7dcd7d0654..ca2b0ab0daf 100644 --- a/server/sonar-web/src/main/js/app/components/App.tsx +++ b/server/sonar-web/src/main/js/app/components/App.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { Outlet } from 'react-router-dom'; import { lazyLoadComponent } from '../../components/lazyLoadComponent'; import { AppState } from '../../types/appstate'; import { GlobalSettingKeys } from '../../types/settings'; @@ -91,7 +92,7 @@ export class App extends React.PureComponent<Props> { return ( <> <PageTracker>{this.renderPreconnectLink()}</PageTracker> - {this.props.children} + <Outlet /> <KeyboardShortcutsModal /> </> ); diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index a3d571fcb2e..4d6d2f3fac0 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -19,6 +19,7 @@ */ import { differenceBy } from 'lodash'; import * as React from 'react'; +import { Outlet } from 'react-router-dom'; import { getProjectAlmBinding, validateProjectAlmBinding } from '../../api/alm-settings'; import { getBranches, getPullRequests } from '../../api/branches'; import { getAnalysisStatus, getTasksForComponent } from '../../api/ce'; @@ -46,16 +47,15 @@ import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; import withAppStateContext from './app-state/withAppStateContext'; import withBranchStatusActions from './branch-status/withBranchStatusActions'; import ComponentContainerNotFound from './ComponentContainerNotFound'; -import { ComponentContext } from './ComponentContext'; +import { ComponentContext } from './componentContext/ComponentContext'; import PageUnavailableDueToIndexation from './indexation/PageUnavailableDueToIndexation'; import ComponentNav from './nav/component/ComponentNav'; interface Props { appState: AppState; - children: React.ReactElement; - location: Pick<Location, 'query' | 'pathname'>; + location: Location; updateBranchStatus: (branchLike: BranchLike, component: string, status: Status) => void; - router: Pick<Router, 'replace'>; + router: Router; } interface State { @@ -448,8 +448,8 @@ export class ComponentContainer extends React.PureComponent<Props, State> { <i className="spinner" /> </div> ) : ( - <ComponentContext.Provider value={{ branchLike, component }}> - {React.cloneElement(this.props.children, { + <ComponentContext.Provider + value={{ branchLike, branchLikes, component, @@ -458,7 +458,8 @@ export class ComponentContainer extends React.PureComponent<Props, State> { onBranchesChange: this.handleBranchesChange, onComponentChange: this.handleComponentChange, projectBinding - })} + }}> + <Outlet /> </ComponentContext.Provider> )} </div> diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx index e10f29c2411..c6fb0f8c382 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { translate } from '../../helpers/l10n'; export default function ComponentContainerNotFound() { diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index 6bc280ca84f..f9f4cecc211 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { Outlet, useLocation } from 'react-router-dom'; import A11yProvider from '../../components/a11y/A11yProvider'; import A11ySkipLinks from '../../components/a11y/A11ySkipLinks'; import SuggestionsProvider from '../../components/embed-docs-modal/SuggestionsProvider'; @@ -33,15 +34,10 @@ import PromotionNotification from './promotion-notification/PromotionNotificatio import StartupModal from './StartupModal'; import UpdateNotification from './update-notification/UpdateNotification'; -export interface Props { - children: React.ReactNode; - footer?: React.ReactNode; - location: { pathname: string }; -} - -export default function GlobalContainer(props: Props) { +export default function GlobalContainer() { // it is important to pass `location` down to `GlobalNav` to trigger render on url change - const { footer = <GlobalFooter /> } = props; + const location = useLocation(); + return ( <SuggestionsProvider> <A11yProvider> @@ -55,10 +51,10 @@ export default function GlobalContainer(props: Props) { <IndexationContextProvider> <LanguagesContextProvider> <MetricsContextProvider> - <GlobalNav location={props.location} /> + <GlobalNav location={location} /> <IndexationNotification /> <UpdateNotification dismissable={true} /> - {props.children} + <Outlet /> </MetricsContextProvider> </LanguagesContextProvider> </IndexationContextProvider> @@ -67,7 +63,7 @@ export default function GlobalContainer(props: Props) { </div> <PromotionNotification /> </div> - {footer} + <GlobalFooter /> </div> </StartupModal> </A11yProvider> diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx index 9da659e6004..70d929392fd 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import InstanceMessage from '../../components/common/InstanceMessage'; import { Alert } from '../../components/ui/Alert'; import { getEdition } from '../../helpers/editions'; diff --git a/server/sonar-web/src/main/js/app/components/Landing.tsx b/server/sonar-web/src/main/js/app/components/Landing.tsx index 13109175290..e2e2db052e5 100644 --- a/server/sonar-web/src/main/js/app/components/Landing.tsx +++ b/server/sonar-web/src/main/js/app/components/Landing.tsx @@ -18,30 +18,24 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Router, withRouter } from '../../components/hoc/withRouter'; +import { Navigate, To } from 'react-router-dom'; import { getHomePageUrl } from '../../helpers/urls'; import { CurrentUser, isLoggedIn } from '../../types/users'; import withCurrentUserContext from './current-user/withCurrentUserContext'; export interface LandingProps { currentUser: CurrentUser; - router: Router; } -export class Landing extends React.PureComponent<LandingProps> { - componentDidMount() { - const { currentUser } = this.props; - if (isLoggedIn(currentUser) && currentUser.homepage) { - const homepage = getHomePageUrl(currentUser.homepage); - this.props.router.replace(homepage); - } else { - this.props.router.replace('/projects'); - } +export function Landing({ currentUser }: LandingProps) { + let redirectUrl: To; + if (isLoggedIn(currentUser) && currentUser.homepage) { + redirectUrl = getHomePageUrl(currentUser.homepage); + } else { + redirectUrl = '/projects'; } - render() { - return null; - } + return <Navigate to={redirectUrl} replace={true} />; } -export default withRouter(withCurrentUserContext(Landing)); +export default withCurrentUserContext(Landing); diff --git a/server/sonar-web/src/main/js/app/components/MigrationContainer.tsx b/server/sonar-web/src/main/js/app/components/MigrationContainer.tsx index 6cdf95d5477..c11a011ad70 100644 --- a/server/sonar-web/src/main/js/app/components/MigrationContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/MigrationContainer.tsx @@ -18,25 +18,23 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { WithRouterProps } from 'react-router'; +import { Navigate, Outlet, useLocation } from 'react-router-dom'; import { getSystemStatus } from '../../helpers/system'; -export default class MigrationContainer extends React.PureComponent<WithRouterProps> { - componentDidMount() { - if (getSystemStatus() !== 'UP') { - this.props.router.push({ - pathname: '/maintenance', - query: { - return_to: window.location.pathname + window.location.search + window.location.hash - } - }); - } - } +export function MigrationContainer() { + const location = useLocation(); + + if (getSystemStatus() !== 'UP') { + const to = { + pathname: '/maintenance', + search: new URLSearchParams({ + return_to: location.pathname + location.search + location.hash + }).toString() + }; - render() { - if (getSystemStatus() !== 'UP') { - return null; - } - return this.props.children; + return <Navigate to={to} />; } + return <Outlet />; } + +export default MigrationContainer; diff --git a/server/sonar-web/src/main/js/app/components/NonAdminPagesContainer.tsx b/server/sonar-web/src/main/js/app/components/NonAdminPagesContainer.tsx index 622a88610ae..0b1a924ab5f 100644 --- a/server/sonar-web/src/main/js/app/components/NonAdminPagesContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/NonAdminPagesContainer.tsx @@ -18,25 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { Outlet } from 'react-router-dom'; import { Alert } from '../../components/ui/Alert'; import { translate } from '../../helpers/l10n'; import { isApplication } from '../../types/component'; -import { Component } from '../../types/types'; +import { ComponentContext } from './componentContext/ComponentContext'; -export interface NonAdminPagesContainerProps { - children: JSX.Element; - component: Component; -} - -export default function NonAdminPagesContainer(props: NonAdminPagesContainerProps) { - const { children, component } = props; +export default function NonAdminPagesContainer() { + const { component } = React.useContext(ComponentContext); /* * Catch Applications for which the user does not have access to all child projects * and prevent displaying whatever page was requested. * This doesn't apply to admin pages (those are not within this container) */ - if (isApplication(component.qualifier) && !component.canBrowseAllChildProjects) { + if (component && isApplication(component.qualifier) && !component.canBrowseAllChildProjects) { return ( <div className="page page-limited display-flex-justify-center"> <Alert @@ -51,5 +47,5 @@ export default function NonAdminPagesContainer(props: NonAdminPagesContainerProp ); } - return React.cloneElement(children, props); + return <Outlet />; } diff --git a/server/sonar-web/src/main/js/app/components/NotFound.tsx b/server/sonar-web/src/main/js/app/components/NotFound.tsx index e476c137172..2c5546b4a4a 100644 --- a/server/sonar-web/src/main/js/app/components/NotFound.tsx +++ b/server/sonar-web/src/main/js/app/components/NotFound.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { translate } from '../../helpers/l10n'; import SimpleContainer from './SimpleContainer'; diff --git a/server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx b/server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx index 394fa4a10b3..9445a72706f 100644 --- a/server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx +++ b/server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx @@ -38,8 +38,15 @@ export interface PluginRiskConsentProps { export function PluginRiskConsent(props: PluginRiskConsentProps) { const { router, currentUser } = props; - if (!hasGlobalPermission(currentUser, Permissions.Admin)) { - router.replace('/'); + const isAdmin = hasGlobalPermission(currentUser, Permissions.Admin); + + React.useEffect(() => { + if (!isAdmin) { + router.replace('/'); + } + }, [isAdmin, router]); + + if (!isAdmin) { return null; } diff --git a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.tsx b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.tsx index b806154db88..bc718d9be32 100644 --- a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.tsx @@ -18,23 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { Outlet } from 'react-router-dom'; import A11ySkipTarget from '../../components/a11y/A11ySkipTarget'; -import { BranchLike } from '../../types/branch-like'; import { Component } from '../../types/types'; import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; +import withComponentContext from './componentContext/withComponentContext'; interface Props { - children: JSX.Element; - branchLike?: BranchLike; - branchLikes: BranchLike[]; component: Component; - isInProgress?: boolean; - isPending?: boolean; - onBranchesChange: () => void; - onComponentChange: (changes: {}) => void; } -export default class ProjectAdminContainer extends React.PureComponent<Props> { +export class ProjectAdminContainer extends React.PureComponent<Props> { componentDidMount() { this.checkPermissions(); } @@ -59,12 +53,13 @@ export default class ProjectAdminContainer extends React.PureComponent<Props> { return null; } - const { children, ...props } = this.props; return ( <> <A11ySkipTarget anchor="admin_main" /> - {React.cloneElement(children, props)} + <Outlet /> </> ); } } + +export default withComponentContext(ProjectAdminContainer); diff --git a/server/sonar-web/src/main/js/app/components/SimpleContainer.tsx b/server/sonar-web/src/main/js/app/components/SimpleContainer.tsx index 0a9e52d54dd..ad605d25770 100644 --- a/server/sonar-web/src/main/js/app/components/SimpleContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/SimpleContainer.tsx @@ -18,20 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { Outlet } from 'react-router-dom'; import NavBar from '../../components/ui/NavBar'; import { rawSizes } from '../theme'; import GlobalFooter from './GlobalFooter'; -interface Props { - children?: React.ReactNode; -} - -export default function SimpleContainer({ children }: Props) { +/* + * We need to render either children or the Outlet, + * because this component is used both in the context of routes and as a regular container + */ +export default function SimpleContainer({ children }: { children?: React.ReactNode }) { return ( <div className="global-container"> <div className="page-wrapper" id="container"> <NavBar className="navbar-global" height={rawSizes.globalNavHeightRaw} /> - {children} + {children !== undefined ? children : <Outlet />} </div> <GlobalFooter /> </div> diff --git a/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx b/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx index c9564386eaa..6efc8feff87 100644 --- a/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx @@ -18,23 +18,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { Outlet } from 'react-router-dom'; import { lazyLoadComponent } from '../../components/lazyLoadComponent'; import GlobalFooter from './GlobalFooter'; const PageTracker = lazyLoadComponent(() => import('./PageTracker')); -interface Props { - children?: React.ReactNode; -} - -export default function SimpleSessionsContainer({ children }: Props) { +export default function SimpleSessionsContainer() { return ( <> <PageTracker /> <div className="global-container"> <div className="page-wrapper" id="container"> - {children} + <Outlet /> </div> <GlobalFooter hideLoggedInInfo={true} /> </div> diff --git a/server/sonar-web/src/main/js/app/components/StartupModal.tsx b/server/sonar-web/src/main/js/app/components/StartupModal.tsx index 8bf0201d3b4..5e6bfb55512 100644 --- a/server/sonar-web/src/main/js/app/components/StartupModal.tsx +++ b/server/sonar-web/src/main/js/app/components/StartupModal.tsx @@ -42,8 +42,8 @@ interface StateProps { type Props = { children?: React.ReactNode; - location: Pick<Location, 'pathname'>; - router: Pick<Router, 'push'>; + location: Location; + router: Router; appState: AppState; }; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx index e9e3361093a..88f82cbab52 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx @@ -102,7 +102,7 @@ it('changes component', () => { loading: false }); - (wrapper.find(Inner).prop('onComponentChange') as Function)({ visibility: 'private' }); + wrapper.instance().handleComponentChange({ visibility: 'private' }); expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: 'private' }); }); @@ -136,8 +136,6 @@ it("doesn't load branches portfolio", async () => { expect(getPullRequests).not.toBeCalled(); expect(getComponentData).toBeCalledWith({ component: 'portfolioKey', branch: undefined }); expect(getComponentNavigation).toBeCalledWith({ component: 'portfolioKey', branch: undefined }); - wrapper.update(); - expect(wrapper.find(Inner).exists()).toBe(true); }); it('updates branches on change', async () => { @@ -153,7 +151,7 @@ it('updates branches on change', async () => { }), loading: false }); - wrapper.find(Inner).prop<Function>('onBranchesChange')(); + wrapper.instance().handleBranchesChange(); expect(getBranches).toBeCalledWith('projectKey'); expect(getPullRequests).toBeCalledWith('projectKey'); await waitAndUpdate(wrapper); @@ -355,7 +353,7 @@ it('should redirect if the component is a portfolio', async () => { router: mockRouter({ replace }) }); await waitAndUpdate(wrapper); - expect(replace).toBeCalledWith({ pathname: '/portfolio', query: { id: componentKey } }); + expect(replace).toBeCalledWith({ pathname: '/portfolio', search: `?id=${componentKey}` }); }); it('should display display the unavailable page if the component needs issue sync', async () => { diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.ts b/server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.tsx index cc705e4d3b8..dca57e11428 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.ts +++ b/server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.tsx @@ -18,14 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { screen } from '@testing-library/react'; +import React from 'react'; import { addGlobalErrorMessage, addGlobalSuccessMessage } from '../../../helpers/globalMessages'; import { renderComponentApp } from '../../../helpers/testReactTestingUtils'; +function NullComponent() { + return null; +} + it('should display messages', () => { jest.useFakeTimers(); - // we render anything, the GlobalMesasgeContainer is rendered independently from routing - renderComponentApp('sonarqube', () => null); + // we render anything, the GlobalMessageContainer is rendered independently from routing + renderComponentApp('sonarqube', <NullComponent />); addGlobalErrorMessage('This is an error'); addGlobalSuccessMessage('This was a triumph!'); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/Landing-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/Landing-test.tsx index 437ef87cf29..13f854d9bfa 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/Landing-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/Landing-test.tsx @@ -19,9 +19,10 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { mockCurrentUser, mockLoggedInUser, mockRouter } from '../../../helpers/testMocks'; +import { Navigate } from 'react-router-dom'; +import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; import { CurrentUser } from '../../../types/users'; -import { Landing } from '../Landing'; +import { Landing, LandingProps } from '../Landing'; it.each([ [mockCurrentUser(), '/projects'], @@ -30,14 +31,12 @@ it.each([ mockLoggedInUser({ homepage: { type: 'ISSUES' } }), expect.objectContaining({ pathname: '/issues' }) ] -])('should render correctly', (currentUser: CurrentUser, homepageUrl: string) => { - const router = mockRouter(); - shallowRender({ router, currentUser }); - expect(router.replace).toHaveBeenCalledWith(homepageUrl); +])('should render correctly', (currentUser: CurrentUser, expected: string) => { + const wrapper = shallowRender({ currentUser }); + + expect(wrapper.find(Navigate).props().to).toEqual(expected); }); -function shallowRender(props: Partial<Landing['props']> = {}) { - return shallow<Landing>( - <Landing currentUser={mockCurrentUser()} router={mockRouter()} {...props} /> - ); +function shallowRender(props: Partial<LandingProps> = {}) { + return shallow<LandingProps>(<Landing currentUser={mockCurrentUser()} {...props} />); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/NonAdminPagesContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/NonAdminPagesContainer-test.tsx index d6d98b7ab09..02302a5b180 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/NonAdminPagesContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/NonAdminPagesContainer-test.tsx @@ -17,48 +17,41 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import * as React from 'react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { mockComponent } from '../../../helpers/mocks/component'; -import { ComponentQualifier } from '../../../types/component'; -import NonAdminPagesContainer, { NonAdminPagesContainerProps } from '../NonAdminPagesContainer'; +import { ComponentContextShape, ComponentQualifier } from '../../../types/component'; +import { Component } from '../../../types/types'; +import { ComponentContext } from '../componentContext/ComponentContext'; +import NonAdminPagesContainer from '../NonAdminPagesContainer'; function Child() { - return <div />; + return <div>Test Child</div>; } -it('should render correctly', () => { - expect( - shallowRender() - .find(Child) - .exists() - ).toBe(true); - - expect( - shallowRender({ - component: mockComponent({ - qualifier: ComponentQualifier.Application, - canBrowseAllChildProjects: true - }) - }) - .find(Child) - .exists() - ).toBe(true); - - const wrapper = shallowRender({ - component: mockComponent({ - qualifier: ComponentQualifier.Application - }) - }); +it('should render correctly for an user that does not have access to all children', () => { + renderNonAdminPagesContainer( + mockComponent({ qualifier: ComponentQualifier.Application, canBrowseAllChildProjects: false }) + ); + expect(screen.getByText('application.cannot_access_all_child_projects1')).toBeInTheDocument(); +}); - expect(wrapper.find(Child).exists()).toBe(false); - expect(wrapper).toMatchSnapshot(); +it('should render correctly', () => { + renderNonAdminPagesContainer(mockComponent()); + expect(screen.getByText('Test Child')).toBeInTheDocument(); }); -function shallowRender(props: Partial<NonAdminPagesContainerProps> = {}) { - return shallow<NonAdminPagesContainerProps>( - <NonAdminPagesContainer component={mockComponent()} {...props}> - <Child /> - </NonAdminPagesContainer> +function renderNonAdminPagesContainer(component: Component) { + return render( + <ComponentContext.Provider value={{ component } as ComponentContextShape}> + <MemoryRouter> + <Routes> + <Route element={<NonAdminPagesContainer />}> + <Route path="*" element={<Child />} /> + </Route> + </Routes> + </MemoryRouter> + </ComponentContext.Provider> ); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/PluginRiskConsent-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/PluginRiskConsent-test.tsx index 480c3d79d8e..dca6ce7d236 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/PluginRiskConsent-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/PluginRiskConsent-test.tsx @@ -28,6 +28,17 @@ jest.mock('../../../api/settings', () => ({ setSimpleSettingValue: jest.fn().mockResolvedValue({}) })); +jest.mock('react', () => { + return { + ...jest.requireActual('react'), + useEffect: jest.fn().mockImplementation(f => f()) + }; +}); + +afterAll(() => { + jest.clearAllMocks(); +}); + it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot('default'); }); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ProjectAdminContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ProjectAdminContainer-test.tsx index 31013720ff4..445de3ac747 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ProjectAdminContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ProjectAdminContainer-test.tsx @@ -21,18 +21,12 @@ import { mount, shallow } from 'enzyme'; import * as React from 'react'; import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; import { mockComponent } from '../../../helpers/mocks/component'; -import ProjectAdminContainer from '../ProjectAdminContainer'; +import { ProjectAdminContainer } from '../ProjectAdminContainer'; jest.mock('../../utils/handleRequiredAuthorization', () => { return jest.fn(); }); -class ChildComponent extends React.Component { - render() { - return null; - } -} - it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); }); @@ -42,13 +36,6 @@ it('should redirect for authorization if needed', () => { expect(handleRequiredAuthorization).toBeCalled(); }); -it('should pass props to its children', () => { - const child = shallowRender().find(ChildComponent); - // No need to check all... - expect(child.prop('component')).toBeDefined(); - expect(child.prop('onBranchesChange')).toBeDefined(); -}); - function mountRender(props: Partial<ProjectAdminContainer['props']> = {}) { return mount(createComponent(props)); } @@ -60,12 +47,8 @@ function shallowRender(props: Partial<ProjectAdminContainer['props']> = {}) { function createComponent(props: Partial<ProjectAdminContainer['props']> = {}) { return ( <ProjectAdminContainer - branchLikes={[]} component={mockComponent({ configuration: { showSettings: true } })} - onBranchesChange={jest.fn()} - onComponentChange={jest.fn()} - {...props}> - <ChildComponent /> - </ProjectAdminContainer> + {...props} + /> ); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx index c6d82ab38e9..96a80a3e0af 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx @@ -24,7 +24,7 @@ import { showLicense } from '../../../api/editions'; import { toShortNotSoISOString } from '../../../helpers/dates'; import { hasMessage } from '../../../helpers/l10n'; import { get, save } from '../../../helpers/storage'; -import { mockAppState } from '../../../helpers/testMocks'; +import { mockAppState, mockLocation, mockRouter } from '../../../helpers/testMocks'; import { waitAndUpdate } from '../../../helpers/testUtils'; import { EditionKey } from '../../../types/editions'; import { LoggedInUser } from '../../../types/users'; @@ -89,7 +89,7 @@ it('should render only the children', async () => { getWrapper({ appState: mockAppState({ canAdmin: false }), currentUser: { ...LOGGED_IN_USER }, - location: { pathname: '/documentation/' } + location: mockLocation({ pathname: '/documentation/' }) }) ); @@ -97,7 +97,7 @@ it('should render only the children', async () => { getWrapper({ appState: mockAppState({ canAdmin: false }), currentUser: { ...LOGGED_IN_USER }, - location: { pathname: '/create-organization' } + location: mockLocation({ pathname: '/create-organization' }) }) ); }); @@ -129,8 +129,8 @@ function getWrapper(props: Partial<StartupModal['props']> = {}) { <StartupModal appState={mockAppState({ edition: EditionKey.enterprise, canAdmin: true })} currentUser={LOGGED_IN_USER} - location={{ pathname: 'foo/bar' }} - router={{ push: jest.fn() }} + location={mockLocation({ pathname: 'foo/bar' })} + router={mockRouter()} {...props}> <div /> </StartupModal> diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/AdminContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/AdminContainer-test.tsx.snap index b36639ca82e..3e6df64fa42 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/AdminContainer-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/AdminContainer-test.tsx.snap @@ -9,7 +9,7 @@ exports[`should render correctly 1`] = ` prioritizeSeoTags={false} titleTemplate="%s - layout.settings" /> - <SettingsNav + <WithLocation extensions={Array []} fetchPendingPlugins={[Function]} fetchSystemStatus={[Function]} @@ -36,8 +36,12 @@ exports[`should render correctly 1`] = ` } } > - <div - adminPages={Array []} + <Outlet + context={ + Object { + "adminPages": Array [], + } + } /> </ContextProvider> </div> diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap index 8f592cd1ec7..d322409ceb1 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap @@ -3,6 +3,7 @@ exports[`should render correctly: default 1`] = ` <Fragment> <LazyComponentWrapper /> + <Outlet /> <KeyboardShortcutsModal /> </Fragment> `; @@ -15,6 +16,7 @@ exports[`should render correctly: with gravatar 1`] = ` rel="preconnect" /> </LazyComponentWrapper> + <Outlet /> <KeyboardShortcutsModal /> </Fragment> `; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap deleted file mode 100644 index 16b94edc8ff..00000000000 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap +++ /dev/null @@ -1,54 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<SuggestionsProvider> - <A11yProvider> - <withCurrentUserContext(withRouter(withAppStateContext(StartupModal)))> - <A11ySkipLinks /> - <div - className="global-container" - > - <div - className="page-wrapper" - id="container" - > - <div - className="page-container" - > - <BranchStatusContextProvider> - <Workspace> - <withAppStateContext(IndexationContextProvider)> - <LanguagesContextProvider> - <MetricsContextProvider> - <withCurrentUserContext(GlobalNav) - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/path", - "query": Object {}, - "search": "", - "state": Object {}, - } - } - /> - <withCurrentUserContext(withIndexationContext(IndexationNotification)) /> - <withCurrentUserContext(withAppStateContext(UpdateNotification)) - dismissable={true} - /> - <ChildComponent /> - </MetricsContextProvider> - </LanguagesContextProvider> - </withAppStateContext(IndexationContextProvider)> - </Workspace> - </BranchStatusContextProvider> - </div> - <withCurrentUserContext(PromotionNotification) /> - </div> - <withAppStateContext(GlobalFooter) /> - </div> - </withCurrentUserContext(withRouter(withAppStateContext(StartupModal)))> - </A11yProvider> -</SuggestionsProvider> -`; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap index d5450c04210..233f033bc31 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap @@ -45,8 +45,6 @@ exports[`should display the sq version 1`] = ` className="page-footer-menu-item" > <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation" > footer.documentation @@ -67,8 +65,6 @@ exports[`should display the sq version 1`] = ` className="page-footer-menu-item" > <Link - onlyActiveOnIndex={false} - style={Object {}} to="/web_api" > footer.web_api @@ -113,8 +109,6 @@ exports[`should not render the only logged in information 1`] = ` className="page-footer-menu-item" > <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation" > footer.documentation @@ -180,8 +174,6 @@ exports[`should render the only logged in information 1`] = ` className="page-footer-menu-item" > <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation" > footer.documentation @@ -202,8 +194,6 @@ exports[`should render the only logged in information 1`] = ` className="page-footer-menu-item" > <Link - onlyActiveOnIndex={false} - style={Object {}} to="/web_api" > footer.web_api diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/NonAdminPagesContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/NonAdminPagesContainer-test.tsx.snap deleted file mode 100644 index ec24550e566..00000000000 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/NonAdminPagesContainer-test.tsx.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<div - className="page page-limited display-flex-justify-center" -> - <Alert - className="it__alert-no-access-all-child-project max-width-60 huge-spacer-top" - display="block" - variant="error" - > - <p> - application.cannot_access_all_child_projects1 - </p> - <br /> - <p> - application.cannot_access_all_child_projects2 - </p> - </Alert> -</div> -`; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ProjectAdminContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ProjectAdminContainer-test.tsx.snap index 90341d94f9a..c3f74fb0aa7 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ProjectAdminContainer-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ProjectAdminContainer-test.tsx.snap @@ -5,35 +5,6 @@ exports[`should render correctly 1`] = ` <A11ySkipTarget anchor="admin_main" /> - <ChildComponent - branchLikes={Array []} - component={ - Object { - "breadcrumbs": Array [], - "configuration": Object { - "showSettings": true, - }, - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": Array [], - } - } - onBranchesChange={[MockFunction]} - onComponentChange={[MockFunction]} - /> + <Outlet /> </Fragment> `; diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/PortfolioPage-test.tsx b/server/sonar-web/src/main/js/app/components/admin/withAdminPagesOutletContext.tsx index 1e1166e1807..961f4f93626 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/PortfolioPage-test.tsx +++ b/server/sonar-web/src/main/js/app/components/admin/withAdminPagesOutletContext.tsx @@ -17,25 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; import * as React from 'react'; -import { mockComponent } from '../../../../helpers/mocks/component'; -import { mockLocation, mockRouter } from '../../../../helpers/testMocks'; -import { PortfolioPage, PortfolioPageProps } from '../PortfolioPage'; +import { useOutletContext } from 'react-router-dom'; +import { AdminPagesContext } from '../../../types/admin'; -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); +export default function withAdminPagesOutletContext( + WrappedComponent: React.ComponentType<AdminPagesContext> +) { + return function WithAdminPagesOutletContext() { + const { adminPages } = useOutletContext<AdminPagesContext>(); -function shallowRender(props?: Partial<PortfolioPageProps>) { - return shallow<PortfolioPageProps>( - <PortfolioPage - component={mockComponent()} - location={mockLocation()} - router={mockRouter()} - routes={[]} - params={{}} - {...props} - /> - ); + return <WrappedComponent adminPages={adminPages} />; + }; } diff --git a/server/sonar-web/src/main/js/app/components/ComponentContext.tsx b/server/sonar-web/src/main/js/app/components/componentContext/ComponentContext.ts index f00f4dfc645..10db938858d 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContext.tsx +++ b/server/sonar-web/src/main/js/app/components/componentContext/ComponentContext.ts @@ -17,16 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { noop } from 'lodash'; import * as React from 'react'; -import { BranchLike } from '../../types/branch-like'; -import { Component } from '../../types/types'; +import { ComponentContextShape } from '../../../types/component'; -interface ComponentContextType { - branchLike: BranchLike | undefined; - component: Component | undefined; -} - -export const ComponentContext = React.createContext<ComponentContextType>({ - branchLike: undefined, - component: undefined +export const ComponentContext = React.createContext<ComponentContextShape>({ + branchLikes: [], + onBranchesChange: noop, + onComponentChange: noop }); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalContainer-test.tsx b/server/sonar-web/src/main/js/app/components/componentContext/withComponentContext.tsx index e094b6512df..a9071e2ebb2 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/GlobalContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/componentContext/withComponentContext.tsx @@ -17,35 +17,25 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; import * as React from 'react'; -import { mockLocation } from '../../../helpers/testMocks'; -import GlobalContainer, { Props } from '../GlobalContainer'; +import { getWrappedDisplayName } from '../../../components/hoc/utils'; +import { ComponentContextShape } from '../../../types/component'; +import { ComponentContext } from './ComponentContext'; -jest.mock('../../../components/embed-docs-modal/SuggestionsProvider', () => { - class SuggestionsProvider extends React.Component { - render() { - return this.props.children; - } - } - - return SuggestionsProvider; -}); +export default function withComponentContext<P extends Partial<ComponentContextShape>>( + WrappedComponent: React.ComponentType<P> +) { + return class WithComponentContext extends React.PureComponent< + Omit<P, keyof ComponentContextShape> + > { + static displayName = getWrappedDisplayName(WrappedComponent, 'withComponentContext'); -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -function shallowRender(props: Partial<Props> = {}) { - class ChildComponent extends React.Component { render() { - return null; + return ( + <ComponentContext.Consumer> + {componentContext => <WrappedComponent {...componentContext} {...(this.props as P)} />} + </ComponentContext.Consumer> + ); } - } - - return shallow( - <GlobalContainer location={mockLocation()} {...props}> - <ChildComponent /> - </GlobalContainer> - ); + }; } diff --git a/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx index 28974085d08..388aab27464 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx @@ -18,20 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Extension as TypeExtension } from '../../../types/types'; +import { useOutletContext, useParams } from 'react-router-dom'; +import { AdminPagesContext } from '../../../types/admin'; import NotFound from '../NotFound'; import Extension from './Extension'; -interface Props { - adminPages: TypeExtension[] | undefined; - params: { extensionKey: string; pluginKey: string }; -} +export default function GlobalAdminPageExtension() { + const { pluginKey, extensionKey } = useParams(); + const { adminPages } = useOutletContext<AdminPagesContext>(); -export default function GlobalAdminPageExtension(props: Props) { - const { - params: { extensionKey, pluginKey }, - adminPages - } = props; const extension = (adminPages || []).find(p => p.key === `${pluginKey}/${extensionKey}`); return extension ? <Extension extension={extension} /> : <NotFound withContainer={false} />; } diff --git a/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx index 0c4206497c2..6216cf5f55e 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx @@ -18,22 +18,33 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { useParams } from 'react-router-dom'; import { AppState } from '../../../types/appstate'; import withAppStateContext from '../app-state/withAppStateContext'; import NotFound from '../NotFound'; import Extension from './Extension'; -interface Props { +export interface GlobalPageExtensionProps { appState: AppState; - params: { extensionKey: string; pluginKey: string }; + params?: { + extensionKey: string; + pluginKey: string; + }; } -function GlobalPageExtension(props: Props) { +function GlobalPageExtension(props: GlobalPageExtensionProps) { const { - params: { extensionKey, pluginKey }, - appState: { globalPages } + appState: { globalPages }, + params } = props; - const extension = (globalPages || []).find(p => p.key === `${pluginKey}/${extensionKey}`); + const { extensionKey, pluginKey } = useParams(); + + const fullKey = + params !== undefined + ? `${params.pluginKey}/${params.extensionKey}` + : `${pluginKey}/${extensionKey}`; + + const extension = (globalPages || []).find(p => p.key === fullKey); return extension ? <Extension extension={extension} /> : <NotFound withContainer={false} />; } diff --git a/server/sonar-web/src/main/js/app/components/extensions/PortfolioPage.tsx b/server/sonar-web/src/main/js/app/components/extensions/PortfolioPage.tsx index 389c138c607..9925a233ebb 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/PortfolioPage.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/PortfolioPage.tsx @@ -18,23 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { WithRouterProps } from 'react-router'; import withIndexationGuard from '../../../components/hoc/withIndexationGuard'; -import { Component } from '../../../types/types'; import { PageContext } from '../indexation/PageUnavailableDueToIndexation'; import ProjectPageExtension from './ProjectPageExtension'; -export interface PortfolioPageProps extends WithRouterProps { - component: Component; -} - -export function PortfolioPage({ component }: PortfolioPageProps) { - return ( - <ProjectPageExtension - component={component} - params={{ pluginKey: 'governance', extensionKey: 'portfolio' }} - /> - ); +export function PortfolioPage() { + return <ProjectPageExtension params={{ pluginKey: 'governance', extensionKey: 'portfolio' }} />; } export default withIndexationGuard(PortfolioPage, PageContext.Portfolios); diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx index efc6ae1a1ae..58e16dc1093 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx @@ -18,22 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Component } from '../../../types/types'; +import { useParams } from 'react-router-dom'; +import { ComponentContext } from '../componentContext/ComponentContext'; import NotFound from '../NotFound'; import Extension from './Extension'; -export interface ProjectAdminPageExtensionProps { - component: Component; - params: { extensionKey: string; pluginKey: string }; -} - -export default function ProjectAdminPageExtension(props: ProjectAdminPageExtensionProps) { - const { - component, - params: { extensionKey, pluginKey } - } = props; +export default function ProjectAdminPageExtension() { + const { extensionKey, pluginKey } = useParams(); + const { component } = React.useContext(ComponentContext); const extension = + component && component.configuration && (component.configuration.extensions || []).find(p => p.key === `${pluginKey}/${extensionKey}`); diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx index 40f8bd94b39..de3ef4ce9ff 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx @@ -18,26 +18,32 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { BranchLike } from '../../../types/branch-like'; -import { Component } from '../../../types/types'; +import { useParams } from 'react-router-dom'; +import { ComponentContext } from '../componentContext/ComponentContext'; import NotFound from '../NotFound'; import Extension from './Extension'; export interface ProjectPageExtensionProps { - branchLike?: BranchLike; - component: Component; - params: { + params?: { extensionKey: string; pluginKey: string; }; } -export default function ProjectPageExtension(props: ProjectPageExtensionProps) { - const { extensionKey, pluginKey } = props.params; - const { branchLike, component } = props; - const extension = - component.extensions && - component.extensions.find(p => p.key === `${pluginKey}/${extensionKey}`); +export default function ProjectPageExtension({ params }: ProjectPageExtensionProps) { + const { extensionKey, pluginKey } = useParams(); + const { branchLike, component } = React.useContext(ComponentContext); + + if (component === undefined) { + return null; + } + + const fullKey = + params !== undefined + ? `${params.pluginKey}/${params.extensionKey}` + : `${pluginKey}/${extensionKey}`; + + const extension = component.extensions && component.extensions.find(p => p.key === fullKey); return extension ? ( <Extension extension={extension} options={{ branchLike, component }} /> ) : ( diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/GlobalPageExtension-test.tsx b/server/sonar-web/src/main/js/app/components/extensions/__tests__/GlobalPageExtension-test.tsx new file mode 100644 index 00000000000..62f4375af20 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/GlobalPageExtension-test.tsx @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { screen } from '@testing-library/react'; +import * as React from 'react'; +import { mockAppState } from '../../../../helpers/testMocks'; +import { renderComponentApp } from '../../../../helpers/testReactTestingUtils'; +import { Extension } from '../../../../types/types'; +import GlobalPageExtension, { GlobalPageExtensionProps } from '../GlobalPageExtension'; + +jest.mock('../Extension', () => ({ + __esModule: true, + default(props: { extension: { key: string; name: string } }) { + return <h1>{props.extension.name}</h1>; + } +})); + +const extensions = [{ key: 'plugin123/ext42', name: 'extension 42' }]; + +it('should find the extension from params', () => { + renderGlobalPageExtension('extension/plugin123/ext42', extensions); + + expect(screen.getByText('extension 42')).toBeInTheDocument(); +}); + +it('should notify if extension is not found', () => { + renderGlobalPageExtension('extension/plugin123/wrong-extension', extensions); + + expect(screen.getByText('page_not_found')).toBeInTheDocument(); +}); + +it('should find the extension from props', () => { + const params = { pluginKey: 'plugin123', extensionKey: 'ext42' }; + + renderGlobalPageExtension('extension/whatever/overridden', extensions, params); + + expect(screen.getByText('extension 42')).toBeInTheDocument(); +}); + +function renderGlobalPageExtension( + navigateTo: string, + globalPages: Extension[] = [], + params?: GlobalPageExtensionProps['params'] +) { + renderComponentApp( + `extension/:pluginKey/:extensionKey`, + <GlobalPageExtension params={params} />, + { + appState: mockAppState({ globalPages }), + navigateTo + } + ); +} diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx b/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx index 7b29077a35f..eea7470d3c2 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx @@ -17,30 +17,59 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import * as React from 'react'; +import { HelmetProvider } from 'react-helmet-async'; +import { IntlProvider } from 'react-intl'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { getExtensionStart } from '../../../../helpers/extensions'; import { mockComponent } from '../../../../helpers/mocks/component'; -import ProjectAdminPageExtension, { - ProjectAdminPageExtensionProps -} from '../ProjectAdminPageExtension'; +import { ComponentContextShape } from '../../../../types/component'; +import { Component } from '../../../../types/types'; +import { ComponentContext } from '../../componentContext/ComponentContext'; +import ProjectAdminPageExtension from '../ProjectAdminPageExtension'; -it('should render correctly', () => { - expect( - shallowRender({ - component: mockComponent({ - configuration: { extensions: [{ key: 'foo/bar', name: 'Foo Bar' }] } - }) - }) - ).toMatchSnapshot('extension exists'); - expect(shallowRender()).toMatchSnapshot('extension not found'); +jest.mock('../../../../helpers/extensions', () => ({ + getExtensionStart: jest.fn().mockResolvedValue(jest.fn()) +})); + +it('should render correctly when the extension is found', () => { + renderProjectAdminPageExtension( + mockComponent({ + configuration: { extensions: [{ key: 'pluginId/extensionId', name: 'name' }] } + }), + { pluginKey: 'pluginId', extensionKey: 'extensionId' } + ); + expect(getExtensionStart).toBeCalledWith('pluginId/extensionId'); +}); + +it('should render correctly when the extension is not found', () => { + renderProjectAdminPageExtension( + mockComponent({ extensions: [{ key: 'pluginId/extensionId', name: 'name' }] }), + { pluginKey: 'not-found-plugin', extensionKey: 'not-found-extension' } + ); + expect(screen.getByText('page_not_found')).toBeInTheDocument(); }); -function shallowRender(props: Partial<ProjectAdminPageExtensionProps> = {}) { - return shallow( - <ProjectAdminPageExtension - component={mockComponent()} - params={{ extensionKey: 'bar', pluginKey: 'foo' }} - {...props} - /> +function renderProjectAdminPageExtension( + component: Component, + params: { + extensionKey: string; + pluginKey: string; + } +) { + const { pluginKey, extensionKey } = params; + return render( + <HelmetProvider context={{}}> + <IntlProvider defaultLocale="en" locale="en"> + <ComponentContext.Provider value={{ component } as ComponentContextShape}> + <MemoryRouter initialEntries={[`/${pluginKey}/${extensionKey}`]}> + <Routes> + <Route path="/:pluginKey/:extensionKey" element={<ProjectAdminPageExtension />} /> + </Routes> + </MemoryRouter> + </ComponentContext.Provider> + </IntlProvider> + </HelmetProvider> ); } diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectPageExtension-test.tsx b/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectPageExtension-test.tsx index 1d0dab75dec..0e8517dab9d 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectPageExtension-test.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectPageExtension-test.tsx @@ -17,33 +17,67 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import * as React from 'react'; -import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; +import { HelmetProvider } from 'react-helmet-async'; +import { IntlProvider } from 'react-intl'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { getExtensionStart } from '../../../../helpers/extensions'; import { mockComponent } from '../../../../helpers/mocks/component'; +import { ComponentContextShape } from '../../../../types/component'; +import { Component } from '../../../../types/types'; +import { ComponentContext } from '../../componentContext/ComponentContext'; import ProjectPageExtension, { ProjectPageExtensionProps } from '../ProjectPageExtension'; -it('should render correctly', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); +jest.mock('../../../../helpers/extensions', () => ({ + getExtensionStart: jest.fn().mockResolvedValue(jest.fn()) +})); + +it('should not render when no component is passed', () => { + renderProjectPageExtension(); + expect(screen.queryByText('page_not_found')).not.toBeInTheDocument(); + expect(getExtensionStart).not.toBeCalledWith('pluginId/extensionId'); +}); + +it('should render correctly when the extension is found', () => { + renderProjectPageExtension( + mockComponent({ extensions: [{ key: 'pluginId/extensionId', name: 'name' }] }), + { params: { pluginKey: 'pluginId', extensionKey: 'extensionId' } } + ); + expect(getExtensionStart).toBeCalledWith('pluginId/extensionId'); }); it('should render correctly when the extension is not found', () => { - const wrapper = shallowRender({ - params: { pluginKey: 'not-found-plugin', extensionKey: 'not-found-extension' } - }); - expect(wrapper).toMatchSnapshot(); + renderProjectPageExtension( + mockComponent({ extensions: [{ key: 'pluginId/extensionId', name: 'name' }] }), + { params: { pluginKey: 'not-found-plugin', extensionKey: 'not-found-extension' } } + ); + expect(screen.getByText('page_not_found')).toBeInTheDocument(); }); -function shallowRender(props?: Partial<ProjectPageExtensionProps>) { - return shallow( - <ProjectPageExtension - branchLike={mockMainBranch()} - component={mockComponent({ - extensions: [{ key: 'plugin-key/extension-key', name: 'plugin' }] - })} - params={{ extensionKey: 'extension-key', pluginKey: 'plugin-key' }} - {...props} - /> +function renderProjectPageExtension( + component?: Component, + props?: Partial<ProjectPageExtensionProps> +) { + return render( + <HelmetProvider context={{}}> + <IntlProvider defaultLocale="en" locale="en"> + <ComponentContext.Provider value={{ component } as ComponentContextShape}> + <MemoryRouter> + <Routes> + <Route + path="*" + element={ + <ProjectPageExtension + params={{ extensionKey: 'extensionId', pluginKey: 'pluginId' }} + {...props} + /> + } + /> + </Routes> + </MemoryRouter> + </ComponentContext.Provider> + </IntlProvider> + </HelmetProvider> ); } diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap index 183e24e1dfc..44929f62890 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap @@ -27,7 +27,6 @@ exports[`should render React extensions correctly 1`] = ` intl={Object {}} location={ Object { - "action": "PUSH", "hash": "", "key": "key", "pathname": "/path", @@ -87,7 +86,6 @@ exports[`should render React extensions correctly 2`] = ` intl={Object {}} location={ Object { - "action": "PUSH", "hash": "", "key": "key", "pathname": "/path", diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/PortfolioPage-test.tsx.snap b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/PortfolioPage-test.tsx.snap deleted file mode 100644 index dc84ed679d0..00000000000 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/PortfolioPage-test.tsx.snap +++ /dev/null @@ -1,34 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<ProjectPageExtension - component={ - Object { - "breadcrumbs": Array [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": Array [], - } - } - params={ - Object { - "extensionKey": "portfolio", - "pluginKey": "governance", - } - } -/> -`; diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap deleted file mode 100644 index 690ff078cd9..00000000000 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap +++ /dev/null @@ -1,50 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: extension exists 1`] = ` -<injectIntl(withRouter(withAppStateContext(withCurrentUserContext(Extension)))) - extension={ - Object { - "key": "foo/bar", - "name": "Foo Bar", - } - } - options={ - Object { - "component": Object { - "breadcrumbs": Array [], - "configuration": Object { - "extensions": Array [ - Object { - "key": "foo/bar", - "name": "Foo Bar", - }, - ], - }, - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": Array [], - }, - } - } -/> -`; - -exports[`should render correctly: extension not found 1`] = ` -<NotFound - withContainer={false} -/> -`; diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectPageExtension-test.tsx.snap b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectPageExtension-test.tsx.snap deleted file mode 100644 index c99ec489367..00000000000 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectPageExtension-test.tsx.snap +++ /dev/null @@ -1,54 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<injectIntl(withRouter(withAppStateContext(withCurrentUserContext(Extension)))) - extension={ - Object { - "key": "plugin-key/extension-key", - "name": "plugin", - } - } - options={ - Object { - "branchLike": Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - }, - "component": Object { - "breadcrumbs": Array [], - "extensions": Array [ - Object { - "key": "plugin-key/extension-key", - "name": "plugin", - }, - ], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": Array [], - }, - } - } -/> -`; - -exports[`should render correctly when the extension is not found 1`] = ` -<NotFound - withContainer={false} -/> -`; diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx index 8c192a2490d..16439c7b2f0 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx @@ -22,9 +22,10 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { Alert, AlertProps } from '../../../components/ui/Alert'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { queryToSearch } from '../../../helpers/urls'; import { IndexationNotificationType } from '../../../types/indexation'; import { TaskStatuses, TaskTypes } from '../../../types/tasks'; @@ -143,10 +144,10 @@ function renderBackgroundTasksPageLink(hasError: boolean, text: string) { <Link to={{ pathname: '/admin/background_tasks', - query: { + search: queryToSearch({ taskType: TaskTypes.IssueSync, status: hasError ? TaskStatuses.Failed : undefined - } + }) }}> {text} </Link> diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap index aebc9ce3e46..6802c479180 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap @@ -95,15 +95,10 @@ exports[`should render correctly for type="CompletedWithFailure" & isSystemAdmin values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/admin/background_tasks", - "query": Object { - "status": "FAILED", - "taskType": "ISSUE_SYNC", - }, + "search": "?taskType=ISSUE_SYNC&status=FAILED", } } > @@ -182,15 +177,10 @@ exports[`should render correctly for type="InProgress" & isSystemAdmin=true 1`] values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/admin/background_tasks", - "query": Object { - "status": undefined, - "taskType": "ISSUE_SYNC", - }, + "search": "?taskType=ISSUE_SYNC", } } > @@ -272,15 +262,10 @@ exports[`should render correctly for type="InProgressWithFailure" & isSystemAdmi values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/admin/background_tasks", - "query": Object { - "status": "FAILED", - "taskType": "ISSUE_SYNC", - }, + "search": "?taskType=ISSUE_SYNC&status=FAILED", } } > diff --git a/server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx b/server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx index c54b4d6161e..f96ee9b78d0 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx @@ -19,7 +19,7 @@ */ import { last } from 'lodash'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import QualifierIcon from '../../../../components/icons/QualifierIcon'; import { isMainBranch } from '../../../../helpers/branch-like'; import { getComponentOverviewUrl } from '../../../../helpers/urls'; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx index 4b7908296ef..bdd5fce58c6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx @@ -19,9 +19,9 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link, WithRouterProps } from 'react-router'; +import { Link } from 'react-router-dom'; import { STATUSES } from '../../../../apps/background-tasks/constants'; -import { withRouter } from '../../../../components/hoc/withRouter'; +import { Location, withRouter } from '../../../../components/hoc/withRouter'; import { Alert } from '../../../../components/ui/Alert'; import { hasMessage, translate } from '../../../../helpers/l10n'; import { getComponentBackgroundTaskUrl } from '../../../../helpers/urls'; @@ -29,12 +29,13 @@ import { Task, TaskStatuses } from '../../../../types/tasks'; import { Component } from '../../../../types/types'; import ComponentNavLicenseNotif from './ComponentNavLicenseNotif'; -interface Props extends Pick<WithRouterProps, 'location'> { +interface Props { component: Component; currentTask?: Task; currentTaskOnSameBranch?: boolean; isInProgress?: boolean; isPending?: boolean; + location: Location; } export class ComponentNavBgTaskNotif extends React.PureComponent<Props> { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx index 7ada2c4db56..ba2f645c2d1 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { isValidLicense } from '../../../../api/editions'; import { Alert } from '../../../../components/ui/Alert'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavProjectBindingErrorNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavProjectBindingErrorNotif.tsx index 58a0175c583..5cbb5494a0a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavProjectBindingErrorNotif.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavProjectBindingErrorNotif.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { PULL_REQUEST_DECORATION_BINDING_CATEGORY } from '../../../../apps/settings/constants'; import { Alert } from '../../../../components/ui/Alert'; import { translate } from '../../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx index 7b33626b62a..1f8a8888dc8 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx @@ -18,10 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; -import { LocationDescriptorObject } from 'history'; -import { omit } from 'lodash'; import * as React from 'react'; -import { Link, LinkProps } from 'react-router'; +import { NavLink } from 'react-router-dom'; import Dropdown from '../../../../components/controls/Dropdown'; import Tooltip from '../../../../components/controls/Tooltip'; import BulletListIcon from '../../../../components/icons/BulletListIcon'; @@ -33,7 +31,7 @@ import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls'; import { AppState } from '../../../../types/appstate'; import { BranchLike, BranchParameters } from '../../../../types/branch-like'; import { ComponentQualifier, isPortfolioLike } from '../../../../types/component'; -import { Component, Extension } from '../../../../types/types'; +import { Component, Dict, Extension } from '../../../../types/types'; import withAppStateContext from '../../app-state/withAppStateContext'; import './Menu.css'; @@ -131,11 +129,12 @@ export class Menu extends React.PureComponent<Props> { renderMenuLink = ({ label, - to, - ...props - }: Omit<LinkProps, 'to'> & { + pathname, + additionalQueryParams = {} + }: { label: React.ReactNode; - to: LocationDescriptorObject; + pathname: string; + additionalQueryParams?: Dict<string>; }) => { const hasAnalysis = this.hasAnalysis(); const isApplicationChildInaccessble = this.isApplicationChildInaccessble(); @@ -146,12 +145,13 @@ export class Menu extends React.PureComponent<Props> { return ( <li> {hasAnalysis ? ( - <Link - activeClassName="active" - to={{ ...to, query: { ...query, ...to.query } }} - {...omit(props, ['to'])}> + <NavLink + to={{ + pathname, + search: new URLSearchParams({ ...query, ...additionalQueryParams }).toString() + }}> {label} - </Link> + </NavLink> ) : ( <Tooltip overlay={translate('layout.must_be_configured')}> <a aria-disabled="true" className="disabled-link"> @@ -169,9 +169,7 @@ export class Menu extends React.PureComponent<Props> { if (this.isPortfolio()) { return this.isGovernanceEnabled() ? ( <li> - <Link activeClassName="active" to={getPortfolioUrl(id)}> - {translate('overview.page')} - </Link> + <NavLink to={getPortfolioUrl(id)}>{translate('overview.page')}</NavLink> </li> ) : null; } @@ -182,9 +180,7 @@ export class Menu extends React.PureComponent<Props> { } return ( <li> - <Link activeClassName="active" to={getProjectQueryUrl(id, branchLike)}> - {translate('overview.page')} - </Link> + <NavLink to={getProjectQueryUrl(id, branchLike)}>{translate('overview.page')}</NavLink> </li> ); }; @@ -193,7 +189,7 @@ export class Menu extends React.PureComponent<Props> { return this.isPortfolio() && this.isGovernanceEnabled() ? this.renderMenuLink({ label: translate('portfolio_breakdown.page'), - to: { pathname: '/code' } + pathname: '/code' }) : null; }; @@ -205,7 +201,7 @@ export class Menu extends React.PureComponent<Props> { const label = this.isApplication() ? translate('view_projects.page') : translate('code.page'); - return this.renderMenuLink({ label, to: { pathname: '/code' } }); + return this.renderMenuLink({ label, pathname: '/code' }); }; renderActivityLink = () => { @@ -217,21 +213,22 @@ export class Menu extends React.PureComponent<Props> { return this.renderMenuLink({ label: translate('project_activity.page'), - to: { pathname: '/project/activity' } + pathname: '/project/activity' }); }; renderIssuesLink = () => { return this.renderMenuLink({ label: translate('issues.page'), - to: { pathname: '/project/issues', query: { resolved: 'false' } } + pathname: '/project/issues', + additionalQueryParams: { resolved: 'false' } }); }; renderComponentMeasuresLink = () => { return this.renderMenuLink({ label: translate('layout.measures'), - to: { pathname: '/component_measures' } + pathname: '/component_measures' }); }; @@ -241,7 +238,7 @@ export class Menu extends React.PureComponent<Props> { !isPortfolio && this.renderMenuLink({ label: translate('layout.security_hotspots'), - to: { pathname: '/security_hotspots' } + pathname: '/security_hotspots' }) ); }; @@ -264,7 +261,7 @@ export class Menu extends React.PureComponent<Props> { return this.renderMenuLink({ label: translate('layout.security_reports'), - to: { pathname: '/project/extension/securityreport/securityreport' } + pathname: '/project/extension/securityreport/securityreport' }); }; @@ -373,9 +370,10 @@ export class Menu extends React.PureComponent<Props> { } return ( <li key="settings"> - <Link activeClassName="active" to={{ pathname: '/project/settings', query }}> + <NavLink + to={{ pathname: '/project/settings', search: new URLSearchParams(query).toString() }}> {translate('project_settings.page')} - </Link> + </NavLink> </li> ); }; @@ -391,9 +389,10 @@ export class Menu extends React.PureComponent<Props> { return ( <li key="branches"> - <Link activeClassName="active" to={{ pathname: '/project/branches', query }}> + <NavLink + to={{ pathname: '/project/branches', search: new URLSearchParams(query).toString() }}> {translate('project_branch_pull_request.page')} - </Link> + </NavLink> </li> ); }; @@ -404,9 +403,10 @@ export class Menu extends React.PureComponent<Props> { } return ( <li key="baseline"> - <Link activeClassName="active" to={{ pathname: '/project/baseline', query }}> + <NavLink + to={{ pathname: '/project/baseline', search: new URLSearchParams(query).toString() }}> {translate('project_baseline.page')} - </Link> + </NavLink> </li> ); }; @@ -417,9 +417,13 @@ export class Menu extends React.PureComponent<Props> { } return ( <li key="import-export"> - <Link activeClassName="active" to={{ pathname: '/project/import_export', query }}> + <NavLink + to={{ + pathname: '/project/import_export', + search: new URLSearchParams(query).toString() + }}> {translate('project_dump.page')} - </Link> + </NavLink> </li> ); }; @@ -430,9 +434,13 @@ export class Menu extends React.PureComponent<Props> { } return ( <li key="profiles"> - <Link activeClassName="active" to={{ pathname: '/project/quality_profiles', query }}> + <NavLink + to={{ + pathname: '/project/quality_profiles', + search: new URLSearchParams(query).toString() + }}> {translate('project_quality_profiles.page')} - </Link> + </NavLink> </li> ); }; @@ -443,9 +451,10 @@ export class Menu extends React.PureComponent<Props> { } return ( <li key="quality_gate"> - <Link activeClassName="active" to={{ pathname: '/project/quality_gate', query }}> + <NavLink + to={{ pathname: '/project/quality_gate', search: new URLSearchParams(query).toString() }}> {translate('project_quality_gate.page')} - </Link> + </NavLink> </li> ); }; @@ -456,9 +465,9 @@ export class Menu extends React.PureComponent<Props> { } return ( <li key="links"> - <Link activeClassName="active" to={{ pathname: '/project/links', query }}> + <NavLink to={{ pathname: '/project/links', search: new URLSearchParams(query).toString() }}> {translate('project_links.page')} - </Link> + </NavLink> </li> ); }; @@ -469,9 +478,9 @@ export class Menu extends React.PureComponent<Props> { } return ( <li key="permissions"> - <Link activeClassName="active" to={{ pathname: '/project_roles', query }}> + <NavLink to={{ pathname: '/project_roles', search: new URLSearchParams(query).toString() }}> {translate('permissions.page')} - </Link> + </NavLink> </li> ); }; @@ -482,9 +491,13 @@ export class Menu extends React.PureComponent<Props> { } return ( <li key="background_tasks"> - <Link activeClassName="active" to={{ pathname: '/project/background_tasks', query }}> + <NavLink + to={{ + pathname: '/project/background_tasks', + search: new URLSearchParams(query).toString() + }}> {translate('background_tasks.page')} - </Link> + </NavLink> </li> ); }; @@ -495,9 +508,9 @@ export class Menu extends React.PureComponent<Props> { } return ( <li key="update_key"> - <Link activeClassName="active" to={{ pathname: '/project/key', query }}> + <NavLink to={{ pathname: '/project/key', search: new URLSearchParams(query).toString() }}> {translate('update_key.page')} - </Link> + </NavLink> </li> ); }; @@ -508,9 +521,10 @@ export class Menu extends React.PureComponent<Props> { } return ( <li key="webhooks"> - <Link activeClassName="active" to={{ pathname: '/project/webhooks', query }}> + <NavLink + to={{ pathname: '/project/webhooks', search: new URLSearchParams(query).toString() }}> {translate('webhooks.page')} - </Link> + </NavLink> </li> ); }; @@ -534,9 +548,10 @@ export class Menu extends React.PureComponent<Props> { return ( <li key="project_delete"> - <Link activeClassName="active" to={{ pathname: '/project/deletion', query }}> + <NavLink + to={{ pathname: '/project/deletion', search: new URLSearchParams(query).toString() }}> {translate('deletion.page')} - </Link> + </NavLink> </li> ); }; @@ -546,9 +561,7 @@ export class Menu extends React.PureComponent<Props> { const query = { ...baseQuery, qualifier: this.props.component.qualifier }; return ( <li key={key}> - <Link activeClassName="active" to={{ pathname, query }}> - {name} - </Link> + <NavLink to={{ pathname, search: new URLSearchParams(query).toString() }}>{name}</NavLink> </li> ); }; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Breadcrumb-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Breadcrumb-test.tsx.snap index 161aff0449d..e78e2c5452a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Breadcrumb-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Breadcrumb-test.tsx.snap @@ -14,15 +14,11 @@ exports[`should render correctly 1`] = ` /> <Link className="link-no-underline text-ellipsis" - onlyActiveOnIndex={false} - style={Object {}} title="parent-portfolio" to={ Object { "pathname": "/portfolio", - "query": Object { - "id": "parent-portfolio", - }, + "search": "?id=parent-portfolio", } } > diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavLicenseNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavLicenseNotif-test.tsx.snap index 2dd678f30a4..30fcabe4912 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavLicenseNotif-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavLicenseNotif-test.tsx.snap @@ -20,8 +20,6 @@ exports[`renders background task license info correctly 1`] = ` Foo </span> <Link - onlyActiveOnIndex={false} - style={Object {}} to="/admin/extension/license/app" > license.component_navigation.button.LICENSING @@ -55,8 +53,6 @@ exports[`renders correctly for LICENSING_LOC error 1`] = ` Foo </span> <Link - onlyActiveOnIndex={false} - style={Object {}} to="/admin/extension/license/app" > license.component_navigation.button.LICENSING_LOC diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavProjectBindingErrorNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavProjectBindingErrorNotif-test.tsx.snap index 4ef746752eb..86ee8a11613 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavProjectBindingErrorNotif-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavProjectBindingErrorNotif-test.tsx.snap @@ -28,15 +28,10 @@ exports[`should render correctly: project admin 1`] = ` values={ Object { "action": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/project/settings", - "query": Object { - "category": "pull_request_decoration_binding", - "id": "my-project", - }, + "search": "?id=my-project&category=pull_request_decoration_binding", } } > diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap index 87467c85c81..0ff47f8d61a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap @@ -86,21 +86,16 @@ exports[`should disable links if application has inaccessible projects 1`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > deletion.page - </Link> + </NavLink> </li> </ul> } @@ -130,21 +125,16 @@ exports[`should disable links if no analysis has been done 1`] = ` > <NavBarTabs> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, + "search": "?id=foo", } } > overview.page - </Link> + </NavLink> </li> <li> <Tooltip @@ -233,22 +223,16 @@ exports[`should render correctly for security extensions 1`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/extension/component-bar", - "query": Object { - "id": "foo", - "qualifier": "TRK", - }, + "search": "id=foo&qualifier=TRK", } } > ComponentBar - </Link> + </NavLink> </li> </ul> } @@ -266,113 +250,76 @@ exports[`should work for a branch 1`] = ` > <NavBarTabs> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "?id=foo&branch=release", } } > overview.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/issues", - "query": Object { - "branch": "release", - "id": "foo", - "resolved": "false", - }, + "search": "id=foo&branch=release&resolved=false", } } > issues.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/security_hotspots", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > layout.security_hotspots - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/component_measures", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > layout.measures - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/code", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > code.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/activity", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > project_activity.page - </Link> + </NavLink> </li> <Dropdown data-test="extensions" @@ -381,23 +328,16 @@ exports[`should work for a branch 1`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/extension/component-foo", - "query": Object { - "branch": "release", - "id": "foo", - "qualifier": "TRK", - }, + "search": "id=foo&branch=release&qualifier=TRK", } } > ComponentFoo - </Link> + </NavLink> </li> </ul> } @@ -414,112 +354,76 @@ exports[`should work for a branch 1`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/settings", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > project_settings.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/branches", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > project_branch_pull_request.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/baseline", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > project_baseline.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/import_export", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > project_dump.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/webhooks", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > webhooks.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/deletion", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > deletion.page - </Link> + </NavLink> </li> </ul> } @@ -550,113 +454,76 @@ exports[`should work for a branch 2`] = ` > <NavBarTabs> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "?id=foo&branch=release", } } > overview.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/issues", - "query": Object { - "branch": "release", - "id": "foo", - "resolved": "false", - }, + "search": "id=foo&branch=release&resolved=false", } } > issues.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/security_hotspots", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > layout.security_hotspots - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/component_measures", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > layout.measures - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/code", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > code.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/activity", - "query": Object { - "branch": "release", - "id": "foo", - }, + "search": "id=foo&branch=release", } } > project_activity.page - </Link> + </NavLink> </li> <Dropdown data-test="extensions" @@ -665,23 +532,16 @@ exports[`should work for a branch 2`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/extension/component-foo", - "query": Object { - "branch": "release", - "id": "foo", - "qualifier": "TRK", - }, + "search": "id=foo&branch=release&qualifier=TRK", } } > ComponentFoo - </Link> + </NavLink> </li> </ul> } @@ -714,95 +574,64 @@ exports[`should work for pull requests 1`] = ` > <NavBarTabs> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "foo", - "pullRequest": "1001", - }, + "search": "?id=foo&pullRequest=1001", } } > overview.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/issues", - "query": Object { - "id": "foo", - "pullRequest": "1001", - "resolved": "false", - }, + "search": "id=foo&pullRequest=1001&resolved=false", } } > issues.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/security_hotspots", - "query": Object { - "id": "foo", - "pullRequest": "1001", - }, + "search": "id=foo&pullRequest=1001", } } > layout.security_hotspots - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/component_measures", - "query": Object { - "id": "foo", - "pullRequest": "1001", - }, + "search": "id=foo&pullRequest=1001", } } > layout.measures - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/code", - "query": Object { - "id": "foo", - "pullRequest": "1001", - }, + "search": "id=foo&pullRequest=1001", } } > code.page - </Link> + </NavLink> </li> <Dropdown data-test="extensions" @@ -811,23 +640,16 @@ exports[`should work for pull requests 1`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/extension/component-foo", - "query": Object { - "id": "foo", - "pullRequest": "1001", - "qualifier": "TRK", - }, + "search": "id=foo&pullRequest=1001&qualifier=TRK", } } > ComponentFoo - </Link> + </NavLink> </li> </ul> } @@ -846,95 +668,64 @@ exports[`should work for pull requests 2`] = ` > <NavBarTabs> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "foo", - "pullRequest": "1001", - }, + "search": "?id=foo&pullRequest=1001", } } > overview.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/issues", - "query": Object { - "id": "foo", - "pullRequest": "1001", - "resolved": "false", - }, + "search": "id=foo&pullRequest=1001&resolved=false", } } > issues.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/security_hotspots", - "query": Object { - "id": "foo", - "pullRequest": "1001", - }, + "search": "id=foo&pullRequest=1001", } } > layout.security_hotspots - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/component_measures", - "query": Object { - "id": "foo", - "pullRequest": "1001", - }, + "search": "id=foo&pullRequest=1001", } } > layout.measures - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/code", - "query": Object { - "id": "foo", - "pullRequest": "1001", - }, + "search": "id=foo&pullRequest=1001", } } > code.page - </Link> + </NavLink> </li> <Dropdown data-test="extensions" @@ -943,23 +734,16 @@ exports[`should work for pull requests 2`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/extension/component-foo", - "query": Object { - "id": "foo", - "pullRequest": "1001", - "qualifier": "TRK", - }, + "search": "id=foo&pullRequest=1001&qualifier=TRK", } } > ComponentFoo - </Link> + </NavLink> </li> </ul> } @@ -978,107 +762,76 @@ exports[`should work for qualifier: APP, false 1`] = ` > <NavBarTabs> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, + "search": "?id=foo", } } > overview.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/issues", - "query": Object { - "id": "foo", - "resolved": "false", - }, + "search": "id=foo&resolved=false", } } > issues.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/security_hotspots", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > layout.security_hotspots - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/component_measures", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > layout.measures - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/code", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > view_projects.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/activity", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_activity.page - </Link> + </NavLink> </li> </NavBarTabs> <NavBarTabs> @@ -1089,21 +842,16 @@ exports[`should work for qualifier: APP, false 1`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > deletion.page - </Link> + </NavLink> </li> </ul> } @@ -1134,56 +882,40 @@ exports[`should work for qualifier: SVW, false 1`] = ` > <NavBarTabs> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/issues", - "query": Object { - "id": "foo", - "resolved": "false", - }, + "search": "id=foo&resolved=false", } } > issues.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/component_measures", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > layout.measures - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/activity", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_activity.page - </Link> + </NavLink> </li> </NavBarTabs> <NavBarTabs /> @@ -1196,90 +928,64 @@ exports[`should work for qualifier: SVW, true 1`] = ` > <NavBarTabs> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/portfolio", - "query": Object { - "id": "foo", - }, + "search": "?id=foo", } } > overview.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/code", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > portfolio_breakdown.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/issues", - "query": Object { - "id": "foo", - "resolved": "false", - }, + "search": "id=foo&resolved=false", } } > issues.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/component_measures", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > layout.measures - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/activity", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_activity.page - </Link> + </NavLink> </li> </NavBarTabs> <NavBarTabs /> @@ -1292,107 +998,76 @@ exports[`should work for qualifier: TRK, false 1`] = ` > <NavBarTabs> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, + "search": "?id=foo", } } > overview.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/issues", - "query": Object { - "id": "foo", - "resolved": "false", - }, + "search": "id=foo&resolved=false", } } > issues.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/security_hotspots", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > layout.security_hotspots - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/component_measures", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > layout.measures - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/code", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > code.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/activity", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_activity.page - </Link> + </NavLink> </li> </NavBarTabs> <NavBarTabs> @@ -1403,106 +1078,76 @@ exports[`should work for qualifier: TRK, false 1`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/settings", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_settings.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/branches", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_branch_pull_request.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/baseline", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_baseline.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/import_export", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_dump.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/webhooks", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > webhooks.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > deletion.page - </Link> + </NavLink> </li> </ul> } @@ -1533,56 +1178,40 @@ exports[`should work for qualifier: VW, false 1`] = ` > <NavBarTabs> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/issues", - "query": Object { - "id": "foo", - "resolved": "false", - }, + "search": "id=foo&resolved=false", } } > issues.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/component_measures", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > layout.measures - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/activity", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_activity.page - </Link> + </NavLink> </li> </NavBarTabs> <NavBarTabs> @@ -1593,21 +1222,16 @@ exports[`should work for qualifier: VW, false 1`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > deletion.page - </Link> + </NavLink> </li> </ul> } @@ -1625,90 +1249,64 @@ exports[`should work for qualifier: VW, true 1`] = ` > <NavBarTabs> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/portfolio", - "query": Object { - "id": "foo", - }, + "search": "?id=foo", } } > overview.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/code", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > portfolio_breakdown.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/issues", - "query": Object { - "id": "foo", - "resolved": "false", - }, + "search": "id=foo&resolved=false", } } > issues.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/component_measures", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > layout.measures - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/activity", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_activity.page - </Link> + </NavLink> </li> </NavBarTabs> <NavBarTabs> @@ -1719,21 +1317,16 @@ exports[`should work for qualifier: VW, true 1`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > deletion.page - </Link> + </NavLink> </li> </ul> } @@ -1753,22 +1346,16 @@ exports[`should work with extensions 1`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/extension/component-foo", - "query": Object { - "id": "foo", - "qualifier": "TRK", - }, + "search": "id=foo&qualifier=TRK", } } > ComponentFoo - </Link> + </NavLink> </li> </ul> } @@ -1786,124 +1373,88 @@ exports[`should work with extensions 2`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/settings", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_settings.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/branches", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_branch_pull_request.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/baseline", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_baseline.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/admin/extension/foo", - "query": Object { - "id": "foo", - "qualifier": "TRK", - }, + "search": "id=foo&qualifier=TRK", } } > Foo - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/import_export", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_dump.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/webhooks", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > webhooks.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > deletion.page - </Link> + </NavLink> </li> </ul> } @@ -1921,40 +1472,28 @@ exports[`should work with multiple extensions 1`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/extension/component-foo", - "query": Object { - "id": "foo", - "qualifier": "TRK", - }, + "search": "id=foo&qualifier=TRK", } } > ComponentFoo - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/extension/component-bar", - "query": Object { - "id": "foo", - "qualifier": "TRK", - }, + "search": "id=foo&qualifier=TRK", } } > ComponentBar - </Link> + </NavLink> </li> </ul> } @@ -1972,142 +1511,100 @@ exports[`should work with multiple extensions 2`] = ` className="menu" > <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/settings", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_settings.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/branches", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_branch_pull_request.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/baseline", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_baseline.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/admin/extension/foo", - "query": Object { - "id": "foo", - "qualifier": "TRK", - }, + "search": "id=foo&qualifier=TRK", } } > Foo - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/admin/extension/bar", - "query": Object { - "id": "foo", - "qualifier": "TRK", - }, + "search": "id=foo&qualifier=TRK", } } > Bar - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/import_export", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > project_dump.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/webhooks", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > webhooks.page - </Link> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to={ Object { "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, + "search": "id=foo", } } > deletion.page - </Link> + </NavLink> </li> </ul> } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx index 9512d929c2a..32ec2231bf3 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import DocumentationTooltip from '../../../../../components/common/DocumentationTooltip'; import HelpTooltip from '../../../../../components/controls/HelpTooltip'; import BranchLikeIcon from '../../../../../components/icons/BranchLikeIcon'; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx index bbfb7738e85..8e5f1127d15 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { DropdownOverlay } from '../../../../../components/controls/Dropdown'; import SearchBox from '../../../../../components/controls/SearchBox'; import { Router, withRouter } from '../../../../../components/hoc/withRouter'; @@ -30,7 +30,7 @@ import { } from '../../../../../helpers/branch-like'; import { KeyboardKeys } from '../../../../../helpers/keycodes'; import { translate } from '../../../../../helpers/l10n'; -import { getBranchLikeUrl } from '../../../../../helpers/urls'; +import { getBranchLikeUrl, queryToSearch } from '../../../../../helpers/urls'; import { BranchLike, BranchLikeTree } from '../../../../../types/branch-like'; import { ComponentQualifier } from '../../../../../types/component'; import { Component } from '../../../../../types/types'; @@ -42,7 +42,7 @@ interface Props { component: Component; currentBranchLike: BranchLike; onClose: () => void; - router: Pick<Router, 'push'>; + router: Router; } interface State { @@ -190,7 +190,7 @@ export class Menu extends React.PureComponent<Props, State> { <div className="hint-container text-right"> <Link onClick={() => onClose()} - to={{ pathname: '/project/branches', query: { id: component.key } }}> + to={{ pathname: '/project/branches', search: queryToSearch({ id: component.key }) }}> {translate('branch_like_navigation.manage')} </Link> </div> diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx index 261de2df20e..b11f3a30f7a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx @@ -19,7 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import SearchBox from '../../../../../../components/controls/SearchBox'; import { KeyboardKeys } from '../../../../../../helpers/keycodes'; import { @@ -29,6 +29,7 @@ import { import { mockComponent } from '../../../../../../helpers/mocks/component'; import { mockRouter } from '../../../../../../helpers/testMocks'; import { click, mockEvent } from '../../../../../../helpers/testUtils'; +import { queryToSearch } from '../../../../../../helpers/urls'; import { Menu } from '../Menu'; import { MenuItemList } from '../MenuItemList'; @@ -67,10 +68,10 @@ it('should change url and close menu when an element is selected', () => { expect(onClose).toHaveBeenCalled(); expect(push).toHaveBeenCalledWith( expect.objectContaining({ - query: { + search: queryToSearch({ id: component.key, pullRequest: pr.key - } + }) }) ); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap index 1548a5317c3..e45e2c250b4 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap @@ -77,14 +77,10 @@ exports[`applications should render correctly when there is only one branch and className="spacer-top spacer-bottom" /> <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/project/admin/extension/developer-server/application-console", - "query": Object { - "id": "my-project", - }, + "search": "?id=my-project", } } > diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap index ce2339c7560..df18a2f2f5a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap @@ -148,14 +148,10 @@ exports[`should render correctly 1`] = ` > <Link onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/project/branches", - "query": Object { - "id": "my-project", - }, + "search": "?id=my-project", } } > @@ -313,14 +309,10 @@ exports[`should render correctly with no current branch like 1`] = ` > <Link onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/project/branches", - "query": Object { - "id": "my-project", - }, + "search": "?id=my-project", } } > diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/utils-test.ts b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/utils-test.ts index 37913495f2c..ecee525da3c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/utils-test.ts @@ -17,14 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Location } from '../../../../../../../helpers/urls'; +import { Location } from '../../../../../../../components/hoc/withRouter'; import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from '../utils'; jest.mock('../../../../../../../helpers/urls', () => ({ ...jest.requireActual('../../../../../../../helpers/urls'), getHostUrl: () => 'host', - getPathUrlAsString: (o: Location) => - `host${o.pathname}?id=${o.query ? o.query.id : ''}&branch=${o.query ? o.query.branch : ''}` + getPathUrlAsString: (o: Location) => `host${o.pathname}${o.search}` })); const options: BadgeOptions = { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/utils.ts b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/utils.ts index 4e866f51747..8c8dcc73253 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/utils.ts +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/utils.ts @@ -43,27 +43,27 @@ export function getBadgeSnippet(type: BadgeType, options: BadgeOptions, token: s if (format === 'url') { return url; - } else { - let label; - let projectUrl; + } - switch (type) { - case BadgeType.measure: - label = getLocalizedMetricName({ key: metric }); - break; - case BadgeType.qualityGate: - default: - label = 'Quality gate'; - break; - } + let label; + let projectUrl; - if (project) { - projectUrl = getPathUrlAsString(getProjectUrl(project, branch), false); - } + switch (type) { + case BadgeType.measure: + label = getLocalizedMetricName({ key: metric }); + break; + case BadgeType.qualityGate: + default: + label = 'Quality gate'; + break; + } - const mdImage = `![${label}](${url})`; - return projectUrl ? `[${mdImage}](${projectUrl})` : mdImage; + if (project) { + projectUrl = getPathUrlAsString(getProjectUrl(project, branch), false); } + + const mdImage = `![${label}](${url})`; + return projectUrl ? `[${mdImage}](${projectUrl})` : mdImage; } export function getBadgeUrl( diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityGate.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityGate.tsx index 84333914751..379997fccab 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityGate.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityGate.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { translate } from '../../../../../../helpers/l10n'; import { getQualityGateUrl } from '../../../../../../helpers/urls'; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityProfiles.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityProfiles.tsx index 0f0fc343ff5..de1836db9fe 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityProfiles.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityProfiles.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { searchRules } from '../../../../../../api/rules'; import Tooltip from '../../../../../../components/controls/Tooltip'; import { translate, translateWithParameters } from '../../../../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaQualityProfiles-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaQualityProfiles-test.tsx.snap index 2e585fd377d..3f7561213ee 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaQualityProfiles-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaQualityProfiles-test.tsx.snap @@ -43,15 +43,10 @@ exports[`should render correctly 1`] = ` ) </span> <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/profiles/show", - "query": Object { - "language": "css", - "name": "name", - }, + "search": "?name=name&language=css", } } > @@ -110,15 +105,10 @@ exports[`should render correctly 2`] = ` ) </span> <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/profiles/show", - "query": Object { - "language": "css", - "name": "name", - }, + "search": "?name=name&language=css", } } > diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.tsx index 6a952f3cc4b..5e0231911bc 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { translate } from '../../../../helpers/l10n'; import { getBaseUrl } from '../../../../helpers/system'; import { AppState } from '../../../../types/appstate'; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx index e608efc5955..866004c5051 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx @@ -19,7 +19,7 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link, NavLink } from 'react-router-dom'; import { isMySet } from '../../../../apps/issues/utils'; import Dropdown from '../../../../components/controls/Dropdown'; import DropdownIcon from '../../../../components/icons/DropdownIcon'; @@ -37,6 +37,7 @@ interface Props { location: { pathname: string }; } +const ACTIVE_CLASS_NAME = 'active'; export class GlobalNavMenu extends React.PureComponent<Props> { renderProjects() { const active = @@ -55,9 +56,9 @@ export class GlobalNavMenu extends React.PureComponent<Props> { renderPortfolios() { return ( <li> - <Link activeClassName="active" to="/portfolios"> + <NavLink className={({ isActive }) => (isActive ? ACTIVE_CLASS_NAME : '')} to="/portfolios"> {translate('portfolios.page')} - </Link> + </NavLink> </li> ); } @@ -65,13 +66,13 @@ export class GlobalNavMenu extends React.PureComponent<Props> { renderIssuesLink() { const active = this.props.location.pathname.startsWith('/issues'); - const query = - this.props.currentUser.isLoggedIn && isMySet() - ? { resolved: 'false', myIssues: 'true' } - : { resolved: 'false' }; + const search = (this.props.currentUser.isLoggedIn && isMySet() + ? new URLSearchParams({ resolved: 'false', myIssues: 'true' }) + : new URLSearchParams({ resolved: 'false' }) + ).toString(); return ( <li> - <Link className={classNames({ active })} to={{ pathname: '/issues', query }}> + <Link className={classNames({ active })} to={{ pathname: '/issues', search }}> {translate('issues.page')} </Link> </li> @@ -81,9 +82,11 @@ export class GlobalNavMenu extends React.PureComponent<Props> { renderRulesLink() { return ( <li> - <Link activeClassName="active" to="/coding_rules"> + <NavLink + className={({ isActive }) => (isActive ? ACTIVE_CLASS_NAME : '')} + to="/coding_rules"> {translate('coding_rules.page')} - </Link> + </NavLink> </li> ); } @@ -91,9 +94,9 @@ export class GlobalNavMenu extends React.PureComponent<Props> { renderProfilesLink() { return ( <li> - <Link activeClassName="active" to="/profiles"> + <NavLink className={({ isActive }) => (isActive ? ACTIVE_CLASS_NAME : '')} to="/profiles"> {translate('quality_profiles.page')} - </Link> + </NavLink> </li> ); } @@ -101,9 +104,11 @@ export class GlobalNavMenu extends React.PureComponent<Props> { renderQualityGatesLink() { return ( <li> - <Link activeClassName="active" to={getQualityGatesUrl()}> + <NavLink + className={({ isActive }) => (isActive ? ACTIVE_CLASS_NAME : '')} + to={getQualityGatesUrl()}> {translate('quality_gates.page')} - </Link> + </NavLink> </li> ); } @@ -115,9 +120,9 @@ export class GlobalNavMenu extends React.PureComponent<Props> { return ( <li> - <Link activeClassName="active" to="/admin"> + <NavLink className={({ isActive }) => (isActive ? ACTIVE_CLASS_NAME : '')} to="/admin"> {translate('layout.settings')} - </Link> + </NavLink> </li> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx index 91079564dbe..3a74eb08d06 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import Dropdown from '../../../../components/controls/Dropdown'; import { Router, withRouter } from '../../../../components/hoc/withRouter'; import Avatar from '../../../../components/ui/Avatar'; @@ -29,7 +29,7 @@ import { rawSizes } from '../../../theme'; interface Props { currentUser: CurrentUser; - router: Pick<Router, 'push'>; + router: Router; } export class GlobalNavUser extends React.PureComponent<Props> { diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx index 1a56df67d0f..5afe85b9f35 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx @@ -17,9 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; +import { screen } from '@testing-library/dom'; import * as React from 'react'; -import { mockAppState } from '../../../../../helpers/testMocks'; +import { mockAppState, mockCurrentUser } from '../../../../../helpers/testMocks'; +import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; import { GlobalNavMenu } from '../GlobalNavMenu'; it('should work with extensions', () => { @@ -27,13 +28,12 @@ it('should work with extensions', () => { globalPages: [{ key: 'foo', name: 'Foo' }], qualifiers: ['TRK'] }); + const currentUser = { isLoggedIn: false }; - const wrapper = shallow( - <GlobalNavMenu appState={appState} currentUser={currentUser} location={{ pathname: '' }} /> - ); - expect(wrapper.find('Dropdown')).toMatchSnapshot(); + renderGlobalNavMenu({ appState, currentUser }); + expect(screen.getByText('more')).toBeInTheDocument(); }); it('should show administration menu if the user has the rights', () => { @@ -45,8 +45,17 @@ it('should show administration menu if the user has the rights', () => { const currentUser = { isLoggedIn: false }; - const wrapper = shallow( - <GlobalNavMenu appState={appState} currentUser={currentUser} location={{ pathname: '' }} /> - ); - expect(wrapper).toMatchSnapshot(); + + renderGlobalNavMenu({ appState, currentUser }); + expect(screen.getByText('layout.settings')).toBeInTheDocument(); }); + +function renderGlobalNavMenu({ + appState = mockAppState(), + currentUser = mockCurrentUser(), + location = { pathname: '' } +}: Partial<GlobalNavMenu['props']>) { + renderComponent( + <GlobalNavMenu appState={appState} currentUser={currentUser} location={location} /> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavBranding-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavBranding-test.tsx.snap index 5419fd45cbb..9fe279984b6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavBranding-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavBranding-test.tsx.snap @@ -3,8 +3,6 @@ exports[`should render correctly: default 1`] = ` <Link className="navbar-brand" - onlyActiveOnIndex={false} - style={Object {}} to="/" > <img @@ -20,8 +18,6 @@ exports[`should render correctly: default 1`] = ` exports[`should render correctly: with logo 1`] = ` <Link className="navbar-brand" - onlyActiveOnIndex={false} - style={Object {}} to="/" > <img @@ -37,8 +33,6 @@ exports[`should render correctly: with logo 1`] = ` exports[`should render correctly: with logo and width 1`] = ` <Link className="navbar-brand" - onlyActiveOnIndex={false} - style={Object {}} to="/" > <img diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap deleted file mode 100644 index e59e925873f..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap +++ /dev/null @@ -1,102 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should show administration menu if the user has the rights 1`] = ` -<ul - className="global-navbar-menu" -> - <li> - <Link - className="" - onlyActiveOnIndex={false} - style={Object {}} - to="/projects" - > - projects.page - </Link> - </li> - <li> - <Link - className="" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/issues", - "query": Object { - "resolved": "false", - }, - } - } - > - issues.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/coding_rules" - > - coding_rules.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/profiles" - > - quality_profiles.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/quality_gates", - } - } - > - quality_gates.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/admin" - > - layout.settings - </Link> - </li> -</ul> -`; - -exports[`should work with extensions 1`] = ` -<Dropdown - overlay={ - <ul - className="menu" - > - <li> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/extension/foo" - > - Foo - </Link> - </li> - </ul> - } - tagName="li" -> - <Component /> -</Dropdown> -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap index f131dfc9555..40956326a7d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap @@ -36,8 +36,6 @@ exports[`should render the right interface for logged in user 1`] = ` /> <li> <Link - onlyActiveOnIndex={false} - style={Object {}} to="/account" > my_account.page diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx index 0dd3a155f42..3a1381823d3 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx @@ -19,8 +19,9 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { IndexLink, Link } from 'react-router'; +import { Location, NavLink } from 'react-router-dom'; import Dropdown from '../../../../components/controls/Dropdown'; +import withLocation from '../../../../components/hoc/withLocation'; import DropdownIcon from '../../../../components/icons/DropdownIcon'; import ContextNavBar from '../../../../components/ui/ContextNavBar'; import NavBarTabs from '../../../../components/ui/NavBarTabs'; @@ -37,19 +38,20 @@ interface Props { extensions: Extension[]; fetchPendingPlugins: () => void; fetchSystemStatus: () => void; + location: Location; pendingPlugins: PendingPluginResult; systemStatus: SysStatus; } -export default class SettingsNav extends React.PureComponent<Props> { +export class SettingsNav extends React.PureComponent<Props> { static defaultProps = { extensions: [] }; - isSomethingActive(urls: string[]): boolean { - const path = window.location.pathname; + isSomethingActive = (urls: string[]) => { + const path = this.props.location.pathname; return urls.some((url: string) => path.indexOf(getBaseUrl() + url) === 0); - } + }; isSecurityActive() { const urls = [ @@ -84,9 +86,7 @@ export default class SettingsNav extends React.PureComponent<Props> { renderExtension = ({ key, name }: Extension) => { return ( <li key={key}> - <Link activeClassName="active" to={`/admin/extension/${key}`}> - {name} - </Link> + <NavLink to={`/admin/extension/${key}`}>{name}</NavLink> </li> ); }; @@ -100,19 +100,19 @@ export default class SettingsNav extends React.PureComponent<Props> { overlay={ <ul className="menu"> <li> - <IndexLink activeClassName="active" to="/admin/settings"> + <NavLink end={true} to="/admin/settings"> {translate('settings.page')} - </IndexLink> + </NavLink> </li> <li> - <IndexLink activeClassName="active" to="/admin/settings/encryption"> + <NavLink end={true} to="/admin/settings/encryption"> {translate('property.category.security.encryption')} - </IndexLink> + </NavLink> </li> <li> - <IndexLink activeClassName="active" to="/admin/webhooks"> + <NavLink end={true} to="/admin/webhooks"> {translate('webhooks.page')} - </IndexLink> + </NavLink> </li> {extensionsWithoutSupport.map(this.renderExtension)} </ul> @@ -150,14 +150,14 @@ export default class SettingsNav extends React.PureComponent<Props> { overlay={ <ul className="menu"> <li> - <IndexLink activeClassName="active" to="/admin/projects_management"> + <NavLink end={true} to="/admin/projects_management"> {translate('management')} - </IndexLink> + </NavLink> </li> <li> - <IndexLink activeClassName="active" to="/admin/background_tasks"> + <NavLink end={true} to="/admin/background_tasks"> {translate('background_tasks.page')} - </IndexLink> + </NavLink> </li> </ul> } @@ -184,24 +184,24 @@ export default class SettingsNav extends React.PureComponent<Props> { overlay={ <ul className="menu"> <li> - <IndexLink activeClassName="active" to="/admin/users"> + <NavLink end={true} to="/admin/users"> {translate('users.page')} - </IndexLink> + </NavLink> </li> <li> - <IndexLink activeClassName="active" to="/admin/groups"> + <NavLink end={true} to="/admin/groups"> {translate('user_groups.page')} - </IndexLink> + </NavLink> </li> <li> - <IndexLink activeClassName="active" to="/admin/permissions"> + <NavLink end={true} to="/admin/permissions"> {translate('global_permissions.page')} - </IndexLink> + </NavLink> </li> <li> - <IndexLink activeClassName="active" to="/admin/permission_templates"> + <NavLink end={true} to="/admin/permission_templates"> {translate('permission_templates')} - </IndexLink> + </NavLink> </li> </ul> } @@ -262,30 +262,30 @@ export default class SettingsNav extends React.PureComponent<Props> { {this.renderProjectsTab()} <li> - <IndexLink activeClassName="active" to="/admin/system"> + <NavLink end={true} to="/admin/system"> {translate('sidebar.system')} - </IndexLink> + </NavLink> </li> <li> - <IndexLink activeClassName="active" to="/admin/marketplace"> + <NavLink end={true} to="/admin/marketplace"> {translate('marketplace.page')} - </IndexLink> + </NavLink> </li> {hasGovernanceExtension && ( <li> - <IndexLink activeClassName="active" to="/admin/audit"> + <NavLink end={true} to="/admin/audit"> {translate('audit_logs.page')} - </IndexLink> + </NavLink> </li> )} {hasSupportExtension && ( <li> - <IndexLink activeClassName="active" to="/admin/extension/license/support"> + <NavLink end={true} to="/admin/extension/license/support"> {translate('support')} - </IndexLink> + </NavLink> </li> )} </NavBarTabs> @@ -293,3 +293,5 @@ export default class SettingsNav extends React.PureComponent<Props> { ); } } + +export default withLocation(SettingsNav); diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SystemRestartNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SystemRestartNotif.tsx index b127a6f2f09..b16dca9e9cb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SystemRestartNotif.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SystemRestartNotif.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { Alert } from '../../../../components/ui/Alert'; import { translate } from '../../../../helpers/l10n'; import { getInstance } from '../../../../helpers/system'; @@ -32,11 +32,7 @@ export default function SystemRestartNotif() { id="system.instance_restarting" values={{ instance: getInstance(), - link: ( - <Link to={{ pathname: '/admin/background_tasks' }}> - {translate('background_tasks.page')} - </Link> - ) + link: <Link to="/admin/background_tasks">{translate('background_tasks.page')}</Link> }} /> </Alert> diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx index 7b96b2cc92a..c2264d9e1d6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx @@ -19,8 +19,9 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockLocation } from '../../../../../helpers/testMocks'; import { AdminPageExtension } from '../../../../../types/extension'; -import SettingsNav from '../SettingsNav'; +import { SettingsNav } from '../SettingsNav'; it('should work with extensions', () => { const wrapper = shallowRender(); @@ -65,6 +66,7 @@ function shallowRender(props: Partial<SettingsNav['props']> = {}) { extensions={[{ key: 'foo', name: 'Foo' }]} fetchPendingPlugins={jest.fn()} fetchSystemStatus={jest.fn()} + location={mockLocation()} pendingPlugins={{ installing: [], removing: [], updating: [] }} systemStatus="UP" {...props} diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap index a93612af5d0..029675a85cb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap @@ -43,38 +43,35 @@ exports[`should render correctly when governance is active 1`] = ` className="menu" > <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/settings" > settings.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/settings/encryption" > property.category.security.encryption - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/webhooks" > webhooks.page - </IndexLink> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to="/admin/extension/governance/views_console" > governance - </Link> + </NavLink> </li> </ul> } @@ -88,36 +85,36 @@ exports[`should render correctly when governance is active 1`] = ` className="menu" > <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/users" > users.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/groups" > user_groups.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/permissions" > global_permissions.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/permission_templates" > permission_templates - </IndexLink> + </NavLink> </li> </ul> } @@ -131,20 +128,20 @@ exports[`should render correctly when governance is active 1`] = ` className="menu" > <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/projects_management" > management - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/background_tasks" > background_tasks.page - </IndexLink> + </NavLink> </li> </ul> } @@ -153,28 +150,28 @@ exports[`should render correctly when governance is active 1`] = ` <Component /> </Dropdown> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/system" > sidebar.system - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/marketplace" > marketplace.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/audit" > audit_logs.page - </IndexLink> + </NavLink> </li> </NavBarTabs> </ContextNavBar> @@ -199,38 +196,35 @@ exports[`should work with extensions 1`] = ` className="menu" > <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/settings" > settings.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/settings/encryption" > property.category.security.encryption - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/webhooks" > webhooks.page - </IndexLink> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to="/admin/extension/foo" > Foo - </Link> + </NavLink> </li> </ul> } @@ -244,36 +238,36 @@ exports[`should work with extensions 1`] = ` className="menu" > <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/users" > users.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/groups" > user_groups.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/permissions" > global_permissions.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/permission_templates" > permission_templates - </IndexLink> + </NavLink> </li> </ul> } @@ -287,20 +281,20 @@ exports[`should work with extensions 1`] = ` className="menu" > <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/projects_management" > management - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/background_tasks" > background_tasks.page - </IndexLink> + </NavLink> </li> </ul> } @@ -309,20 +303,20 @@ exports[`should work with extensions 1`] = ` <Component /> </Dropdown> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/system" > sidebar.system - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/marketplace" > marketplace.page - </IndexLink> + </NavLink> </li> </NavBarTabs> </ContextNavBar> @@ -336,38 +330,35 @@ Array [ className="menu" > <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/settings" > settings.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/settings/encryption" > property.category.security.encryption - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/webhooks" > webhooks.page - </IndexLink> + </NavLink> </li> <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} + <NavLink to="/admin/extension/foo" > Foo - </Link> + </NavLink> </li> </ul> } @@ -381,36 +372,36 @@ Array [ className="menu" > <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/users" > users.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/groups" > user_groups.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/permissions" > global_permissions.page - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/permission_templates" > permission_templates - </IndexLink> + </NavLink> </li> </ul> } @@ -424,20 +415,20 @@ Array [ className="menu" > <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/projects_management" > management - </IndexLink> + </NavLink> </li> <li> - <IndexLink - activeClassName="active" + <NavLink + end={true} to="/admin/background_tasks" > background_tasks.page - </IndexLink> + </NavLink> </li> </ul> } diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SystemRestartNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SystemRestartNotif-test.tsx.snap index b6b1713bc1d..1bec0d5aa3a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SystemRestartNotif-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SystemRestartNotif-test.tsx.snap @@ -12,13 +12,7 @@ exports[`should render correctly 1`] = ` Object { "instance": undefined, "link": <Link - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/admin/background_tasks", - } - } + to="/admin/background_tasks" > background_tasks.page </Link>, diff --git a/server/sonar-web/src/main/js/app/components/search/Search.tsx b/server/sonar-web/src/main/js/app/components/search/Search.tsx index 03b0ea8d70c..cc7d82729ba 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.tsx +++ b/server/sonar-web/src/main/js/app/components/search/Search.tsx @@ -20,11 +20,11 @@ import { debounce, keyBy, uniqBy } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { withRouter, WithRouterProps } from 'react-router'; import { getSuggestions } from '../../../api/components'; import { DropdownOverlay } from '../../../components/controls/Dropdown'; import OutsideClickHandler from '../../../components/controls/OutsideClickHandler'; import SearchBox from '../../../components/controls/SearchBox'; +import { Router, withRouter } from '../../../components/hoc/withRouter'; import ClockIcon from '../../../components/icons/ClockIcon'; import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; @@ -42,6 +42,10 @@ import { ComponentResult, More, Results, sortQualifiers } from './utils'; const SearchResults = lazyLoadComponent(() => import('./SearchResults')); const SearchResult = lazyLoadComponent(() => import('./SearchResult')); +interface Props { + router: Router; +} + interface State { loading: boolean; loadingMore?: string; @@ -54,13 +58,13 @@ interface State { shortQuery: boolean; } -export class Search extends React.PureComponent<WithRouterProps, State> { +export class Search extends React.PureComponent<Props, State> { input?: HTMLInputElement | null; node?: HTMLElement | null; nodes: Dict<HTMLElement>; mounted = false; - constructor(props: WithRouterProps) { + constructor(props: Props) { super(props); this.nodes = {}; this.search = debounce(this.search, 250); @@ -80,7 +84,7 @@ export class Search extends React.PureComponent<WithRouterProps, State> { document.addEventListener('keydown', this.handleSKeyDown); } - componentDidUpdate(_prevProps: WithRouterProps, prevState: State) { + componentDidUpdate(_prevProps: Props, prevState: State) { if (prevState.selected !== this.state.selected) { this.scrollToSelected(); } @@ -403,4 +407,4 @@ export class Search extends React.PureComponent<WithRouterProps, State> { } } -export default withRouter<{}>(Search); +export default withRouter(Search); diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx b/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx index 183fd66509e..16f946915d7 100644 --- a/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx +++ b/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import Tooltip from '../../../components/controls/Tooltip'; import ClockIcon from '../../../components/icons/ClockIcon'; import FavoriteIcon from '../../../components/icons/FavoriteIcon'; diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx index 13935091084..9a08c0d4980 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx @@ -22,6 +22,7 @@ import * as React from 'react'; import { KeyboardKeys } from '../../../../helpers/keycodes'; import { mockRouter } from '../../../../helpers/testMocks'; import { elementKeydown, keydown } from '../../../../helpers/testUtils'; +import { queryToSearch } from '../../../../helpers/urls'; import { ComponentQualifier } from '../../../../types/component'; import { Search } from '../Search'; @@ -54,7 +55,10 @@ it('opens selected project on enter', () => { }); elementKeydown(form.find('SearchBox'), KeyboardKeys.Enter); - expect(router.push).toBeCalledWith({ pathname: '/dashboard', query: { id: selectedKey } }); + expect(router.push).toBeCalledWith({ + pathname: '/dashboard', + search: queryToSearch({ id: selectedKey }) + }); }); it('opens selected portfolio on enter', () => { @@ -70,7 +74,10 @@ it('opens selected portfolio on enter', () => { }); elementKeydown(form.find('SearchBox'), KeyboardKeys.Enter); - expect(router.push).toBeCalledWith({ pathname: '/portfolio', query: { id: selectedKey } }); + expect(router.push).toBeCalledWith({ + pathname: '/portfolio', + search: queryToSearch({ id: selectedKey }) + }); }); it('opens selected subportfolio on enter', () => { @@ -86,7 +93,10 @@ it('opens selected subportfolio on enter', () => { }); elementKeydown(form.find('SearchBox'), KeyboardKeys.Enter); - expect(router.push).toBeCalledWith({ pathname: '/portfolio', query: { id: selectedKey } }); + expect(router.push).toBeCalledWith({ + pathname: '/portfolio', + search: queryToSearch({ id: selectedKey }) + }); }); it('shows warning about short input', () => { diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap index d8f8d4dcb7e..d4e19eaf97b 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap @@ -13,14 +13,10 @@ exports[`renders favorite 1`] = ` <Link data-key="foo" onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, + "search": "?id=foo", } } > @@ -64,14 +60,10 @@ exports[`renders match 1`] = ` <Link data-key="foo" onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, + "search": "?id=foo", } } > @@ -114,14 +106,10 @@ exports[`renders projects 1`] = ` <Link data-key="qwe" onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "qwe", - }, + "search": "?id=qwe", } } > @@ -169,14 +157,10 @@ exports[`renders recently browsed 1`] = ` <Link data-key="foo" onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, + "search": "?id=foo", } } > @@ -219,14 +203,10 @@ exports[`renders selected 1`] = ` <Link data-key="foo" onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, + "search": "?id=foo", } } > @@ -267,14 +247,10 @@ exports[`renders selected 2`] = ` <Link data-key="foo" onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, + "search": "?id=foo", } } > diff --git a/server/sonar-web/src/main/js/app/index.ts b/server/sonar-web/src/main/js/app/index.ts index 85532d23a70..a665c02f376 100644 --- a/server/sonar-web/src/main/js/app/index.ts +++ b/server/sonar-web/src/main/js/app/index.ts @@ -19,9 +19,11 @@ */ import { installExtensionsHandler, installWebAnalyticsHandler } from '../helpers/extensionsHandler'; import { loadL10nBundle } from '../helpers/l10nBundle'; -import { parseJSON, request } from '../helpers/request'; +import { HttpStatus, parseJSON, request } from '../helpers/request'; import { getBaseUrl, getSystemStatus } from '../helpers/system'; import { AppState } from '../types/appstate'; +import { L10nBundle } from '../types/l10nBundle'; +import { CurrentUser } from '../types/users'; import './styles/sonar.ts'; installWebAnalyticsHandler(); @@ -29,12 +31,12 @@ installWebAnalyticsHandler(); if (isMainApp()) { installExtensionsHandler(); - Promise.all([loadL10nBundle(), loadUser(), loadAppState(), loadApp()]).then( + loadAll(loadAppState, loadUser).then( ([l10nBundle, user, appState, startReactApp]) => { startReactApp(l10nBundle.locale, appState, user); }, error => { - if (isResponse(error) && error.status === 401) { + if (isResponse(error) && error.status === HttpStatus.Unauthorized) { redirectToLogin(); } else { logError(error); @@ -44,24 +46,19 @@ if (isMainApp()) { } else { // login, maintenance or setup pages - const appStatePromise: Promise<AppState> = new Promise(resolve => { - loadAppState() - .then(data => { - resolve(data); - }) - .catch(() => { - resolve({ - edition: undefined, - productionDatabase: true, - qualifiers: [], - settings: {}, - version: '' - }); - }); - }); + const appStateLoader = () => + loadAppState().catch(() => { + return { + edition: undefined, + productionDatabase: true, + qualifiers: [], + settings: {}, + version: '' + }; + }); - Promise.all([loadL10nBundle(), appStatePromise, loadApp()]).then( - ([l10nBundle, appState, startReactApp]) => { + loadAll(appStateLoader).then( + ([l10nBundle, _user, appState, startReactApp]) => { startReactApp(l10nBundle.locale, appState); }, error => { @@ -70,6 +67,28 @@ if (isMainApp()) { ); } +async function loadAll( + appStateLoader: () => Promise<AppState>, + userLoader?: () => Promise<CurrentUser | undefined> +): Promise< + [ + Required<L10nBundle>, + CurrentUser | undefined, + AppState, + (lang: string, appState: AppState, currentUser?: CurrentUser) => void + ] +> { + const [l10nBundle, user, appState] = await Promise.all([ + loadL10nBundle(), + userLoader ? userLoader() : undefined, + appStateLoader() + ]); + + const startReactApp = await loadApp(); + + return [l10nBundle, user, appState, startReactApp]; +} + function loadUser() { return request('/api/users/current') .submit() diff --git a/server/sonar-web/src/main/js/app/utils/NavigateWithParams.tsx b/server/sonar-web/src/main/js/app/utils/NavigateWithParams.tsx new file mode 100644 index 00000000000..6550da209a3 --- /dev/null +++ b/server/sonar-web/src/main/js/app/utils/NavigateWithParams.tsx @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Navigate, Params, useLocation, useParams, useSearchParams } from 'react-router-dom'; +import { Dict } from '../../types/types'; + +export interface NavigateWithParamsProps { + pathname: string; + transformParams: (params: Params) => Dict<string>; +} + +export default function NavigateWithParams({ pathname, transformParams }: NavigateWithParamsProps) { + const urlParams = useParams(); + const location = useLocation(); + const [searchParams] = useSearchParams(); + + /* Append transformed path params to search params */ + const transformedParams = transformParams(urlParams); + Object.keys(transformedParams).forEach(key => { + searchParams.append(key, transformedParams[key]); + }); + + return ( + <Navigate + to={{ pathname, search: searchParams.toString(), hash: location.hash }} + replace={true} + /> + ); +} diff --git a/server/sonar-web/src/main/js/app/utils/NavigateWithSearchAndHash.tsx b/server/sonar-web/src/main/js/app/utils/NavigateWithSearchAndHash.tsx new file mode 100644 index 00000000000..048e5451867 --- /dev/null +++ b/server/sonar-web/src/main/js/app/utils/NavigateWithSearchAndHash.tsx @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; + +export interface NavigateWithSearchAndHashProps { + pathname: string; +} + +export default function NavigateWithSearchAndHash({ pathname }: NavigateWithSearchAndHashProps) { + const location = useLocation(); + + return <Navigate to={{ ...location, pathname }} replace={true} />; +} diff --git a/server/sonar-web/src/main/js/app/utils/__tests__/NavigateWithParams-test.tsx b/server/sonar-web/src/main/js/app/utils/__tests__/NavigateWithParams-test.tsx new file mode 100644 index 00000000000..b4f47021d50 --- /dev/null +++ b/server/sonar-web/src/main/js/app/utils/__tests__/NavigateWithParams-test.tsx @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter, Params, Route, Routes } from 'react-router-dom'; +import { CatchAll } from '../../../helpers/testReactTestingUtils'; +import { Dict } from '../../../types/types'; +import NavigateWithParams from '../NavigateWithParams'; + +it('should transform path parameters to search params', () => { + const transformParams = jest.fn((params: Params) => { + return { also: 'this', ...params }; + }); + + renderNavigateWithParams(transformParams); + + expect(transformParams).toBeCalled(); + expect(screen.getByText('/target?also=this&key=hello&subkey=test')).toBeInTheDocument(); +}); + +function renderNavigateWithParams(transformParams: (params: Params) => Dict<string>) { + render( + <MemoryRouter initialEntries={['/source/hello/test']}> + <Routes> + <Route + path="/source/:key/:subkey" + element={<NavigateWithParams pathname="/target" transformParams={transformParams} />} + /> + <Route path="*" element={<CatchAll />} /> + </Routes> + </MemoryRouter> + ); +} diff --git a/server/sonar-web/src/main/js/app/utils/__tests__/handleRequiredAuthorization-test.ts b/server/sonar-web/src/main/js/app/utils/__tests__/handleRequiredAuthorization-test.ts index cb100fec0c3..5fc4a3c8b3e 100644 --- a/server/sonar-web/src/main/js/app/utils/__tests__/handleRequiredAuthorization-test.ts +++ b/server/sonar-web/src/main/js/app/utils/__tests__/handleRequiredAuthorization-test.ts @@ -18,14 +18,36 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import getHistory from '../../../helpers/getHistory'; import handleRequiredAuthorization from '../handleRequiredAuthorization'; -jest.mock('../../../helpers/getHistory', () => jest.fn()); +const originalLocation = window.location; + +const replace = jest.fn(); + +beforeAll(() => { + const location = { + ...window.location, + pathname: '/path', + search: '?id=12', + hash: '#tag', + replace + }; + Object.defineProperty(window, 'location', { + writable: true, + value: location + }); +}); + +afterAll(() => { + Object.defineProperty(window, 'location', { + writable: true, + value: originalLocation + }); +}); it('should not render for anonymous user', () => { - const replace = jest.fn(); - (getHistory as jest.Mock<any>).mockReturnValue({ replace }); handleRequiredAuthorization(); - expect(replace).toBeCalledWith(expect.objectContaining({ pathname: '/sessions/new' })); + expect(replace).toBeCalledWith( + '/sessions/new?return_to=%2Fpath%3Fid%3D12%23tag&authorizationError=true' + ); }); diff --git a/server/sonar-web/src/main/js/app/utils/exportModulesAsGlobals.ts b/server/sonar-web/src/main/js/app/utils/exportModulesAsGlobals.ts index f9351d9ef26..df3a3308f43 100644 --- a/server/sonar-web/src/main/js/app/utils/exportModulesAsGlobals.ts +++ b/server/sonar-web/src/main/js/app/utils/exportModulesAsGlobals.ts @@ -26,7 +26,7 @@ import React from 'react'; import * as ReactDom from 'react-dom'; import * as ReactIntl from 'react-intl'; import ReactModal from 'react-modal'; -import * as ReactRouter from 'react-router'; +import * as ReactRouterDom from 'react-router-dom'; /* * Expose dependencies to extensions @@ -41,5 +41,5 @@ export default function exportModulesAsGlobals() { w.ReactDOM = ReactDom; w.ReactIntl = ReactIntl; w.ReactModal = ReactModal; - w.ReactRouter = ReactRouter; + w.ReactRouterDom = ReactRouterDom; } diff --git a/server/sonar-web/src/main/js/app/utils/handleRequiredAuthorization.ts b/server/sonar-web/src/main/js/app/utils/handleRequiredAuthorization.ts index 44d052feb73..fd1ec8cca5c 100644 --- a/server/sonar-web/src/main/js/app/utils/handleRequiredAuthorization.ts +++ b/server/sonar-web/src/main/js/app/utils/handleRequiredAuthorization.ts @@ -17,13 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import getHistory from '../../helpers/getHistory'; - export default function handleRequiredAuthorization() { - const history = getHistory(); const returnTo = window.location.pathname + window.location.search + window.location.hash; - history.replace({ - pathname: '/sessions/new', - query: { return_to: returnTo, authorizationError: true } - }); + const searchParams = new URLSearchParams({ return_to: returnTo, authorizationError: 'true' }); + window.location.replace(`/sessions/new?${searchParams.toString()}`); } diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index 622c56db585..ace3ec81bd5 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -17,24 +17,22 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ - -import { Location } from 'history'; -import { pick } from 'lodash'; import * as React from 'react'; import { render } from 'react-dom'; import { HelmetProvider } from 'react-helmet-async'; import { IntlProvider } from 'react-intl'; -import { IndexRoute, Redirect, Route, RouteConfig, RouteProps, Router } from 'react-router'; +import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import accountRoutes from '../../apps/account/routes'; import auditLogsRoutes from '../../apps/audit-logs/routes'; import backgroundTasksRoutes from '../../apps/background-tasks/routes'; +import ChangeAdminPasswordApp from '../../apps/change-admin-password/ChangeAdminPasswordApp'; import codeRoutes from '../../apps/code/routes'; import codingRulesRoutes from '../../apps/coding-rules/routes'; import componentMeasuresRoutes from '../../apps/component-measures/routes'; import documentationRoutes from '../../apps/documentation/routes'; import groupsRoutes from '../../apps/groups/routes'; -import Issues from '../../apps/issues/components/AppContainer'; -import { maintenanceRoutes, setupRoutes } from '../../apps/maintenance/routes'; +import { globalIssuesRoutes, projectIssuesRoutes } from '../../apps/issues/routes'; +import maintenanceRoutes from '../../apps/maintenance/routes'; import marketplaceRoutes from '../../apps/marketplace/routes'; import overviewRoutes from '../../apps/overview/routes'; import permissionTemplatesRoutes from '../../apps/permission-templates/routes'; @@ -42,13 +40,17 @@ import { globalPermissionsRoutes, projectPermissionsRoutes } from '../../apps/pe import projectActivityRoutes from '../../apps/projectActivity/routes'; import projectBaselineRoutes from '../../apps/projectBaseline/routes'; import projectBranchesRoutes from '../../apps/projectBranches/routes'; +import ProjectDeletionApp from '../../apps/projectDeletion/App'; import projectDumpRoutes from '../../apps/projectDump/routes'; +import ProjectKeyApp from '../../apps/projectKey/Key'; +import ProjectLinksApp from '../../apps/projectLinks/App'; import projectQualityGateRoutes from '../../apps/projectQualityGate/routes'; import projectQualityProfilesRoutes from '../../apps/projectQualityProfiles/routes'; import projectsRoutes from '../../apps/projects/routes'; import projectsManagementRoutes from '../../apps/projectsManagement/routes'; import qualityGatesRoutes from '../../apps/quality-gates/routes'; import qualityProfilesRoutes from '../../apps/quality-profiles/routes'; +import SecurityHotspotsApp from '../../apps/security-hotspots/SecurityHotspotsApp'; import sessionsRoutes from '../../apps/sessions/routes'; import settingsRoutes from '../../apps/settings/routes'; import systemRoutes from '../../apps/system/routes'; @@ -56,197 +58,146 @@ import tutorialsRoutes from '../../apps/tutorials/routes'; import usersRoutes from '../../apps/users/routes'; import webAPIRoutes from '../../apps/web-api/routes'; import webhooksRoutes from '../../apps/webhooks/routes'; -import withIndexationGuard from '../../components/hoc/withIndexationGuard'; -import { lazyLoadComponent } from '../../components/lazyLoadComponent'; -import getHistory from '../../helpers/getHistory'; +import { omitNil } from '../../helpers/request'; import { AppState } from '../../types/appstate'; import { CurrentUser } from '../../types/users'; +import AdminContainer from '../components/AdminContainer'; import App from '../components/App'; import AppStateContextProvider from '../components/app-state/AppStateContextProvider'; +import ComponentContainer from '../components/ComponentContainer'; import CurrentUserContextProvider from '../components/current-user/CurrentUserContextProvider'; +import GlobalAdminPageExtension from '../components/extensions/GlobalAdminPageExtension'; +import GlobalPageExtension from '../components/extensions/GlobalPageExtension'; +import PortfolioPage from '../components/extensions/PortfolioPage'; +import PortfoliosPage from '../components/extensions/PortfoliosPage'; +import ProjectAdminPageExtension from '../components/extensions/ProjectAdminPageExtension'; +import ProjectPageExtension from '../components/extensions/ProjectPageExtension'; +import FormattingHelp from '../components/FormattingHelp'; import GlobalContainer from '../components/GlobalContainer'; import GlobalMessagesContainer from '../components/GlobalMessagesContainer'; -import { PageContext } from '../components/indexation/PageUnavailableDueToIndexation'; +import Landing from '../components/Landing'; import MigrationContainer from '../components/MigrationContainer'; import NonAdminPagesContainer from '../components/NonAdminPagesContainer'; +import NotFound from '../components/NotFound'; +import PluginRiskConsent from '../components/PluginRiskConsent'; +import ProjectAdminContainer from '../components/ProjectAdminContainer'; +import ResetPassword from '../components/ResetPassword'; +import SimpleContainer from '../components/SimpleContainer'; import exportModulesAsGlobals from './exportModulesAsGlobals'; +import NavigateWithParams from './NavigateWithParams'; +import NavigateWithSearchAndHash from './NavigateWithSearchAndHash'; -function handleUpdate(this: { state: { location: Location } }) { - const { action } = this.state.location; - - if (action === 'PUSH') { - window.scrollTo(0, 0); - } +function renderRedirect({ from, to }: { from: string; to: string }) { + return <Route path={from} element={<Navigate to={{ pathname: to }} replace={true} />} />; } -// this is not an official api -export const RouteWithChildRoutes = Route as React.ComponentClass< - RouteProps & { childRoutes: RouteConfig } ->; - function renderRedirects() { return ( <> <Route path="/account/issues" - onEnter={(_, replace) => { - replace({ pathname: '/issues', query: { myIssues: 'true', resolved: 'false' } }); - }} + element={ + <NavigateWithParams + pathname="/issues" + transformParams={() => ({ myIssues: 'true', resolved: 'false' })} + /> + } /> - <Route - path="/codingrules" - onEnter={(_, replace) => { - replace(`/coding_rules${window.location.hash}`); - }} - /> + <Route path="/codingrules" element={<NavigateWithSearchAndHash pathname="/coding_rules" />} /> <Route path="/dashboard/index/:key" - onEnter={(nextState, replace) => { - replace({ pathname: '/dashboard', query: { id: nextState.params.key } }); - }} + element={ + <NavigateWithParams + pathname="/dashboard" + transformParams={params => omitNil({ id: params['key'] })} + /> + } /> <Route path="/application/console" - onEnter={(nextState, replace) => { - replace({ - pathname: '/project/admin/extension/developer-server/application-console', - query: { id: nextState.location.query.id } - }); - }} + element={ + <NavigateWithSearchAndHash pathname="/project/admin/extension/developer-server/application-console" /> + } /> <Route path="/application/settings" - onEnter={(nextState, replace) => { - replace({ - pathname: '/project/admin/extension/governance/application_report', - query: { id: nextState.location.query.id } - }); - }} + element={ + <NavigateWithSearchAndHash pathname="/project/admin/extension/governance/application_report" /> + } /> - <Route - path="/issues/search" - onEnter={(_, replace) => { - replace(`/issues${window.location.hash}`); - }} - /> + <Route path="/issues/search" element={<NavigateWithSearchAndHash pathname="/issues" />} /> - <Redirect from="/admin" to="/admin/settings" /> - <Redirect from="/background_tasks" to="/admin/background_tasks" /> - <Redirect from="/component/index" to="/component" /> - <Redirect from="/component_issues" to="/project/issues" /> - <Redirect from="/dashboard/index" to="/dashboard" /> - <Redirect - from="/documentation/analysis/languages/vb" - to="/documentation/analysis/languages/vbnet/" - /> - <Redirect from="/governance" to="/portfolio" /> - <Redirect from="/groups" to="/admin/groups" /> - <Redirect from="/extension/governance/portfolios" to="/portfolios" /> - <Redirect from="/permission_templates" to="/admin/permission_templates" /> - <Redirect from="/profiles/index" to="/profiles" /> - <Redirect from="/projects_admin" to="/admin/projects_management" /> - <Redirect from="/quality_gates/index" to="/quality_gates" /> - <Redirect from="/roles/global" to="/admin/permissions" /> - <Redirect from="/admin/roles/global" to="/admin/permissions" /> - <Redirect from="/settings" to="/admin/settings" /> - <Redirect from="/settings/encryption" to="/admin/settings/encryption" /> - <Redirect from="/settings/index" to="/admin/settings" /> - <Redirect from="/sessions/login" to="/sessions/new" /> - <Redirect from="/system" to="/admin/system" /> - <Redirect from="/system/index" to="/admin/system" /> - <Redirect from="/view" to="/portfolio" /> - <Redirect from="/users" to="/admin/users" /> - <Redirect from="/onboarding" to="/projects/create" /> - <Redirect from="markdown/help" to="formatting/help" /> + {renderRedirect({ from: '/admin', to: '/admin/settings' })} + {renderRedirect({ from: '/background_tasks', to: '/admin/background_tasks' })} + {renderRedirect({ + from: '/documentation/analysis/languages/vb', + to: '/documentation/analysis/languages/vbnet/' + })} + {renderRedirect({ from: '/groups', to: '/admin/groups' })} + {renderRedirect({ from: '/extension/governance/portfolios', to: '/portfolios' })} + {renderRedirect({ from: '/permission_templates', to: '/admin/permission_templates' })} + {renderRedirect({ from: '/profiles/index', to: '/profiles' })} + {renderRedirect({ from: '/projects_admin', to: '/admin/projects_management' })} + {renderRedirect({ from: '/quality_gates/index', to: '/quality_gates' })} + {renderRedirect({ from: '/roles/global', to: '/admin/permissions' })} + {renderRedirect({ from: '/admin/roles/global', to: '/admin/permissions' })} + {renderRedirect({ from: '/settings', to: '/admin/settings' })} + {renderRedirect({ from: '/settings/encryption', to: '/admin/settings/encryption' })} + {renderRedirect({ from: '/settings/index', to: '/admin/settings' })} + {renderRedirect({ from: '/sessions/login', to: '/sessions/new' })} + {renderRedirect({ from: '/system', to: '/admin/system' })} + {renderRedirect({ from: '/system/index', to: '/admin/system' })} + {renderRedirect({ from: '/users', to: '/admin/users' })} + {renderRedirect({ from: '/onboarding', to: '/projects/create' })} + {renderRedirect({ from: '/markdown/help', to: '/formatting/help' })} </> ); } function renderComponentRoutes() { return ( - <Route component={lazyLoadComponent(() => import('../components/ComponentContainer'))}> + <Route element={<ComponentContainer />}> {/* This container is a catch-all for all non-admin pages */} - <Route component={NonAdminPagesContainer}> - <RouteWithChildRoutes path="code" childRoutes={codeRoutes} /> - <RouteWithChildRoutes path="component_measures" childRoutes={componentMeasuresRoutes} /> - <RouteWithChildRoutes path="dashboard" childRoutes={overviewRoutes} /> - <Route - path="portfolio" - component={lazyLoadComponent(() => import('../components/extensions/PortfolioPage'))} - /> - <RouteWithChildRoutes path="project/activity" childRoutes={projectActivityRoutes} /> + <Route element={<NonAdminPagesContainer />}> + {codeRoutes()} + {componentMeasuresRoutes()} + {overviewRoutes()} + <Route path="portfolio" element={<PortfolioPage />} /> + {projectActivityRoutes()} <Route path="project/extension/:pluginKey/:extensionKey" - component={lazyLoadComponent(() => - import('../components/extensions/ProjectPageExtension') - )} + element={<ProjectPageExtension />} /> - <Route - path="project/issues" - component={Issues} - onEnter={({ location: { query } }, replace) => { - if (query.types) { - if (query.types === 'SECURITY_HOTSPOT') { - replace({ - pathname: '/security_hotspots', - query: { - ...pick(query, ['id', 'branch', 'pullRequest']), - assignedToMe: false - } - }); - } else { - query.types = query.types - .split(',') - .filter((type: string) => type !== 'SECURITY_HOTSPOT') - .join(','); - } - } - }} - /> - <Route - path="security_hotspots" - component={lazyLoadComponent(() => - import('../../apps/security-hotspots/SecurityHotspotsApp') - )} - /> - <RouteWithChildRoutes path="project/quality_gate" childRoutes={projectQualityGateRoutes} /> - <RouteWithChildRoutes - path="project/quality_profiles" - childRoutes={projectQualityProfilesRoutes} - /> - <RouteWithChildRoutes path="tutorials" childRoutes={tutorialsRoutes} /> + {projectIssuesRoutes()} + <Route path="security_hotspots" element={<SecurityHotspotsApp />} /> + {projectQualityGateRoutes()} + {projectQualityProfilesRoutes()} + + {tutorialsRoutes()} </Route> - <Route component={lazyLoadComponent(() => import('../components/ProjectAdminContainer'))}> - <Route - path="project/admin/extension/:pluginKey/:extensionKey" - component={lazyLoadComponent(() => - import('../components/extensions/ProjectAdminPageExtension') - )} - /> - <RouteWithChildRoutes path="project/background_tasks" childRoutes={backgroundTasksRoutes} /> - <RouteWithChildRoutes path="project/baseline" childRoutes={projectBaselineRoutes} /> - <RouteWithChildRoutes path="project/branches" childRoutes={projectBranchesRoutes} /> - <RouteWithChildRoutes path="project/import_export" childRoutes={projectDumpRoutes} /> - <RouteWithChildRoutes path="project/settings" childRoutes={settingsRoutes} /> - <RouteWithChildRoutes path="project_roles" childRoutes={projectPermissionsRoutes} /> - <RouteWithChildRoutes path="project/webhooks" childRoutes={webhooksRoutes} /> - <Route - path="project/deletion" - component={lazyLoadComponent(() => import('../../apps/projectDeletion/App'))} - /> - <Route - path="project/links" - component={lazyLoadComponent(() => import('../../apps/projectLinks/App'))} - /> - <Route - path="project/key" - component={lazyLoadComponent(() => import('../../apps/projectKey/Key'))} - /> + <Route element={<ProjectAdminContainer />}> + <Route path="project"> + <Route + path="admin/extension/:pluginKey/:extensionKey" + element={<ProjectAdminPageExtension />} + /> + {backgroundTasksRoutes()} + {projectBaselineRoutes()} + {projectBranchesRoutes()} + {projectDumpRoutes()} + {settingsRoutes()} + {webhooksRoutes()} + + <Route path="deletion" element={<ProjectDeletionApp />} /> + <Route path="links" element={<ProjectLinksApp />} /> + <Route path="key" element={<ProjectKeyApp />} /> + </Route> + {projectPermissionsRoutes()} </Route> </Route> ); @@ -254,24 +205,19 @@ function renderComponentRoutes() { function renderAdminRoutes() { return ( - <Route component={lazyLoadComponent(() => import('../components/AdminContainer'))} path="admin"> - <Route - path="extension/:pluginKey/:extensionKey" - component={lazyLoadComponent(() => - import('../components/extensions/GlobalAdminPageExtension') - )} - /> - <RouteWithChildRoutes path="audit" childRoutes={auditLogsRoutes} /> - <RouteWithChildRoutes path="background_tasks" childRoutes={backgroundTasksRoutes} /> - <RouteWithChildRoutes path="groups" childRoutes={groupsRoutes} /> - <RouteWithChildRoutes path="permission_templates" childRoutes={permissionTemplatesRoutes} /> - <RouteWithChildRoutes path="permissions" childRoutes={globalPermissionsRoutes} /> - <RouteWithChildRoutes path="projects_management" childRoutes={projectsManagementRoutes} /> - <RouteWithChildRoutes path="settings" childRoutes={settingsRoutes} /> - <RouteWithChildRoutes path="system" childRoutes={systemRoutes} /> - <RouteWithChildRoutes path="marketplace" childRoutes={marketplaceRoutes} /> - <RouteWithChildRoutes path="users" childRoutes={usersRoutes} /> - <RouteWithChildRoutes path="webhooks" childRoutes={webhooksRoutes} /> + <Route path="admin" element={<AdminContainer />}> + <Route path="extension/:pluginKey/:extensionKey" element={<GlobalAdminPageExtension />} /> + {settingsRoutes()} + {auditLogsRoutes()} + {backgroundTasksRoutes()} + {groupsRoutes()} + {permissionTemplatesRoutes()} + {globalPermissionsRoutes()} + {projectsManagementRoutes()} + {systemRoutes()} + {marketplaceRoutes()} + {usersRoutes()} + {webhooksRoutes()} </Route> ); } @@ -281,100 +227,78 @@ export default function startReactApp(lang: string, appState: AppState, currentU const el = document.getElementById('content'); - const history = getHistory(); - render( <HelmetProvider> <AppStateContextProvider appState={appState}> <CurrentUserContextProvider currentUser={currentUser}> <IntlProvider defaultLocale={lang} locale={lang}> <GlobalMessagesContainer /> - <Router history={history} onUpdate={handleUpdate}> - {renderRedirects()} + <BrowserRouter> + <Routes> + {renderRedirects()} - <Route - path="formatting/help" - component={lazyLoadComponent(() => import('../components/FormattingHelp'))} - /> + <Route path="formatting/help" element={<FormattingHelp />} /> - <Route component={lazyLoadComponent(() => import('../components/SimpleContainer'))}> - <Route path="maintenance">{maintenanceRoutes}</Route> - <Route path="setup">{setupRoutes}</Route> - </Route> + <Route element={<SimpleContainer />}>{maintenanceRoutes()}</Route> - <Route component={MigrationContainer}> - <Route - component={lazyLoadComponent(() => - import('../components/SimpleSessionsContainer') - )}> - <RouteWithChildRoutes path="/sessions" childRoutes={sessionsRoutes} /> - </Route> + <Route element={<MigrationContainer />}> + {sessionsRoutes()} + + <Route path="/" element={<App />}> + <Route index={true} element={<Landing />} /> + + <Route element={<GlobalContainer />}> + {accountRoutes()} + + {codingRulesRoutes()} - <Route path="/" component={App}> - <IndexRoute - component={lazyLoadComponent(() => import('../components/Landing'))} - /> + {documentationRoutes()} - <Route component={GlobalContainer}> - <RouteWithChildRoutes path="account" childRoutes={accountRoutes} /> - <RouteWithChildRoutes path="coding_rules" childRoutes={codingRulesRoutes} /> - <RouteWithChildRoutes path="documentation" childRoutes={documentationRoutes} /> + <Route + path="extension/:pluginKey/:extensionKey" + element={<GlobalPageExtension />} + /> + + {globalIssuesRoutes()} + + {projectsRoutes()} + + {qualityGatesRoutes()} + {qualityProfilesRoutes()} + + <Route path="portfolios" element={<PortfoliosPage />} /> + {webAPIRoutes()} + + {renderComponentRoutes()} + + {renderAdminRoutes()} + </Route> <Route - path="extension/:pluginKey/:extensionKey" - component={lazyLoadComponent(() => - import('../components/extensions/GlobalPageExtension') - )} + // We don't want this route to have any menu. + // That is why we can not have it under the accountRoutes + path="account/reset_password" + element={<ResetPassword />} /> + <Route - path="issues" - component={withIndexationGuard(Issues, PageContext.Issues)} + // We don't want this route to have any menu. This is why we define it here + // rather than under the admin routes. + path="admin/change_admin_password" + element={<ChangeAdminPasswordApp />} /> - <RouteWithChildRoutes path="projects" childRoutes={projectsRoutes} /> - <RouteWithChildRoutes path="quality_gates" childRoutes={qualityGatesRoutes} /> + <Route - path="portfolios" - component={lazyLoadComponent(() => - import('../components/extensions/PortfoliosPage') - )} + // We don't want this route to have any menu. This is why we define it here + // rather than under the admin routes. + path="admin/plugin_risk_consent" + element={<PluginRiskConsent />} /> - <RouteWithChildRoutes path="profiles" childRoutes={qualityProfilesRoutes} /> - <RouteWithChildRoutes path="web_api" childRoutes={webAPIRoutes} /> - - {renderComponentRoutes()} - - {renderAdminRoutes()} + <Route path="not_found" element={<NotFound />} /> + <Route path="*" element={<NotFound />} /> </Route> - <Route - // We don't want this route to have any menu. - // That is why we can not have it under the accountRoutes - path="account/reset_password" - component={lazyLoadComponent(() => import('../components/ResetPassword'))} - /> - <Route - // We don't want this route to have any menu. This is why we define it here - // rather than under the admin routes. - path="admin/change_admin_password" - component={lazyLoadComponent(() => - import('../../apps/change-admin-password/ChangeAdminPasswordApp') - )} - /> - <Route - // We don't want this route to have any menu. This is why we define it here - // rather than under the admin routes. - path="admin/plugin_risk_consent" - component={lazyLoadComponent(() => import('../components/PluginRiskConsent'))} - /> - <Route - path="not_found" - component={lazyLoadComponent(() => import('../components/NotFound'))} - /> - <Route - path="*" - component={lazyLoadComponent(() => import('../components/NotFound'))} - /> </Route> - </Route> - </Router> + </Routes> + </BrowserRouter> </IntlProvider> </CurrentUserContextProvider> </AppStateContextProvider> |