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 | |
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')
486 files changed, 10129 insertions, 6706 deletions
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 16eacdb717d..4c8c0a85765 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -19,7 +19,6 @@ "date-fns": "1.30.1", "dompurify": "2.3.6", "formik": "1.2.0", - "history": "3.3.0", "lodash": "4.17.21", "lunr": "2.3.9", "mdast-util-toc": "5.0.2", @@ -31,7 +30,7 @@ "react-helmet-async": "1.2.3", "react-intl": "3.12.1", "react-modal": "3.14.4", - "react-router": "3.2.6", + "react-router-dom": "6.3.0", "react-select": "4.3.1", "react-virtualized": "9.22.3", "regenerator-runtime": "0.13.9", @@ -69,7 +68,6 @@ "@types/react-dom": "16.8.4", "@types/react-helmet": "5.0.15", "@types/react-modal": "3.13.1", - "@types/react-router": "3.0.20", "@types/react-select": "4.0.16", "@types/react-virtualized": "9.21.20", "@types/valid-url": "1.0.3", diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts index 65128a0e82e..8b641a56d62 100644 --- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -269,7 +269,7 @@ export default class IssuesServiceMock { }; handleSearchIssues = (query: RequestData): Promise<RawIssuesResponse> => { - const facets = query.facets.split(',').map((name: string) => { + const facets = (query.facets ?? '').split(',').map((name: string) => { if (name === 'owaspTop10-2021') { return this.owasp2021FacetList(); } 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/helpers/getHistory.ts b/server/sonar-web/src/main/js/app/components/admin/withAdminPagesOutletContext.tsx index 849bc0ed070..961f4f93626 100644 --- a/server/sonar-web/src/main/js/helpers/getHistory.ts +++ b/server/sonar-web/src/main/js/app/components/admin/withAdminPagesOutletContext.tsx @@ -17,20 +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 { createHistory, History } from 'history'; -import { useRouterHistory } from 'react-router'; -import { getBaseUrl } from './system'; +import * as React from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { AdminPagesContext } from '../../../types/admin'; -let history: History; +export default function withAdminPagesOutletContext( + WrappedComponent: React.ComponentType<AdminPagesContext> +) { + return function WithAdminPagesOutletContext() { + const { adminPages } = useOutletContext<AdminPagesContext>(); -function ensureHistory() { - // eslint-disable-next-line react-hooks/rules-of-hooks - history = useRouterHistory(createHistory)({ - basename: getBaseUrl() - }); - return history; -} - -export default function getHistory() { - return history ? history : ensureHistory(); + 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> diff --git a/server/sonar-web/src/main/js/apps/account/Account.tsx b/server/sonar-web/src/main/js/apps/account/Account.tsx index 1a7b5fcd4eb..6cfc316a9eb 100644 --- a/server/sonar-web/src/main/js/apps/account/Account.tsx +++ b/server/sonar-web/src/main/js/apps/account/Account.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; +import { Outlet } from 'react-router-dom'; import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; import A11ySkipTarget from '../../components/a11y/A11ySkipTarget'; import Suggestions from '../../components/embed-docs-modal/Suggestions'; @@ -41,7 +42,7 @@ export class Account extends React.PureComponent<Props> { } render() { - const { currentUser, children } = this.props; + const { currentUser } = this.props; if (!currentUser.isLoggedIn) { return null; @@ -60,7 +61,7 @@ export class Account extends React.PureComponent<Props> { </div> </header> - {children} + <Outlet /> </div> ); } diff --git a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx index 5fc50ba13bc..b2f4304d0ce 100644 --- a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx +++ b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx @@ -24,7 +24,6 @@ import selectEvent from 'react-select-event'; import { getMyProjects, getScannableProjects } from '../../../api/components'; import NotificationsMock from '../../../api/mocks/NotificationsMock'; import UserTokensMock from '../../../api/mocks/UserTokensMock'; -import getHistory from '../../../helpers/getHistory'; import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; import { renderApp } from '../../../helpers/testReactTestingUtils'; import { Permissions } from '../../../types/permissions'; @@ -128,11 +127,21 @@ jest.mock('../../../api/users', () => ({ changePassword: jest.fn().mockResolvedValue(true) })); -it('should handle a currentUser not logged in', async () => { +it('should handle a currentUser not logged in', () => { + const replace = jest.fn(); + const locationMock = jest.spyOn(window, 'location', 'get').mockReturnValue(({ + pathname: '/account', + search: '', + hash: '', + replace + } as unknown) as Location); + renderAccountApp(mockCurrentUser()); // Make sure we're redirected to the login screen - expect(await screen.findByText('/sessions/new?return_to=%2Faccount')).toBeInTheDocument(); + expect(replace).toBeCalledWith('/sessions/new?return_to=%2Faccount'); + + locationMock.mockRestore(); }); it('should render the top menu', () => { @@ -544,5 +553,5 @@ function getCheckboxByRowName(name: string) { } function renderAccountApp(currentUser: CurrentUser, navigateTo?: string) { - renderApp('account', routes, { currentUser, history: getHistory(), navigateTo }); + renderApp('account', routes, { currentUser, navigateTo }); } diff --git a/server/sonar-web/src/main/js/apps/account/components/Nav.tsx b/server/sonar-web/src/main/js/apps/account/components/Nav.tsx index 54545c89085..76159e9c080 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Nav.tsx +++ b/server/sonar-web/src/main/js/apps/account/components/Nav.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { IndexLink, Link } from 'react-router'; +import { NavLink } from 'react-router-dom'; import NavBarTabs from '../../../components/ui/NavBarTabs'; import { translate } from '../../../helpers/l10n'; @@ -27,24 +27,18 @@ export default function Nav() { <nav className="account-nav"> <NavBarTabs> <li> - <IndexLink activeClassName="active" to="/account/"> + <NavLink end={true} to="/account"> {translate('my_account.profile')} - </IndexLink> + </NavLink> </li> <li> - <Link activeClassName="active" to="/account/security/"> - {translate('my_account.security')} - </Link> + <NavLink to="/account/security">{translate('my_account.security')}</NavLink> </li> <li> - <Link activeClassName="active" to="/account/notifications"> - {translate('my_account.notifications')} - </Link> + <NavLink to="/account/notifications">{translate('my_account.notifications')}</NavLink> </li> <li> - <Link activeClassName="active" to="/account/projects/"> - {translate('my_account.projects')} - </Link> + <NavLink to="/account/projects">{translate('my_account.projects')}</NavLink> </li> </NavBarTabs> </nav> diff --git a/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx b/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx index 620f401b13a..f8d3e916aad 100644 --- a/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx +++ b/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.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 MetaLink from '../../../app/components/nav/component/projectInformation/meta/MetaLink'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import DateFromNow from '../../../components/intl/DateFromNow'; diff --git a/server/sonar-web/src/main/js/apps/account/routes.ts b/server/sonar-web/src/main/js/apps/account/routes.ts deleted file mode 100644 index 618015e7955..00000000000 --- a/server/sonar-web/src/main/js/apps/account/routes.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - component: lazyLoadComponent(() => import('./Account')), - childRoutes: [ - { - indexRoute: { component: lazyLoadComponent(() => import('./profile/Profile')) } - }, - { - path: 'security', - component: lazyLoadComponent(() => import('./security/Security')) - }, - { - path: 'projects', - component: lazyLoadComponent(() => import('./projects/ProjectsContainer')) - }, - { - path: 'notifications', - component: lazyLoadComponent(() => import('./notifications/Notifications')) - } - ] - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/account/routes.tsx b/server/sonar-web/src/main/js/apps/account/routes.tsx new file mode 100644 index 00000000000..a39d51dd419 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/routes.tsx @@ -0,0 +1,37 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import Account from './Account'; +import Notifications from './notifications/Notifications'; +import Profile from './profile/Profile'; +import ProjectsContainer from './projects/ProjectsContainer'; +import Security from './security/Security'; + +const routes = () => ( + <Route path="account" element={<Account />}> + <Route index={true} element={<Profile />} /> + <Route path="security" element={<Security />} /> + <Route path="projects" element={<ProjectsContainer />} /> + <Route path="notifications" element={<Notifications />} /> + </Route> +); + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx index da2e3196a93..30790038dc8 100644 --- a/server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx +++ b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { getValues } from '../../../api/settings'; +import withAdminPagesOutletContext from '../../../app/components/admin/withAdminPagesOutletContext'; import { AdminPageExtension } from '../../../types/extension'; import { SettingsKey } from '../../../types/settings'; import { Extension } from '../../../types/types'; @@ -37,7 +38,7 @@ interface State { selection: RangeOption; } -export default class AuditApp extends React.PureComponent<Props, State> { +export class AuditApp extends React.PureComponent<Props, State> { constructor(props: Props) { super(props); @@ -102,3 +103,5 @@ export default class AuditApp extends React.PureComponent<Props, State> { ); } } + +export default withAdminPagesOutletContext(AuditApp); diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx index c8f087b80c3..c3f1952a905 100644 --- a/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx @@ -21,11 +21,12 @@ import { subDays } from 'date-fns'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import DateRangeInput from '../../../components/controls/DateRangeInput'; import Radio from '../../../components/controls/Radio'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import { translate } from '../../../helpers/l10n'; +import { queryToSearch } from '../../../helpers/urls'; import '../style.css'; import { HousekeepingPolicy, now, RangeOption } from '../utils'; import DownloadButton from './DownloadButton'; @@ -90,7 +91,7 @@ export default function AuditAppRenderer(props: AuditAppRendererProps) { <Link to={{ pathname: '/admin/settings', - query: { category: 'housekeeping' }, + search: queryToSearch({ category: 'housekeeping' }), hash: '#auditLogs' }}> {translate('audit_logs.page.description.link')} diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-test.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-test.tsx index baac6e88cda..5d519095633 100644 --- a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-test.tsx @@ -24,7 +24,7 @@ import { getValues } from '../../../../api/settings'; import { waitAndUpdate } from '../../../../helpers/testUtils'; import { AdminPageExtension } from '../../../../types/extension'; import { HousekeepingPolicy, RangeOption } from '../../utils'; -import AuditApp from '../AuditApp'; +import { AuditApp } from '../AuditApp'; import AuditAppRenderer from '../AuditAppRenderer'; jest.mock('../../../../api/settings', () => ({ diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditAppRenderer-test.tsx.snap index f5c6b757702..fa377d15b9c 100644 --- a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditAppRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditAppRenderer-test.tsx.snap @@ -35,15 +35,11 @@ exports[`should render correctly for Monthly housekeeping policy 1`] = ` Object { "housekeeping": "audit_logs.housekeeping_policy.Monthly", "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "hash": "#auditLogs", "pathname": "/admin/settings", - "query": Object { - "category": "housekeeping", - }, + "search": "?category=housekeeping", } } > @@ -161,15 +157,11 @@ exports[`should render correctly for Trimestrial housekeeping policy 1`] = ` Object { "housekeeping": "audit_logs.housekeeping_policy.Trimestrial", "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "hash": "#auditLogs", "pathname": "/admin/settings", - "query": Object { - "category": "housekeeping", - }, + "search": "?category=housekeeping", } } > @@ -299,15 +291,11 @@ exports[`should render correctly for Weekly housekeeping policy 1`] = ` Object { "housekeeping": "audit_logs.housekeeping_policy.Weekly", "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "hash": "#auditLogs", "pathname": "/admin/settings", - "query": Object { - "category": "housekeeping", - }, + "search": "?category=housekeeping", } } > @@ -413,15 +401,11 @@ exports[`should render correctly for Yearly housekeeping policy 1`] = ` Object { "housekeeping": "audit_logs.housekeeping_policy.Yearly", "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "hash": "#auditLogs", "pathname": "/admin/settings", - "query": Object { - "category": "housekeeping", - }, + "search": "?category=housekeeping", } } > diff --git a/server/sonar-web/src/main/js/apps/groups/routes.ts b/server/sonar-web/src/main/js/apps/audit-logs/routes.tsx index 9d8f2bd42e4..247f80add12 100644 --- a/server/sonar-web/src/main/js/apps/groups/routes.ts +++ b/server/sonar-web/src/main/js/apps/audit-logs/routes.tsx @@ -17,12 +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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import AuditApp from './components/AuditApp'; -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/App')) } - } -]; +const routes = () => <Route path="audit" element={<AuditApp />} />; export default routes; diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx index 82bc95ab111..3e10c43987f 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx @@ -27,13 +27,14 @@ import { getStatus, getTypes } from '../../../api/ce'; +import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; -import { Location, Router } from '../../../components/hoc/withRouter'; +import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { toShortNotSoISOString } from '../../../helpers/dates'; import { translate } from '../../../helpers/l10n'; import { parseAsDate } from '../../../helpers/query'; import { Task, TaskStatuses } from '../../../types/tasks'; -import { Component } from '../../../types/types'; +import { Component, RawQuery } from '../../../types/types'; import '../background-tasks.css'; import { CURRENTS, DEBOUNCE_DELAY, DEFAULT_FILTERS } from '../constants'; import { mapFiltersToParameters, Query, updateTask } from '../utils'; @@ -44,9 +45,9 @@ import Stats from './Stats'; import Tasks from './Tasks'; interface Props { - component?: Pick<Component, 'key'> & { id: string }; // id should be removed when api/ce/activity accept a component key instead of an id + component?: Component; location: Location; - router: Pick<Router, 'push'>; + router: Router; } interface State { @@ -58,7 +59,7 @@ interface State { types?: string[]; } -export default class BackgroundTasksApp extends React.PureComponent<Props, State> { +export class BackgroundTasksApp extends React.PureComponent<Props, State> { loadTasksDebounced: () => void; mounted = false; @@ -134,7 +135,7 @@ export default class BackgroundTasksApp extends React.PureComponent<Props, State }; handleFilterUpdate = (nextState: Partial<Query>) => { - const nextQuery = { ...this.props.location.query, ...nextState }; + const nextQuery: RawQuery = { ...this.props.location.query, ...nextState }; // remove defaults Object.keys(DEFAULT_FILTERS).forEach((key: keyof typeof DEFAULT_FILTERS) => { @@ -253,3 +254,5 @@ export default class BackgroundTasksApp extends React.PureComponent<Props, State ); } } + +export default withComponentContext(withRouter(BackgroundTasksApp)); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Header.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Header.tsx index 660d47c34f4..fa7c2e92c3a 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Header.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Header.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 Workers from './Workers'; diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx index ebbc373e4d3..f642e4521d5 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.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 BranchIcon from '../../../components/icons/BranchIcon'; import PullRequestIcon from '../../../components/icons/PullRequestIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon'; diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/BackgroundTasksApp-test.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/BackgroundTasksApp-test.tsx index 886c779709f..dccd2c33373 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/BackgroundTasksApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/BackgroundTasksApp-test.tsx @@ -19,9 +19,10 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockComponent } from '../../../../helpers/mocks/component'; import { mockLocation, mockRouter } from '../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../helpers/testUtils'; -import BackgroundTasksApp from '../BackgroundTasksApp'; +import { BackgroundTasksApp } from '../BackgroundTasksApp'; jest.mock('../../../../api/ce', () => ({ getTypes: jest.fn().mockResolvedValue({ @@ -88,7 +89,7 @@ it('should render correctly', async () => { function shallowRender(props: Partial<BackgroundTasksApp['props']> = {}) { return shallow( <BackgroundTasksApp - component={{ key: 'foo', id: '564' }} + component={mockComponent({ key: 'foo' })} location={mockLocation()} router={mockRouter()} {...props} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/BackgroundTasksApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/BackgroundTasksApp-test.tsx.snap index 75c79b093c4..5e4d8554894 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/BackgroundTasksApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/BackgroundTasksApp-test.tsx.snap @@ -26,16 +26,48 @@ exports[`should render correctly: loaded 1`] = ` <Header component={ Object { - "id": "564", + "breadcrumbs": Array [], "key": "foo", + "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 [], } } /> <Stats component={ Object { - "id": "564", + "breadcrumbs": Array [], "key": "foo", + "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 [], } } failingCount={15} @@ -46,8 +78,24 @@ exports[`should render correctly: loaded 1`] = ` <Search component={ Object { - "id": "564", + "breadcrumbs": Array [], "key": "foo", + "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 [], } } currents="__ALL__" @@ -71,8 +119,24 @@ exports[`should render correctly: loaded 1`] = ` <Tasks component={ Object { - "id": "564", + "breadcrumbs": Array [], "key": "foo", + "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 [], } } loading={false} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap index 3dadcaf9fbf..44c3899b20b 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap @@ -11,15 +11,10 @@ exports[`renders correctly 1`] = ` </span> <Link className="spacer-right" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "foo", - }, + "search": "?id=foo", } } > @@ -38,15 +33,10 @@ exports[`renders correctly: branch 1`] = ` /> <Link className="spacer-right" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "feature", - "id": "foo", - }, + "search": "?branch=feature&id=foo", } } > @@ -81,15 +71,10 @@ exports[`renders correctly: branch 2`] = ` /> <Link className="spacer-right" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "branch-6.7", - "id": "foo", - }, + "search": "?branch=branch-6.7&id=foo", } } > @@ -128,14 +113,10 @@ exports[`renders correctly: portfolio 1`] = ` </span> <Link className="spacer-right" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/portfolio", - "query": Object { - "id": "foo", - }, + "search": "?id=foo", } } > @@ -154,15 +135,10 @@ exports[`renders correctly: pull request 1`] = ` /> <Link className="spacer-right" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "foo", - "pullRequest": "pr-89", - }, + "search": "?id=foo&pullRequest=pr-89", } } > diff --git a/server/sonar-web/src/main/js/apps/code/routes.ts b/server/sonar-web/src/main/js/apps/background-tasks/routes.tsx index 947e2e92bc0..71f07c4e582 100644 --- a/server/sonar-web/src/main/js/apps/code/routes.ts +++ b/server/sonar-web/src/main/js/apps/background-tasks/routes.tsx @@ -17,12 +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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import BackgroundTasksApp from './components/BackgroundTasksApp'; -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/CodeApp')) } - } -]; +const routes = () => <Route path="background_tasks" element={<BackgroundTasksApp />} />; export default routes; diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordApp-test.tsx.snap index e7e4585e6ae..1403f55f727 100644 --- a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordApp-test.tsx.snap @@ -5,7 +5,6 @@ exports[`should render correctly: admin is not using the default password 1`] = confirmPasswordValue="" location={ Object { - "action": "PUSH", "hash": "", "key": "key", "pathname": "/path", @@ -29,7 +28,6 @@ exports[`should render correctly: default 1`] = ` confirmPasswordValue="" location={ Object { - "action": "PUSH", "hash": "", "key": "key", "pathname": "/path", diff --git a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx index b49a0559215..6383c73a0a0 100644 --- a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx @@ -20,17 +20,17 @@ */ import styled from '@emotion/styled'; import classNames from 'classnames'; -import { Location } from 'history'; import { debounce, intersection } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { InjectedRouter } from 'react-router'; import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; +import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import ListFooter from '../../../components/controls/ListFooter'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; +import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { Alert } from '../../../components/ui/Alert'; import { isPullRequest, isSameBranchLike } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; @@ -55,8 +55,8 @@ interface Props { branchLike?: BranchLike; component: Component; fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>; - location: Pick<Location, 'query'>; - router: Pick<InjectedRouter, 'push'>; + location: Location; + router: Router; metrics: Dict<Metric>; } @@ -402,4 +402,6 @@ const AlertContent = styled.div` align-items: center; `; -export default withBranchStatusActions(withMetricsContext(CodeApp)); +export default withRouter( + withComponentContext(withBranchStatusActions(withMetricsContext(CodeApp))) +); diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx index fae113cae52..6c96b8a34cf 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx @@ -18,13 +18,13 @@ * 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 { colors } from '../../../app/theme'; import BranchIcon from '../../../components/icons/BranchIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; -import { CodeScope, getComponentOverviewUrl } from '../../../helpers/urls'; +import { CodeScope, getComponentOverviewUrl, queryToSearch } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; import { ComponentQualifier, @@ -150,7 +150,7 @@ function renderNameWithIcon( Object.assign(query, { selected: component.key }); } return ( - <Link className="link-with-icon" to={{ pathname: '/code', query }}> + <Link className="link-with-icon" to={{ pathname: '/code', search: queryToSearch(query) }}> <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span> </Link> ); diff --git a/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx b/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx index 7e65b8e497e..58a88ad11f2 100644 --- a/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx @@ -17,9 +17,9 @@ * 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 * as React from 'react'; import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation'; +import { Location } from '../../../components/hoc/withRouter'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; import { scrollToElement } from '../../../helpers/scrolling'; import { BranchLike } from '../../../types/branch-like'; @@ -29,7 +29,7 @@ interface Props { branchLike?: BranchLike; component: string; componentMeasures: Measure[] | undefined; - location: Pick<Location, 'query'>; + location: Location; onIssueChange?: (issue: Issue) => void; } diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx index 08af19dcb29..e0964d7c0c3 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx @@ -21,8 +21,9 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; import { mockComponent, mockComponentMeasure } from '../../../../helpers/mocks/component'; -import { mockIssue, mockRouter } from '../../../../helpers/testMocks'; +import { mockIssue, mockLocation, mockRouter } from '../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { queryToSearch } from '../../../../helpers/urls'; import { ComponentQualifier } from '../../../../types/component'; import { loadMoreChildren, retrieveComponent } from '../../utils'; import { CodeApp } from '../CodeApp'; @@ -161,7 +162,7 @@ it('should handle go to parent correctly', async () => { expect(wrapper.state().highlighted).toBe(breadcrumb); expect(router.push).toHaveBeenCalledWith({ pathname: '/code', - query: { id: 'foo', line: undefined, selected: 'key1' } + search: queryToSearch({ id: 'foo', line: undefined, selected: 'key1' }) }); }); @@ -214,7 +215,7 @@ it('should handle select correctly', () => { wrapper.instance().handleSelect(mockComponentMeasure(true, { refKey: 'test' })); expect(router.push).toHaveBeenCalledWith({ pathname: '/dashboard', - query: { branch: undefined, id: 'test', code_scope: 'new' } + search: queryToSearch({ branch: undefined, id: 'test', code_scope: 'new' }) }); expect(wrapper.state().highlighted).toBeUndefined(); @@ -223,13 +224,13 @@ it('should handle select correctly', () => { wrapper.instance().handleSelect(mockComponentMeasure(true, { refKey: 'test' })); expect(router.push).toHaveBeenCalledWith({ pathname: '/dashboard', - query: { branch: undefined, id: 'test', code_scope: 'overall' } + search: queryToSearch({ branch: undefined, id: 'test', code_scope: 'overall' }) }); wrapper.instance().handleSelect(mockComponentMeasure()); expect(router.push).toHaveBeenCalledWith({ pathname: '/code', - query: { id: 'foo', line: undefined, selected: 'foo' } + search: queryToSearch({ id: 'foo', line: undefined, selected: 'foo' }) }); }); @@ -276,7 +277,7 @@ function shallowRender(props: Partial<CodeApp['props']> = {}) { qualifier: 'FOO' }} fetchBranchStatus={jest.fn()} - location={{ query: { branch: 'b', id: 'foo', line: '7' } }} + location={mockLocation({ search: queryToSearch({ branch: 'b', id: 'foo', line: '7' }) })} metrics={METRICS} router={mockRouter()} {...props} diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap index 8f38318ed18..522ceab3d25 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap @@ -163,15 +163,10 @@ foo:src/index.tsx" > <Link className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/code", - "query": Object { - "id": "foo", - "selected": "foo:src/index.tsx", - }, + "search": "?id=foo&selected=foo%3Asrc%2Findex.tsx", } } > @@ -282,16 +277,10 @@ foo" > <Link className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "code_scope": "new", - "id": "src/main/ts/app", - }, + "search": "?id=src%2Fmain%2Fts%2Fapp&code_scope=new", } } > @@ -320,16 +309,10 @@ foo" > <Link className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "foo", - "code_scope": "new", - "id": "src/main/ts/app", - }, + "search": "?id=src%2Fmain%2Fts%2Fapp&branch=foo&code_scope=new", } } > @@ -369,16 +352,10 @@ foo" > <Link className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "code_scope": "new", - "id": "src/main/ts/app", - }, + "search": "?id=src%2Fmain%2Fts%2Fapp&code_scope=new", } } > diff --git a/server/sonar-web/src/main/js/apps/overview/routes.ts b/server/sonar-web/src/main/js/apps/code/routes.tsx index 9d8f2bd42e4..2674cb21130 100644 --- a/server/sonar-web/src/main/js/apps/overview/routes.ts +++ b/server/sonar-web/src/main/js/apps/code/routes.tsx @@ -17,12 +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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import CodeApp from './components/CodeApp'; -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/App')) } - } -]; +const routes = () => <Route path="code" element={<CodeApp />} />; export default routes; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts index 3665406e76d..ce037b05e6f 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts @@ -317,6 +317,16 @@ it('should be able to bulk deactivate quality profile', async () => { ).toBeInTheDocument(); }); +it('should handle hash parameters', async () => { + renderCodingRulesApp(mockLoggedInUser(), 'coding_rules#languages=c,js|types=BUG'); + + // 2 languages + expect(await screen.findByText('x_selected.2')).toBeInTheDocument(); + expect(screen.getAllByTitle('issue.type.BUG')).toHaveLength(2); + // Only 3 rules shown + expect(screen.getByText('x_of_y_shown.3.3')).toBeInTheDocument(); +}); + function renderCodingRulesApp(currentUser?: CurrentUser, navigateTo?: string) { renderApp('coding_rules', routes, { navigateTo, diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx index 94a9b44e2ea..fa8ce209274 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx @@ -20,7 +20,6 @@ import { keyBy } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { withRouter, WithRouterProps } from 'react-router'; import { Profile, searchQualityProfiles } from '../../../api/quality-profiles'; import { getRulesApp, searchRules } from '../../../api/rules'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; @@ -30,6 +29,7 @@ import ScreenPositionHelper from '../../../components/common/ScreenPositionHelpe import ListFooter from '../../../components/controls/ListFooter'; import SearchBox from '../../../components/controls/SearchBox'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; +import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import BackIcon from '../../../components/icons/BackIcon'; import '../../../components/search-navigator.css'; import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; @@ -79,8 +79,10 @@ const PAGE_SIZE = 100; const MAX_SEARCH_LENGTH = 200; const LIMIT_BEFORE_LOAD_MORE = 5; -interface Props extends WithRouterProps { +interface Props { currentUser: CurrentUser; + location: Location; + router: Router; } interface State { diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx index 4d0fbf62131..569154d2109 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx @@ -19,7 +19,7 @@ */ import { sortBy } from 'lodash'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { deleteRule, searchRules } from '../../../api/rules'; import { Button } from '../../../components/controls/buttons'; import ConfirmButton from '../../../components/controls/ConfirmButton'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx index 1bdf3484a73..b2337639259 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.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 { getFacet } from '../../../api/issues'; import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import Tooltip from '../../../components/controls/Tooltip'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx index 2a3a8cd9e9b..b0582542437 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.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 { ButtonLink } from '../../../components/controls/buttons'; import Dropdown from '../../../components/controls/Dropdown'; import HelpTooltip from '../../../components/controls/HelpTooltip'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx index 4bf55ae6f30..e0ed6fe2146 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx @@ -19,7 +19,7 @@ */ import { filter } from 'lodash'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { activateRule, deactivateRule, Profile } from '../../../api/quality-profiles'; import InstanceMessage from '../../../components/common/InstanceMessage'; import { Button } from '../../../components/controls/buttons'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx index 04d47de7054..28a19498e66 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx @@ -19,7 +19,7 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { deactivateRule, Profile } from '../../../api/quality-profiles'; import { Button } from '../../../components/controls/buttons'; import ConfirmButton from '../../../components/controls/ConfirmButton'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/App-test.tsx index 86ebb0910a7..5b9fab3829d 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/App-test.tsx @@ -112,9 +112,7 @@ function shallowRender(props: Partial<App['props']> = {}) { isLoggedIn: true })} location={mockLocation()} - params={{}} router={mockRouter()} - routes={[]} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx index 763fc8a2761..acfc1083c4a 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-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 { deactivateRule } from '../../../../api/quality-profiles'; import { mockQualityProfile, mockRule } from '../../../../helpers/testMocks'; import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsIssues-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsIssues-test.tsx.snap index 120eb95f601..bdfbd11492e 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsIssues-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsIssues-test.tsx.snap @@ -16,15 +16,10 @@ exports[`should fetch issues and render 1`] = ` > ( <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/issues", - "query": Object { - "resolved": "false", - "rules": "foo", - }, + "search": "?resolved=false&rules=foo", } } > @@ -57,16 +52,10 @@ exports[`should fetch issues and render 1`] = ` className="coding-rules-detail-list-parameters" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/issues", - "query": Object { - "projects": "sample-key", - "resolved": "false", - "rules": "foo", - }, + "search": "?resolved=false&rules=foo&projects=sample-key", } } > @@ -86,16 +75,10 @@ exports[`should fetch issues and render 1`] = ` className="coding-rules-detail-list-parameters" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/issues", - "query": Object { - "projects": "example-key", - "resolved": "false", - "rules": "foo", - }, + "search": "?resolved=false&rules=foo&projects=example-key", } } > diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsMeta-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsMeta-test.tsx.snap index aec10d999a0..cbfba172d8a 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsMeta-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsMeta-test.tsx.snap @@ -17,16 +17,11 @@ exports[`should display right meta info 1`] = ` </span> <Link className="coding-rules-detail-permalink link-no-underline spacer-left text-middle" - onlyActiveOnIndex={false} - style={Object {}} title="permalink" to={ Object { "pathname": "/coding_rules", - "query": Object { - "open": "squid:S1133", - "rule_key": "squid:S1133", - }, + "search": "?open=squid%3AS1133&rule_key=squid%3AS1133", } } > diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap index 9c8e861744c..773efa6f1a9 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap @@ -119,15 +119,10 @@ exports[`should render correctly: default 1`] = ` <Link className="link-no-underline" onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "open": "javascript:S1067", - "rule_key": "javascript:S1067", - }, + "search": "?open=javascript%3AS1067&rule_key=javascript%3AS1067", } } > @@ -221,15 +216,10 @@ exports[`should render correctly: with activation 1`] = ` <Link className="link-no-underline" onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "open": "javascript:S1067", - "rule_key": "javascript:S1067", - }, + "search": "?open=javascript%3AS1067&rule_key=javascript%3AS1067", } } > diff --git a/server/sonar-web/src/main/js/apps/coding-rules/routes.ts b/server/sonar-web/src/main/js/apps/coding-rules/routes.tsx index 5f03341e08b..f3b9c92c99c 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/routes.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/routes.tsx @@ -17,40 +17,48 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { RedirectFunction, RouterState } from 'react-router'; -import { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import React, { useEffect } from 'react'; +import { Route, useLocation, useNavigate } from 'react-router-dom'; import { RawQuery } from '../../types/types'; +import App from './components/App'; import { parseQuery, serializeQuery } from './query'; +const EXPECTED_SPLIT_PARTS = 2; + function parseHash(hash: string): RawQuery { const query: RawQuery = {}; const parts = hash.split('|'); parts.forEach(part => { const tokens = part.split('='); - if (tokens.length === 2) { + if (tokens.length === EXPECTED_SPLIT_PARTS) { query[decodeURIComponent(tokens[0])] = decodeURIComponent(tokens[1]); } }); return query; } -const routes = [ - { - indexRoute: { - onEnter: (nextState: RouterState, replace: RedirectFunction) => { - const { hash } = window.location; - if (hash.length > 1) { - const query = parseHash(hash.substr(1)); - const normalizedQuery = { - ...serializeQuery(parseQuery(query)), - open: query.open - }; - replace({ pathname: nextState.location.pathname, query: normalizedQuery }); - } - }, - component: lazyLoadComponent(() => import('./components/App')) +function HashEditWrapper() { + const location = useLocation(); + const navigate = useNavigate(); + + useEffect(() => { + const { hash } = location; + if (hash.length > 1) { + const query = parseHash(hash.substr(1)); + const normalizedQuery = { + ...serializeQuery(parseQuery(query)), + open: query.open + }; + navigate( + { pathname: location.pathname, search: new URLSearchParams(normalizedQuery).toString() }, + { replace: true } + ); } - } -]; + }, [location, navigate]); + + return <App />; +} + +const routes = () => <Route path="coding_rules" element={<HashEditWrapper />} />; export default routes; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/getHistory-test.ts b/server/sonar-web/src/main/js/apps/component-measures/__tests__/MeasuresApp-it.tsx index 3c934028296..0d65561407f 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/getHistory-test.ts +++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/MeasuresApp-it.tsx @@ -17,13 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { browserHistory } from 'react-router'; -import getHistory from '../getHistory'; +import { screen } from '@testing-library/react'; +import { renderApp } from '../../../helpers/testReactTestingUtils'; +import routes from '../routes'; -it('should get browser history properly', () => { - expect(getHistory()).not.toBeUndefined(); - expect(getHistory().getCurrentLocation().pathname).toBe('/'); - const pathname = '/foo/bar'; - browserHistory.push(pathname); - expect(getHistory().getCurrentLocation().pathname).toBe(pathname); +it('should redirect old history route', () => { + renderMeasuresApp('component_measures/metric/bugs/history'); + + expect( + screen.getByText('/project/activity?graph=custom&custom_metrics=bugs') + ).toBeInTheDocument(); }); + +function renderMeasuresApp(navigateTo?: string) { + renderApp('component_measures', routes, { navigateTo }); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx index 280b6076506..b730f43dbfa 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx @@ -21,13 +21,14 @@ import styled from '@emotion/styled'; import { debounce, keyBy } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { withRouter, WithRouterProps } from 'react-router'; import { getMeasuresWithPeriod } from '../../../api/measures'; import { getAllMetrics } from '../../../api/metrics'; import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; +import { ComponentContext } from '../../../app/components/componentContext/ComponentContext'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; +import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { enhanceMeasure } from '../../../components/measure/utils'; import '../../../components/search-navigator.css'; import { Alert } from '../../../components/ui/Alert'; @@ -73,10 +74,12 @@ import MeasureContent from './MeasureContent'; import MeasureOverviewContainer from './MeasureOverviewContainer'; import MeasuresEmpty from './MeasuresEmpty'; -interface Props extends WithRouterProps { +interface Props { branchLike?: BranchLike; component: ComponentMeasure; fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>; + location: Location; + router: Router; } interface State { @@ -359,4 +362,17 @@ const AlertContent = styled.div` align-items: center; `; -export default withRouter(withBranchStatusActions(App)); +/* + * This needs to be refactored: the issue + * is that we can't use the usual withComponentContext HOC, because the type + * of `component` isn't the same. It probably used to work because of the lazy loading + */ +const WrappedApp = withRouter(withBranchStatusActions(App)); + +function AppWithComponentContext() { + const { branchLike, component } = React.useContext(ComponentContext); + + return <WrappedApp branchLike={branchLike} component={component as ComponentMeasure} />; +} + +export default AppWithComponentContext; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx index 37abf52ec71..37ab7387f28 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx @@ -18,10 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { InjectedRouter } from 'react-router'; import { getComponentTree } from '../../../api/components'; import { getMeasures } from '../../../api/measures'; import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; +import { Router } from '../../../components/hoc/withRouter'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; import PageActions from '../../../components/ui/PageActions'; import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; @@ -62,7 +62,7 @@ interface Props { metrics: Dict<Metric>; onIssueChange?: (issue: Issue) => void; rootComponent: ComponentMeasure; - router: InjectedRouter; + router: Router; selected?: string; asc?: boolean; updateQuery: (query: Partial<Query>) => void; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx index 5a65ec912ca..3911ea503b2 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.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 LanguageDistribution from '../../../components/charts/LanguageDistribution'; import Tooltip from '../../../components/controls/Tooltip'; import HistoryIcon from '../../../components/icons/HistoryIcon'; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx index 3cca0254dcc..3fc1dc8d30e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx @@ -18,8 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { InjectedRouter } from 'react-router'; import { getComponentShow } from '../../../api/components'; +import { Router } from '../../../components/hoc/withRouter'; import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; import { getProjectUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; @@ -43,7 +43,7 @@ interface Props { metrics: Dict<Metric>; onIssueChange?: (issue: Issue) => void; rootComponent: ComponentMeasure; - router: InjectedRouter; + router: Router; selected?: string; updateQuery: (query: Partial<Query>) => void; } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx index fd9cfdfd2fd..61d1a6ab94e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx @@ -164,9 +164,7 @@ function shallowRender(props: Partial<App['props']> = {}) { component={mockComponent({ key: 'foo', name: 'Foo' })} fetchBranchStatus={jest.fn()} location={mockLocation({ pathname: '/component_measures', query: { metric: 'coverage' } })} - params={{}} router={mockRouter()} - routes={[]} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap index 643a2bb9dd8..8235bbe62da 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap @@ -31,16 +31,10 @@ exports[`should render correctly 1`] = ` > <Link className="js-show-history spacer-left button button-small" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/project/activity", - "query": Object { - "custom_metrics": "reliability_rating", - "graph": "custom", - "id": "foo", - }, + "search": "?id=foo&graph=custom&custom_metrics=reliability_rating", } } > @@ -212,16 +206,10 @@ exports[`should work with measure without value 1`] = ` > <Link className="js-show-history spacer-left button button-small" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/project/activity", - "query": Object { - "custom_metrics": "reliability_rating", - "graph": "custom", - "id": "foo", - }, + "search": "?id=foo&graph=custom&custom_metrics=reliability_rating", } } > diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx index 1d33534d3b9..585601da5a6 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx @@ -17,9 +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 { LocationDescriptor } from 'history'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link, To } from 'react-router-dom'; import BranchIcon from '../../../components/icons/BranchIcon'; import LinkIcon from '../../../components/icons/LinkIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon'; @@ -64,7 +63,7 @@ export default function ComponentCell(props: ComponentCellProps) { ({ head, tail } = splitPath(component.path)); } - let path: LocationDescriptor; + let path: To; const targetKey = component.refKey || rootComponent.key; const selectionKey = component.refKey ? '' : component.key; diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx index d93d13f3046..1465e2a1351 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-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 { mockComponentMeasure, mockComponentMeasureEnhanced @@ -63,7 +63,12 @@ it('should properly deal with key and refKey', () => { }) .find(Link) .props().to - ).toEqual(expect.objectContaining({ query: expect.objectContaining({ id: 'port-key' }) })); + ).toEqual( + expect.objectContaining({ + pathname: '/component_measures', + search: '?id=port-key&metric=bugs&view=list' + }) + ); expect( shallowRender() @@ -71,7 +76,8 @@ it('should properly deal with key and refKey', () => { .props().to ).toEqual( expect.objectContaining({ - query: expect.objectContaining({ id: 'foo', selected: 'foo:src/index.tsx' }) + pathname: '/component_measures', + search: '?id=foo&metric=bugs&view=list&selected=foo%3Asrc%2Findex.tsx' }) ); }); @@ -80,62 +86,66 @@ it.each([ [ ComponentQualifier.File, MetricKey.bugs, - expect.objectContaining({ + { pathname: '/component_measures', - query: expect.objectContaining({ branch: 'develop' }) - }) + search: `?id=foo&metric=${MetricKey.bugs}&branch=develop&view=list&selected=foo` + } ], [ ComponentQualifier.Directory, MetricKey.bugs, - expect.objectContaining({ + { pathname: '/component_measures', - query: expect.objectContaining({ branch: 'develop' }) - }) + search: `?id=foo&metric=${MetricKey.bugs}&branch=develop&view=list&selected=foo` + } ], [ ComponentQualifier.Project, MetricKey.projects, - expect.objectContaining({ + { pathname: '/dashboard', - query: expect.objectContaining({ branch: 'develop' }) - }) + search: '?id=foo&branch=develop' + } ], [ ComponentQualifier.Application, MetricKey.releasability_rating, - expect.objectContaining({ + { pathname: '/dashboard', - query: expect.objectContaining({ branch: 'develop' }) - }) + search: '?id=foo&branch=develop' + } ], [ ComponentQualifier.Project, MetricKey.releasability_rating, - expect.objectContaining({ + { pathname: '/dashboard', - query: expect.objectContaining({ branch: 'develop' }) - }) + search: '?id=foo&branch=develop' + } ], [ ComponentQualifier.Application, MetricKey.alert_status, - expect.objectContaining({ + { pathname: '/dashboard', - query: expect.objectContaining({ branch: 'develop' }) - }) + search: '?id=foo&branch=develop' + } ], [ ComponentQualifier.Project, MetricKey.alert_status, - expect.objectContaining({ + { pathname: '/dashboard', - query: expect.objectContaining({ branch: 'develop' }) - }) + search: '?id=foo&branch=develop' + } ] ])( 'should display the proper link path for %s component qualifier and %s metric key', - (componentQualifier: ComponentQualifier, metricKey: MetricKey, expectedTo: any) => { + ( + componentQualifier: ComponentQualifier, + metricKey: MetricKey, + expectedTo: { pathname: string; search: string } + ) => { const wrapper = shallowRender( { component: mockComponentMeasureEnhanced({ @@ -146,7 +156,7 @@ it.each([ metricKey ); - expect(wrapper.find(Link).props().to).toEqual(expectedTo); + expect(wrapper.find(Link).props().to).toEqual(expect.objectContaining(expectedTo)); } ); diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap index 730e6e4fb06..96fcb66989e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap @@ -10,19 +10,10 @@ exports[`should render correctly for a "APP" root component and a component with <Link className="link-no-underline" id="component-measures-component-link-foo" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/component_measures", - "query": Object { - "asc": undefined, - "branch": "develop", - "id": "foo", - "metric": "bugs", - "selected": "foo", - "view": "list", - }, + "search": "?id=foo&metric=bugs&branch=develop&view=list&selected=foo", } } > @@ -60,18 +51,10 @@ exports[`should render correctly for a "APP" root component and a component with <Link className="link-no-underline" id="component-measures-component-link-foo" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/component_measures", - "query": Object { - "asc": undefined, - "id": "foo", - "metric": "bugs", - "selected": "foo", - "view": "list", - }, + "search": "?id=foo&metric=bugs&view=list&selected=foo", } } > @@ -106,19 +89,10 @@ exports[`should render correctly for a "TRK" root component and a component with <Link className="link-no-underline" id="component-measures-component-link-foo" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/component_measures", - "query": Object { - "asc": undefined, - "branch": "develop", - "id": "foo", - "metric": "bugs", - "selected": "foo", - "view": "list", - }, + "search": "?id=foo&metric=bugs&branch=develop&view=list&selected=foo", } } > @@ -148,18 +122,10 @@ exports[`should render correctly for a "TRK" root component and a component with <Link className="link-no-underline" id="component-measures-component-link-foo" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/component_measures", - "query": Object { - "asc": undefined, - "id": "foo", - "metric": "bugs", - "selected": "foo", - "view": "list", - }, + "search": "?id=foo&metric=bugs&view=list&selected=foo", } } > @@ -189,19 +155,10 @@ exports[`should render correctly for a "VW" root component and a component with <Link className="link-no-underline" id="component-measures-component-link-foo" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/component_measures", - "query": Object { - "asc": undefined, - "branch": "develop", - "id": "foo", - "metric": "bugs", - "selected": "foo", - "view": "list", - }, + "search": "?id=foo&metric=bugs&branch=develop&view=list&selected=foo", } } > @@ -239,18 +196,10 @@ exports[`should render correctly for a "VW" root component and a component with <Link className="link-no-underline" id="component-measures-component-link-foo" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/component_measures", - "query": Object { - "asc": undefined, - "id": "foo", - "metric": "bugs", - "selected": "foo", - "view": "list", - }, + "search": "?id=foo&metric=bugs&view=list&selected=foo", } } > @@ -285,18 +234,10 @@ exports[`should render correctly: default 1`] = ` <Link className="link-no-underline" id="component-measures-component-link-foo:src/index.tsx" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/component_measures", - "query": Object { - "asc": undefined, - "id": "foo", - "metric": "bugs", - "selected": "foo:src/index.tsx", - "view": "list", - }, + "search": "?id=foo&metric=bugs&view=list&selected=foo%3Asrc%2Findex.tsx", } } > diff --git a/server/sonar-web/src/main/js/apps/component-measures/routes.ts b/server/sonar-web/src/main/js/apps/component-measures/routes.ts deleted file mode 100644 index 2430817f5f4..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/routes.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 { RedirectFunction, RouterState } from 'react-router'; -import { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/App')) } - }, - { - path: 'domain/:domainName', - onEnter(nextState: RouterState, replace: RedirectFunction) { - replace({ - pathname: '/component_measures', - query: { - ...nextState.location.query, - metric: nextState.params.domainName - } - }); - } - }, - { - path: 'metric/:metricKey(/:view)', - onEnter(nextState: RouterState, replace: RedirectFunction) { - if (nextState.params.view === 'history') { - replace({ - pathname: '/project/activity', - query: { - id: nextState.location.query.id, - graph: 'custom', - custom_metrics: nextState.params.metricKey - } - }); - } else { - replace({ - pathname: '/component_measures', - query: { - ...nextState.location.query, - metric: nextState.params.metricKey, - view: nextState.params.view - } - }); - } - } - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/component-measures/routes.tsx b/server/sonar-web/src/main/js/apps/component-measures/routes.tsx new file mode 100644 index 00000000000..04e9d573fb4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/routes.tsx @@ -0,0 +1,79 @@ +/* + * 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 React from 'react'; +import { Navigate, Route, useParams, useSearchParams } from 'react-router-dom'; +import NavigateWithParams from '../../app/utils/NavigateWithParams'; +import { omitNil } from '../../helpers/request'; +import { searchParamsToQuery } from '../../helpers/urls'; +import App from './components/App'; + +const routes = () => ( + <Route path="component_measures"> + <Route index={true} element={<App />} /> + <Route + path="domain/:domainName" + element={ + <NavigateWithParams + pathname="/component_measures" + transformParams={params => + omitNil({ + metric: params['domainName'] + }) + } + /> + } + /> + + <Route path="metric/:metricKey" element={<MetricRedirect />} /> + <Route path="metric/:metricKey/:view" element={<MetricRedirect />} /> + </Route> +); + +function MetricRedirect() { + const params = useParams(); + const [searchParams] = useSearchParams(); + + if (params.view === 'history') { + const to = { + pathname: '/project/activity', + search: new URLSearchParams( + omitNil({ + id: searchParams.get('id') ?? undefined, + graph: 'custom', + custom_metrics: params.metricKey + }) + ).toString() + }; + return <Navigate to={to} replace={true} />; + } + const to = { + pathname: '/component_measures', + search: new URLSearchParams( + omitNil({ + ...searchParamsToQuery(searchParams), + metric: params.metricKey, + view: params.view + }) + ).toString() + }; + return <Navigate to={to} replace={true} />; +} + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx index 7fd11744ddf..ec51beea670 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx @@ -20,7 +20,7 @@ import classNames from 'classnames'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { colors } from '../../../app/theme'; import BoxedGroupAccordion from '../../../components/controls/BoxedGroupAccordion'; import ListFooter from '../../../components/controls/ListFooter'; @@ -29,7 +29,7 @@ import CheckIcon from '../../../components/icons/CheckIcon'; import { Alert } from '../../../components/ui/Alert'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; -import { getProjectUrl } from '../../../helpers/urls'; +import { getProjectUrl, queryToSearch } from '../../../helpers/urls'; import { AzureProject, AzureRepository } from '../../../types/alm-integration'; import { CreateProjectModes } from './types'; @@ -110,7 +110,7 @@ export default function AzureProjectAccordion(props: AzureProjectAccordionProps) <Link to={{ pathname: '/projects/create', - query: { mode: CreateProjectModes.AzureDevOps, resetPat: 1 } + search: queryToSearch({ mode: CreateProjectModes.AzureDevOps, resetPat: 1 }) }}> {translate('onboarding.create_project.update_your_token')} </Link> diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx index 9084102b075..963e3dd32c8 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx @@ -19,7 +19,6 @@ */ import { groupBy } from 'lodash'; import * as React from 'react'; -import { WithRouterProps } from 'react-router'; import { checkPersonalAccessTokenIsValid, getAzureProjects, @@ -28,16 +27,19 @@ import { searchAzureRepositories, setAlmPersonalAccessToken } from '../../../api/alm-integrations'; +import { Location, Router } from '../../../components/hoc/withRouter'; import { AzureProject, AzureRepository } from '../../../types/alm-integration'; import { AlmSettingsInstance } from '../../../types/alm-settings'; import { Dict } from '../../../types/types'; import AzureCreateProjectRenderer from './AzureProjectCreateRenderer'; -interface Props extends Pick<WithRouterProps, 'location' | 'router'> { +interface Props { canAdmin: boolean; loadingBindings: boolean; onProjectCreate: (projectKey: string) => void; settings: AlmSettingsInstance[]; + location: Location; + router: Router; } interface State { diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx index 51e3911aa03..c4965a8dca7 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.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 { Button } from '../../../components/controls/buttons'; import SearchBox from '../../../components/controls/SearchBox'; import { Alert } from '../../../components/ui/Alert'; diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx index 0bbea571367..19f38414288 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx @@ -19,10 +19,11 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import ListFooter from '../../../components/controls/ListFooter'; import { Alert } from '../../../components/ui/Alert'; import { translate } from '../../../helpers/l10n'; +import { queryToSearch } from '../../../helpers/urls'; import { AzureProject, AzureRepository } from '../../../types/alm-integration'; import { Dict } from '../../../types/types'; import AzureProjectAccordion from './AzureProjectAccordion'; @@ -73,7 +74,7 @@ export default function AzureProjectsList(props: AzureProjectsListProps) { <Link to={{ pathname: '/projects/create', - query: { mode: CreateProjectModes.AzureDevOps, resetPat: 1 } + search: queryToSearch({ mode: CreateProjectModes.AzureDevOps, resetPat: 1 }) }}> {translate('onboarding.create_project.update_your_token')} </Link> diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreate.tsx index cd1274896c7..74fe4a49838 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreate.tsx @@ -18,21 +18,23 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { WithRouterProps } from 'react-router'; import { importBitbucketCloudRepository, searchForBitbucketCloudRepositories } from '../../../api/alm-integrations'; +import { Location, Router } from '../../../components/hoc/withRouter'; import { BitbucketCloudRepository } from '../../../types/alm-integration'; import { AlmSettingsInstance } from '../../../types/alm-settings'; import { Paging } from '../../../types/types'; import BitbucketCloudProjectCreateRenderer from './BitbucketCloudProjectCreateRender'; -interface Props extends Pick<WithRouterProps, 'location' | 'router'> { +interface Props { canAdmin: boolean; settings: AlmSettingsInstance[]; loadingBindings: boolean; onProjectCreate: (projectKey: string) => void; + location: Location; + router: Router; } interface State { diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudSearchForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudSearchForm.tsx index 442ac14aee0..03cb73fc033 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudSearchForm.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudSearchForm.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 { Button } from '../../../components/controls/buttons'; import SearchBox from '../../../components/controls/SearchBox'; import Tooltip from '../../../components/controls/Tooltip'; @@ -30,7 +30,7 @@ import { Alert } from '../../../components/ui/Alert'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; -import { getProjectUrl } from '../../../helpers/urls'; +import { getProjectUrl, queryToSearch } from '../../../helpers/urls'; import { BitbucketCloudRepository } from '../../../types/alm-integration'; import { ComponentQualifier } from '../../../types/component'; import { CreateProjectModes } from './types'; @@ -72,7 +72,7 @@ export default function BitbucketCloudSearchForm(props: BitbucketCloudSearchForm <Link to={{ pathname: '/projects/create', - query: { mode: CreateProjectModes.BitbucketCloud, resetPat: 1 } + search: queryToSearch({ mode: CreateProjectModes.BitbucketCloud, resetPat: 1 }) }}> {translate('onboarding.create_project.update_your_token')} </Link> diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketImportRepositoryForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketImportRepositoryForm.tsx index cdbafd18724..d1a741729a7 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketImportRepositoryForm.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketImportRepositoryForm.tsx @@ -19,10 +19,11 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import SearchBox from '../../../components/controls/SearchBox'; import { Alert } from '../../../components/ui/Alert'; import { translate } from '../../../helpers/l10n'; +import { queryToSearch } from '../../../helpers/urls'; import { BitbucketProject, BitbucketProjectRepositories, @@ -64,7 +65,7 @@ export default function BitbucketImportRepositoryForm(props: BitbucketImportRepo <Link to={{ pathname: '/projects/create', - query: { mode: CreateProjectModes.BitbucketServer, resetPat: 1 } + search: queryToSearch({ mode: CreateProjectModes.BitbucketServer, resetPat: 1 }) }}> {translate('onboarding.create_project.update_your_token')} </Link> diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectAccordion.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectAccordion.tsx index 9f220c1b39f..b1c05705ec8 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectAccordion.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectAccordion.tsx @@ -20,14 +20,14 @@ import classNames from 'classnames'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { colors } from '../../../app/theme'; import BoxedGroupAccordion from '../../../components/controls/BoxedGroupAccordion'; import Radio from '../../../components/controls/Radio'; import CheckIcon from '../../../components/icons/CheckIcon'; import { Alert } from '../../../components/ui/Alert'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { getProjectUrl } from '../../../helpers/urls'; +import { getProjectUrl, queryToSearch } from '../../../helpers/urls'; import { BitbucketProject, BitbucketRepository } from '../../../types/alm-integration'; import { CreateProjectModes } from './types'; @@ -85,7 +85,10 @@ export default function BitbucketProjectAccordion(props: BitbucketProjectAccordi <Link to={{ pathname: '/projects/create', - query: { mode: CreateProjectModes.BitbucketServer, resetPat: 1 } + search: queryToSearch({ + mode: CreateProjectModes.BitbucketServer, + resetPat: 1 + }) }}> {translate('onboarding.create_project.update_your_token')} </Link> diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx index ba764483aa7..5c56e0683c5 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx @@ -18,13 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { WithRouterProps } from 'react-router'; import { getBitbucketServerProjects, getBitbucketServerRepositories, importBitbucketServerProject, searchForBitbucketServerRepositories } from '../../../api/alm-integrations'; +import { Location, Router } from '../../../components/hoc/withRouter'; import { BitbucketProject, BitbucketProjectRepositories, @@ -34,11 +34,13 @@ import { AlmSettingsInstance } from '../../../types/alm-settings'; import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer'; import { DEFAULT_BBS_PAGE_SIZE } from './constants'; -interface Props extends Pick<WithRouterProps, 'location' | 'router'> { +interface Props { canAdmin: boolean; bitbucketSettings: AlmSettingsInstance[]; loadingBindings: boolean; onProjectCreate: (projectKey: string) => void; + location: Location; + router: Router; } interface State { diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx index 1c793becabc..384f7906a66 100644 --- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx @@ -19,10 +19,10 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { WithRouterProps } from 'react-router'; import { getAlmSettings } from '../../../api/alm-settings'; import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; +import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { translate } from '../../../helpers/l10n'; import { getProjectUrl } from '../../../helpers/urls'; import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; @@ -38,8 +38,10 @@ import ManualProjectCreate from './ManualProjectCreate'; import './style.css'; import { CreateProjectModes } from './types'; -interface Props extends Pick<WithRouterProps, 'router' | 'location'> { +interface Props { appState: AppState; + location: Location; + router: Router; } interface State { @@ -270,4 +272,4 @@ export class CreateProjectPage extends React.PureComponent<Props, State> { } } -export default withAppStateContext(CreateProjectPage); +export default withRouter(withAppStateContext(CreateProjectPage)); diff --git a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx index 12e69d5816d..2871ac8eda8 100644 --- a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx @@ -19,24 +19,26 @@ */ import { debounce } from 'lodash'; import * as React from 'react'; -import { WithRouterProps } from 'react-router'; import { getGithubClientId, getGithubOrganizations, getGithubRepositories, importGithubRepository } from '../../../api/alm-integrations'; +import { Location, Router } from '../../../components/hoc/withRouter'; import { getHostUrl } from '../../../helpers/urls'; import { GithubOrganization, GithubRepository } from '../../../types/alm-integration'; import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; import { Paging } from '../../../types/types'; import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer'; -interface Props extends Pick<WithRouterProps, 'location' | 'router'> { +interface Props { canAdmin: boolean; loadingBindings: boolean; onProjectCreate: (projectKey: string) => void; settings: AlmSettingsInstance[]; + location: Location; + router: Router; } interface State { diff --git a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx index e801b649039..7fbfedb3606 100644 --- a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { colors } from '../../../app/theme'; import { Button } from '../../../components/controls/buttons'; import ListFooter from '../../../components/controls/ListFooter'; diff --git a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx index 6dfa1f8d974..f1dab9ad7be 100644 --- a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx @@ -18,18 +18,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { WithRouterProps } from 'react-router'; import { getGitlabProjects, importGitlabProject } from '../../../api/alm-integrations'; +import { Location, Router } from '../../../components/hoc/withRouter'; import { GitlabProject } from '../../../types/alm-integration'; import { AlmSettingsInstance } from '../../../types/alm-settings'; import { Paging } from '../../../types/types'; import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer'; -interface Props extends Pick<WithRouterProps, 'location' | 'router'> { +interface Props { canAdmin: boolean; loadingBindings: boolean; onProjectCreate: (projectKey: string) => void; settings: AlmSettingsInstance[]; + location: Location; + router: Router; } interface State { diff --git a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectSelectionForm.tsx b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectSelectionForm.tsx index 5726f4b4c23..bdbcc5f93be 100644 --- a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectSelectionForm.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectSelectionForm.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 { Button } from '../../../components/controls/buttons'; import ListFooter from '../../../components/controls/ListFooter'; import SearchBox from '../../../components/controls/SearchBox'; @@ -30,7 +30,7 @@ import QualifierIcon from '../../../components/icons/QualifierIcon'; import { Alert } from '../../../components/ui/Alert'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; -import { getProjectUrl } from '../../../helpers/urls'; +import { getProjectUrl, queryToSearch } from '../../../helpers/urls'; import { GitlabProject } from '../../../types/alm-integration'; import { ComponentQualifier } from '../../../types/component'; import { Paging } from '../../../types/types'; @@ -69,7 +69,7 @@ export default function GitlabProjectSelectionForm(props: GitlabProjectSelection <Link to={{ pathname: '/projects/create', - query: { mode: CreateProjectModes.GitLab, resetPat: 1 } + search: queryToSearch({ mode: CreateProjectModes.GitLab, resetPat: 1 }) }}> {translate('onboarding.create_project.update_your_token')} </Link> diff --git a/server/sonar-web/src/main/js/apps/create/project/WrongBindingCountAlert.tsx b/server/sonar-web/src/main/js/apps/create/project/WrongBindingCountAlert.tsx index bed32e27e6f..821d2a1ed7f 100644 --- a/server/sonar-web/src/main/js/apps/create/project/WrongBindingCountAlert.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/WrongBindingCountAlert.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 { getGlobalSettingsUrl } from '../../../helpers/urls'; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectAccordion-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectAccordion-test.tsx.snap index 75ce5ede02c..85fc0eecdd7 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectAccordion-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectAccordion-test.tsx.snap @@ -151,16 +151,11 @@ exports[`should render correctly: search results 1`] = ` className="little-spacer-bottom text-ellipsis" > <Link - onlyActiveOnIndex={false} - style={Object {}} title="SQ Name" to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "sq-key", - }, + "search": "?id=sq-key", } } > @@ -236,16 +231,11 @@ exports[`should render correctly: with repositories 1`] = ` className="little-spacer-bottom text-ellipsis" > <Link - onlyActiveOnIndex={false} - style={Object {}} title="SQ Name" to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "sq-key", - }, + "search": "?id=sq-key", } } > diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap index dfcbf392e44..d39ae29d2d9 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap @@ -165,14 +165,10 @@ exports[`should render correctly: setting missing url, admin 1`] = ` Object { "alm": "onboarding.alm.azure", "url": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/admin/settings", - "query": Object { - "category": "almintegration", - }, + "search": "?category=almintegration", } } > diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap index f26d19c132b..271f606cb68 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap @@ -36,15 +36,10 @@ exports[`should render correctly: empty 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/projects/create", - "query": Object { - "mode": "azure", - "resetPat": 1, - }, + "search": "?mode=azure&resetPat=1", } } > diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudSearchForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudSearchForm-test.tsx.snap index 2f9463a21fb..bbaa3079000 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudSearchForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudSearchForm-test.tsx.snap @@ -11,15 +11,10 @@ exports[`Should render correctly 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/projects/create", - "query": Object { - "mode": "bitbucketcloud", - "resetPat": 1, - }, + "search": "?mode=bitbucketcloud&resetPat=1", } } > @@ -109,15 +104,10 @@ exports[`Should render correctly: Importing 1`] = ` className="project-name display-inline-block text-ellipsis" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "sq-key", - }, + "search": "?id=sq-key", } } > @@ -373,15 +363,10 @@ exports[`Should render correctly: Show more 1`] = ` className="project-name display-inline-block text-ellipsis" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "sq-key", - }, + "search": "?id=sq-key", } } > diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap index fa7fa8fc11d..171179efbaf 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap @@ -62,15 +62,10 @@ exports[`should render correctly: no projects 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/projects/create", - "query": Object { - "mode": "bitbucket", - "resetPat": 1, - }, + "search": "?mode=bitbucket&resetPat=1", } } > diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap index e995e7e8379..4afde710ce4 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap @@ -60,15 +60,10 @@ exports[`should render correctly: default 1`] = ` title="Bar" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "bar", - }, + "search": "?id=bar", } } > @@ -132,15 +127,10 @@ exports[`should render correctly: disable options 1`] = ` title="Bar" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "bar", - }, + "search": "?id=bar", } } > @@ -204,15 +194,10 @@ exports[`should render correctly: no click handler 1`] = ` title="Bar" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "bar", - }, + "search": "?id=bar", } } > @@ -276,15 +261,10 @@ exports[`should render correctly: no project info 1`] = ` title="Bar" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "bar", - }, + "search": "?id=bar", } } > @@ -324,15 +304,10 @@ exports[`should render correctly: no repos 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/projects/create", - "query": Object { - "mode": "bitbucket", - "resetPat": 1, - }, + "search": "?mode=bitbucket&resetPat=1", } } > @@ -393,15 +368,10 @@ exports[`should render correctly: not showing all repos 1`] = ` title="Bar" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "bar", - }, + "search": "?id=bar", } } > @@ -470,15 +440,10 @@ exports[`should render correctly: selected repo 1`] = ` title="Bar" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "bar", - }, + "search": "?id=bar", } } > diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap index 70474361ec7..1d7755f6e7a 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap @@ -96,7 +96,6 @@ exports[`should render correctly for azure mode 1`] = ` loadingBindings={true} location={ Object { - "action": "PUSH", "hash": "", "key": "key", "pathname": "/path", @@ -149,7 +148,6 @@ exports[`should render correctly for bitbucket mode 1`] = ` loadingBindings={true} location={ Object { - "action": "PUSH", "hash": "", "key": "key", "pathname": "/path", @@ -200,7 +198,6 @@ exports[`should render correctly for bitbucketcloud mode 1`] = ` loadingBindings={true} location={ Object { - "action": "PUSH", "hash": "", "key": "key", "pathname": "/path", @@ -252,7 +249,6 @@ exports[`should render correctly for github mode 1`] = ` loadingBindings={true} location={ Object { - "action": "PUSH", "hash": "", "key": "key", "pathname": "/path", @@ -304,7 +300,6 @@ exports[`should render correctly for gitlab mode 1`] = ` loadingBindings={true} location={ Object { - "action": "PUSH", "hash": "", "key": "key", "pathname": "/path", diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap index 56300a11343..8179e528969 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap @@ -112,8 +112,6 @@ exports[`should render correctly: error for admin 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to="/admin/settings?category=almintegration" > onboarding.create_project.github.warning.message_admin.link @@ -375,15 +373,10 @@ exports[`should render correctly: repositories 1`] = ` > <Link className="display-flex-center max-width-60" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "repo2", - }, + "search": "?id=repo2", } } > diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectSelectionForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectSelectionForm-test.tsx.snap index f179ea42dac..daa8b66ab26 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectSelectionForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectSelectionForm-test.tsx.snap @@ -75,15 +75,10 @@ exports[`should render correctly: importing 1`] = ` className="project-name display-inline-block text-ellipsis" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "already-imported", - }, + "search": "?id=already-imported", } } > @@ -154,15 +149,10 @@ exports[`should render correctly: no projects 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/projects/create", - "query": Object { - "mode": "gitlab", - "resetPat": 1, - }, + "search": "?mode=gitlab&resetPat=1", } } > @@ -276,15 +266,10 @@ exports[`should render correctly: projects 1`] = ` className="project-name display-inline-block text-ellipsis" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "already-imported", - }, + "search": "?id=already-imported", } } > @@ -355,15 +340,10 @@ exports[`should render correctly: undefined projects 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/projects/create", - "query": Object { - "mode": "gitlab", - "resetPat": 1, - }, + "search": "?mode=gitlab&resetPat=1", } } > diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/WrongBindingCountAlert-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/WrongBindingCountAlert-test.tsx.snap index b30756a2619..7dff8982253 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/WrongBindingCountAlert-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/WrongBindingCountAlert-test.tsx.snap @@ -27,14 +27,10 @@ exports[`should render correctly: for admin 1`] = ` Object { "alm": "onboarding.alm.bitbucket", "url": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/admin/settings", - "query": Object { - "category": "almintegration", - }, + "search": "?category=almintegration", } } > diff --git a/server/sonar-web/src/main/js/apps/documentation/components/App.tsx b/server/sonar-web/src/main/js/apps/documentation/components/App.tsx index a594ce38dec..05d14b82c1c 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/documentation/components/App.tsx @@ -21,7 +21,7 @@ import * as navigationTreeSonarQube from 'Docs/../static/SonarQubeNavigationTree import { DocNavigationItem } from 'Docs/@types/types'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { Link } from 'react-router'; +import { Link, useLocation, useParams } from 'react-router-dom'; import { getInstalledPlugins } from '../../../api/plugins'; import { getPluginStaticFileContent } from '../../../api/static'; import NotFound from '../../../app/components/NotFound'; @@ -54,7 +54,7 @@ interface State { const LANGUAGES_BASE_URL = 'analysis/languages'; -export default class App extends React.PureComponent<Props, State> { +export class App extends React.PureComponent<Props, State> { mounted = false; state: State = { loading: false, @@ -227,3 +227,10 @@ export default class App extends React.PureComponent<Props, State> { ); } } + +export default function AppWrapper() { + const params = useParams(); + const location = useLocation(); + + return <App params={{ splat: params['*'] }} location={location} />; +} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/MenuItem.tsx b/server/sonar-web/src/main/js/apps/documentation/components/MenuItem.tsx index 2b51c32ef8f..4e57fee57aa 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/MenuItem.tsx +++ b/server/sonar-web/src/main/js/apps/documentation/components/MenuItem.tsx @@ -19,7 +19,7 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { testPathAgainstUrl } from '../navTreeUtils'; import { DocumentationEntry } from '../utils'; diff --git a/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx b/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx index b4da77ead35..6490f365a16 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx +++ b/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx @@ -19,7 +19,7 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { Dict } from '../../../types/types'; import { cutWords, DocumentationEntry, highlightMarks } from '../utils'; diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/App-test.tsx index 85d7439ecf1..bc3b3c3ef48 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/App-test.tsx @@ -24,7 +24,7 @@ import { request } from '../../../../helpers/request'; import { waitAndUpdate } from '../../../../helpers/testUtils'; import { InstalledPlugin } from '../../../../types/plugins'; import getPages from '../../pages'; -import App from '../App'; +import { App } from '../App'; jest.mock('../../../../components/common/ScreenPositionHelper'); diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/App-test.tsx.snap index 94824017b6d..a27c118ffa2 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/App-test.tsx.snap @@ -69,8 +69,6 @@ exports[`should render correctly for SonarQube 2`] = ` weight={10} /> <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/" > <h1> diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuItem-test.tsx.snap index 795f0dbdd79..73eca5dd152 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuItem-test.tsx.snap @@ -4,8 +4,6 @@ exports[`should not render a high depth differently than a depth of 3 1`] = ` <Link className="list-group-item depth-3" key="/bar" - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/bar" > <h3 @@ -18,8 +16,6 @@ exports[`should render correctly 1`] = ` <Link className="list-group-item" key="/bar" - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/bar" > <h3 @@ -32,8 +28,6 @@ exports[`should render correctly if the current node matches the splat 1`] = ` <Link className="list-group-item active" key="/bar" - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/bar" > <h3 diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap index c45f65fc1a5..2a50912a76b 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap @@ -3,8 +3,6 @@ exports[`SearchResultEntry should render 1`] = ` <Link className="list-group-item active" - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/foo/bar" > <SearchResultTitle diff --git a/server/sonar-web/src/main/js/apps/documentation/routes.tsx b/server/sonar-web/src/main/js/apps/documentation/routes.tsx new file mode 100644 index 00000000000..c7be80023d0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/documentation/routes.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 React from 'react'; +import { Route } from 'react-router-dom'; +import App from './components/App'; + +const routes = () => ( + <Route path="documentation"> + <Route index={true} element={<App />} /> + <Route path="*" element={<App />} /> + </Route> +); + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/users/routes.ts b/server/sonar-web/src/main/js/apps/groups/routes.tsx index 90ec2e15515..945f3bba3d8 100644 --- a/server/sonar-web/src/main/js/apps/users/routes.ts +++ b/server/sonar-web/src/main/js/apps/groups/routes.tsx @@ -17,12 +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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import App from './components/App'; -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./UsersApp')) } - } -]; +const routes = () => <Route path="groups" element={<App />} />; export default routes; diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx index 65066ba0845..c6c177b5b85 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx @@ -19,10 +19,13 @@ */ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import React from 'react'; import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; import { renderOwaspTop102021Category } from '../../../helpers/security-standard'; -import { renderComponentApp } from '../../../helpers/testReactTestingUtils'; +import { renderApp, renderComponentApp } from '../../../helpers/testReactTestingUtils'; +import { IssueType } from '../../../types/issues'; import AppContainer from '../components/AppContainer'; +import { projectIssuesRoutes } from '../routes'; jest.mock('../../../api/issues'); jest.mock('../../../api/rules'); @@ -106,6 +109,28 @@ it('should support OWASP Top 10 version 2021', async () => { ); }); +describe('redirects', () => { + it('should work for hotspots', () => { + renderProjectIssuesApp(`project/issues?types=${IssueType.SecurityHotspot}`); + + expect(screen.getByText('/security_hotspots?assignedToMe=false')).toBeInTheDocument(); + }); + + it('should filter out hotspots', async () => { + renderProjectIssuesApp( + `project/issues?types=${IssueType.SecurityHotspot},${IssueType.CodeSmell}` + ); + + expect( + await screen.findByRole('link', { name: `issue.type.${IssueType.CodeSmell}` }) + ).toBeInTheDocument(); + }); +}); + function renderIssueApp() { - renderComponentApp('project/issues', AppContainer); + renderComponentApp('project/issues', <AppContainer />); +} + +function renderProjectIssuesApp(navigateTo?: string) { + renderApp('project/issues', projectIssuesRoutes, { navigateTo }); } diff --git a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx index e3060ab0d0f..04630efd53c 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx @@ -19,9 +19,14 @@ */ import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; +import { PageContext } from '../../../app/components/indexation/PageUnavailableDueToIndexation'; +import withIndexationGuard from '../../../components/hoc/withIndexationGuard'; import { withRouter } from '../../../components/hoc/withRouter'; import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; const IssuesAppContainer = lazyLoadComponent(() => import('./IssuesApp'), 'IssuesAppContainer'); -export default withRouter(withCurrentUserContext(withBranchStatusActions(IssuesAppContainer))); +export default withIndexationGuard( + withRouter(withCurrentUserContext(withBranchStatusActions(IssuesAppContainer))), + PageContext.Issues +); diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx index cdb493c95f4..d234224c8d4 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx @@ -19,7 +19,7 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import BoxedTabs from '../../../components/controls/BoxedTabs'; import { translate } from '../../../helpers/l10n'; import { sanitizeString } from '../../../helpers/sanitize'; diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index 34f4dd92f3c..66997756890 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -25,6 +25,7 @@ import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; import { searchIssues } from '../../../api/issues'; import { getRuleDetails } from '../../../api/rules'; +import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; import EmptySearch from '../../../components/common/EmptySearch'; import FiltersHeader from '../../../components/common/FiltersHeader'; @@ -104,7 +105,7 @@ interface Props { currentUser: CurrentUser; fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => void; location: Location; - router: Pick<Router, 'push' | 'replace'>; + router: Router; } export interface State { @@ -141,7 +142,7 @@ const DEFAULT_QUERY = { resolved: 'false' }; const MAX_INITAL_FETCH = 1000; const BRANCH_STATUS_REFRESH_INTERVAL = 1000; -export default class App extends React.PureComponent<Props, State> { +export class App extends React.PureComponent<Props, State> { mounted = false; constructor(props: Props) { @@ -1154,3 +1155,5 @@ const AlertContent = styled.div` display: flex; align-items: center; `; + +export default withComponentContext(App); diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx index 015fe3499e3..3b035bb3258 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx @@ -52,7 +52,7 @@ import { selectPreviousLocation } from '../../actions'; import BulkChangeModal from '../BulkChangeModal'; -import App from '../IssuesApp'; +import { App } from '../IssuesApp'; import IssuesSourceViewer from '../IssuesSourceViewer'; import IssueViewerTabs from '../IssueTabViewer'; diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx index e39ceb01d82..12d34757886 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx @@ -38,6 +38,23 @@ jest.mock('../../../../api/components', () => ({ getSources: jest.fn().mockResolvedValue([]) })); +/* + * Quick & dirty fix to make the tests pass + * this whole thing should be replaced by RTL tests! + */ +jest.mock('react-router-dom', () => { + const routerDom = jest.requireActual('react-router-dom'); + + function Link() { + return <div>Link</div>; + } + + return { + ...routerDom, + Link + }; +}); + beforeEach(() => { jest.clearAllMocks(); }); diff --git a/server/sonar-web/src/main/js/apps/issues/routes.tsx b/server/sonar-web/src/main/js/apps/issues/routes.tsx new file mode 100644 index 00000000000..ded5c5d5bd3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/routes.tsx @@ -0,0 +1,71 @@ +/* + * 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 React, { useEffect } from 'react'; +import { Route, useNavigate, useSearchParams } from 'react-router-dom'; +import { omitNil } from '../../helpers/request'; +import { IssueType } from '../../types/issues'; +import AppContainer from './components/AppContainer'; + +export const globalIssuesRoutes = () => <Route path="issues" element={<AppContainer />} />; + +export const projectIssuesRoutes = () => ( + <Route path="project/issues" element={<IssuesNavigate />} /> +); + +function IssuesNavigate() { + const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); + + useEffect(() => { + if (searchParams.has('types')) { + const types = searchParams.get('types') ?? ''; + + if (types === IssueType.SecurityHotspot) { + navigate( + { + pathname: '/security_hotspots', + search: new URLSearchParams( + omitNil({ + id: searchParams.get('id'), + branch: searchParams.get('branch'), + pullRequest: searchParams.get('pullRequest'), + assignedToMe: 'false' + }) + ).toString() + }, + { replace: true } + ); + } else { + const filteredTypes = types + .split(',') + .filter((type: string) => type !== IssueType.SecurityHotspot) + .join(','); + + if (types !== filteredTypes) { + searchParams.set('types', filteredTypes); + + setSearchParams(searchParams, { replace: true }); + } + } + } + }, [navigate, searchParams, setSearchParams]); + + return <AppContainer />; +} diff --git a/server/sonar-web/src/main/js/apps/maintenance/components/App.tsx b/server/sonar-web/src/main/js/apps/maintenance/components/App.tsx index 7cf60e0d92e..447c8510fec 100644 --- a/server/sonar-web/src/main/js/apps/maintenance/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/maintenance/components/App.tsx @@ -32,7 +32,7 @@ import { getReturnUrl } from '../../../helpers/urls'; import '../styles.css'; interface Props { - location: { query: { return_to?: string } }; + location: { query?: { return_to?: string } }; setup: boolean; } diff --git a/server/sonar-web/src/main/js/apps/maintenance/components/MaintenanceAppContainer.tsx b/server/sonar-web/src/main/js/apps/maintenance/components/MaintenanceAppContainer.tsx index 1f76587ae92..16b194ab532 100644 --- a/server/sonar-web/src/main/js/apps/maintenance/components/MaintenanceAppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/maintenance/components/MaintenanceAppContainer.tsx @@ -18,12 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { omitNil } from '../../../helpers/request'; import App from './App'; -interface Props { - location: { query: { return_to: string } }; -} +export default function MaintenanceAppContainer() { + const [searchParams] = useSearchParams(); -export default function MaintenanceAppContainer(props: Props) { - return <App setup={false} {...props} />; + return ( + <App + setup={false} + location={{ query: omitNil({ return_to: searchParams.get('return_to') }) }} + /> + ); } diff --git a/server/sonar-web/src/main/js/apps/maintenance/components/SetupAppContainer.tsx b/server/sonar-web/src/main/js/apps/maintenance/components/SetupAppContainer.tsx index 58be849409f..2619cade0eb 100644 --- a/server/sonar-web/src/main/js/apps/maintenance/components/SetupAppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/maintenance/components/SetupAppContainer.tsx @@ -18,12 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { useSearchParams } from 'react-router-dom'; + +import { omitNil } from '../../../helpers/request'; import App from './App'; -interface Props { - location: { query: { return_to: string } }; -} +export default function MaintenanceAppContainer() { + const [searchParams] = useSearchParams(); -export default function MaintenanceAppContainer(props: Props) { - return <App setup={true} {...props} />; + return ( + <App setup={true} location={{ query: omitNil({ return_to: searchParams.get('return_to') }) }} /> + ); } diff --git a/server/sonar-web/src/main/js/apps/maintenance/routes.tsx b/server/sonar-web/src/main/js/apps/maintenance/routes.tsx index 75efeb40688..c5997b49845 100644 --- a/server/sonar-web/src/main/js/apps/maintenance/routes.tsx +++ b/server/sonar-web/src/main/js/apps/maintenance/routes.tsx @@ -18,13 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { IndexRoute } from 'react-router'; -import { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import { Route } from 'react-router-dom'; +import MaintenanceAppContainer from './components/MaintenanceAppContainer'; +import SetupAppContainer from './components/SetupAppContainer'; -export const maintenanceRoutes = ( - <IndexRoute component={lazyLoadComponent(() => import('./components/MaintenanceAppContainer'))} /> +const routes = () => ( + <> + <Route path="maintenance" element={<MaintenanceAppContainer />} /> + <Route path="setup" element={<SetupAppContainer />} /> + </> ); -export const setupRoutes = ( - <IndexRoute component={lazyLoadComponent(() => import('./components/SetupAppContainer'))} /> -); +export default routes; diff --git a/server/sonar-web/src/main/js/apps/marketplace/App.tsx b/server/sonar-web/src/main/js/apps/marketplace/App.tsx index 65076bc8a40..debe1da43a7 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/App.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/App.tsx @@ -21,7 +21,7 @@ import { sortBy, uniqBy } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { getAvailablePlugins, getInstalledPlugins, @@ -51,7 +51,7 @@ interface Props { fetchPendingPlugins: () => void; pendingPlugins: PendingPluginResult; location: Location; - router: Pick<Router, 'push'>; + router: Router; standaloneMode?: boolean; updateCenterActive: boolean; } diff --git a/server/sonar-web/src/main/js/apps/marketplace/MarketplaceAppContainer.tsx b/server/sonar-web/src/main/js/apps/marketplace/MarketplaceAppContainer.tsx index 77050b3ac91..3c39c05c05c 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/MarketplaceAppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/MarketplaceAppContainer.tsx @@ -20,14 +20,14 @@ import * as React from 'react'; import AdminContext from '../../app/components/AdminContext'; import withAppStateContext from '../../app/components/app-state/withAppStateContext'; +import { Location, withRouter } from '../../components/hoc/withRouter'; import { AppState } from '../../types/appstate'; import { EditionKey } from '../../types/editions'; import { GlobalSettingKeys } from '../../types/settings'; -import { RawQuery } from '../../types/types'; import App from './App'; export interface MarketplaceAppContainerProps { - location: { pathname: string; query: RawQuery }; + location: Location; appState: AppState; } @@ -54,4 +54,4 @@ export function MarketplaceAppContainer(props: MarketplaceAppContainerProps) { ); } -export default withAppStateContext(MarketplaceAppContainer); +export default withRouter(withAppStateContext(MarketplaceAppContainer)); diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap index e90ab06818d..f839316faff 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap @@ -44,8 +44,6 @@ exports[`should render correctly: loaded 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/instance-administration/marketplace/" > @@ -144,8 +142,6 @@ exports[`should render correctly: loading 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/instance-administration/marketplace/" > diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/MarketplaceAppContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/MarketplaceAppContainer-test.tsx.snap index 58d697aede5..4f08e115a49 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/MarketplaceAppContainer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/MarketplaceAppContainer-test.tsx.snap @@ -6,7 +6,6 @@ exports[`should render correctly: default 1`] = ` fetchPendingPlugins={[Function]} location={ Object { - "action": "PUSH", "hash": "", "key": "key", "pathname": "/path", @@ -33,7 +32,6 @@ exports[`should render correctly: update center active 1`] = ` fetchPendingPlugins={[Function]} location={ Object { - "action": "PUSH", "hash": "", "key": "key", "pathname": "/path", diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/LicensePromptModal.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/LicensePromptModal.tsx index 14eca641da9..56882029a15 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/components/LicensePromptModal.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/components/LicensePromptModal.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 { ResetButtonLink } from '../../../components/controls/buttons'; import Modal from '../../../components/controls/Modal'; import { translate } from '../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/apps/marketplace/routes.ts b/server/sonar-web/src/main/js/apps/marketplace/routes.ts deleted file mode 100644 index 211f08c5a06..00000000000 --- a/server/sonar-web/src/main/js/apps/marketplace/routes.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./MarketplaceAppContainer')) } - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/routes.ts b/server/sonar-web/src/main/js/apps/marketplace/routes.tsx index 89ddf2f098f..95af0d4350c 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/routes.ts +++ b/server/sonar-web/src/main/js/apps/marketplace/routes.tsx @@ -17,10 +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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import MarketplaceAppContainer from './MarketplaceAppContainer'; -const App = lazyLoadComponent(() => import('./components/App')); - -const routes = [{ indexRoute: { component: App } }, { path: 'show/:id', component: App }]; +export const routes = () => <Route path="marketplace" element={<MarketplaceAppContainer />} />; export default routes; diff --git a/server/sonar-web/src/main/js/apps/overview/branches/FirstAnalysisNextStepsNotif.tsx b/server/sonar-web/src/main/js/apps/overview/branches/FirstAnalysisNextStepsNotif.tsx index a70c5ea4b0b..cbb88c81447 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/FirstAnalysisNextStepsNotif.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/FirstAnalysisNextStepsNotif.tsx @@ -19,10 +19,11 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import DismissableAlert from '../../../components/ui/DismissableAlert'; import { translate } from '../../../helpers/l10n'; +import { queryToSearch } from '../../../helpers/urls'; import { ProjectAlmBindingResponse } from '../../../types/alm-settings'; import { ComponentQualifier } from '../../../types/component'; import { Component } from '../../../types/types'; @@ -66,7 +67,7 @@ export function FirstAnalysisNextStepsNotif(props: FirstAnalysisNextStepsNotifPr <Link to={{ pathname: '/tutorials', - query: { id: component.key } + search: queryToSearch({ id: component.key }) }}> {translate('overview.project.next_steps.links.set_up_ci')} </Link> @@ -75,10 +76,10 @@ export function FirstAnalysisNextStepsNotif(props: FirstAnalysisNextStepsNotifPr <Link to={{ pathname: '/project/settings', - query: { + search: queryToSearch({ id: component.key, category: PULL_REQUEST_DECORATION_BINDING_CATEGORY - } + }) }}> {translate('overview.project.next_steps.links.project_settings')} </Link> diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx index c490803dc3d..0f1e50315e5 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx @@ -18,10 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { withRouter, Location } from '../../../components/hoc/withRouter'; import { rawSizes } from '../../../app/theme'; import BoxedTabs from '../../../components/controls/BoxedTabs'; import ComponentReportActions from '../../../components/controls/ComponentReportActions'; +import { Location, withRouter } from '../../../components/hoc/withRouter'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; import { findMeasure, isDiffMetric } from '../../../helpers/measures'; @@ -54,7 +54,7 @@ export enum MeasuresPanelTabs { Overall } -function MeasuresPanel(props: MeasuresPanelProps) { +export function MeasuresPanel(props: MeasuresPanelProps) { const { appLeak, branch, component, loading, measures = [], period, location } = props; const hasDiffMeasures = measures.some(m => isDiffMetric(m.metric.key)); diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx index 65305b2424d..7e58f648703 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx @@ -19,10 +19,11 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/system'; +import { queryToSearch } from '../../../helpers/urls'; import { Branch } from '../../../types/branch-like'; import { ComponentQualifier } from '../../../types/component'; import { Component, Period } from '../../../types/types'; @@ -77,7 +78,7 @@ export default function MeasuresPanelNoNewCode(props: MeasuresPanelNoNewCodeProp <Link to={{ pathname: '/project/baseline', - query: { id: component.key, ...getBranchLikeQuery(branch) } + search: queryToSearch({ id: component.key, ...getBranchLikeQuery(branch) }) }}> {translate('settings.new_code_period.category')} </Link> diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanel-test.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanel-test.tsx index 0c85d349392..2564e650feb 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanel-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanel-test.tsx @@ -30,7 +30,7 @@ import { } from '../../../../helpers/testMocks'; import { ComponentQualifier } from '../../../../types/component'; import { MetricKey } from '../../../../types/metrics'; -import MeasuresPanel, { MeasuresPanelProps, MeasuresPanelTabs } from '../MeasuresPanel'; +import { MeasuresPanel, MeasuresPanelProps, MeasuresPanelTabs } from '../MeasuresPanel'; jest.mock('react', () => { return { @@ -41,24 +41,18 @@ jest.mock('react', () => { it('should render correctly for projects', () => { const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); - wrapper - .dive() - .find(BoxedTabs) - .prop<Function>('onSelect')(MeasuresPanelTabs.Overall); - expect(wrapper).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot('default'); + wrapper.find(BoxedTabs).prop<Function>('onSelect')(MeasuresPanelTabs.Overall); + expect(wrapper).toMatchSnapshot('overall'); }); it('should render correctly for applications', () => { const wrapper = shallowRender({ component: mockComponent({ qualifier: ComponentQualifier.Application }) }); - expect(wrapper).toMatchSnapshot(); - wrapper - .dive() - .find(BoxedTabs) - .prop<Function>('onSelect')(MeasuresPanelTabs.Overall); - expect(wrapper).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot('default'); + wrapper.find(BoxedTabs).prop<Function>('onSelect')(MeasuresPanelTabs.Overall); + expect(wrapper).toMatchSnapshot('overall'); }); it('should render correctly if there is no new code measures', () => { @@ -68,10 +62,7 @@ it('should render correctly if there is no new code measures', () => { mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.bugs }) }) ] }); - wrapper - .dive() - .find(BoxedTabs) - .prop<Function>('onSelect')(MeasuresPanelTabs.New); + wrapper.find(BoxedTabs).prop<Function>('onSelect')(MeasuresPanelTabs.New); expect(wrapper).toMatchSnapshot(); }); @@ -84,10 +75,7 @@ it('should render correctly if branch is misconfigured', () => { ], period: mockPeriod({ date: undefined, mode: 'REFERENCE_BRANCH', parameter: 'own-reference' }) }); - wrapper - .dive() - .find(BoxedTabs) - .prop<Function>('onSelect')(MeasuresPanelTabs.New); + wrapper.find(BoxedTabs).prop<Function>('onSelect')(MeasuresPanelTabs.New); expect(wrapper).toMatchSnapshot('hide settings'); wrapper.setProps({ component: mockComponent({ configuration: { showSettings: true } }) }); diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanelNoNewCode-test.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanelNoNewCode-test.tsx index 74d1e4d4649..959649c2057 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanelNoNewCode-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanelNoNewCode-test.tsx @@ -61,8 +61,6 @@ it('should render the default message', () => { values={ Object { "learn_more_link": <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/user-guide/clean-as-you-code/" > learn_more diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/FirstAnalysisNextStepsNotif-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/FirstAnalysisNextStepsNotif-test.tsx.snap index cb8c39ea0b6..c16bf79e93d 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/FirstAnalysisNextStepsNotif-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/FirstAnalysisNextStepsNotif-test.tsx.snap @@ -11,14 +11,10 @@ exports[`should render correctly: show prompt to configure CI 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/tutorials", - "query": Object { - "id": "my-project", - }, + "search": "?id=my-project", } } > @@ -41,29 +37,20 @@ exports[`should render correctly: show prompt to configure PR decoration + CI, p values={ Object { "link_ci": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/tutorials", - "query": Object { - "id": "my-project", - }, + "search": "?id=my-project", } } > overview.project.next_steps.links.set_up_ci </Link>, "link_project_settings": <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", } } > @@ -86,14 +73,10 @@ exports[`should render correctly: show prompt to configure PR decoration + CI, r values={ Object { "link_ci": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/tutorials", - "query": Object { - "id": "my-project", - }, + "search": "?id=my-project", } } > @@ -116,15 +99,10 @@ exports[`should render correctly: show prompt to configure PR decoration, projec values={ Object { "link_project_settings": <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/apps/overview/branches/__tests__/__snapshots__/MeasuresPanel-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanel-test.tsx.snap index 338eab97b8c..97d53169603 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanel-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanel-test.tsx.snap @@ -1,1151 +1,5884 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly for applications 1`] = ` -<Memo(MeasuresPanel) - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - component={ - Object { - "breadcrumbs": Array [], - "key": "my-project", - "name": "MyProject", - "qualifier": "APP", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": Array [], - } - } - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/path", - "query": Object {}, - "search": "", - "state": Object {}, - } - } - measures={ - Array [ - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_coverage", - "key": "new_coverage", - "name": "New_coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "bugs", - "key": "bugs", - "name": "Bugs", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_bugs", - "key": "new_bugs", - "name": "New_bugs", - "type": "PERCENT", +exports[`should render correctly for applications: default 1`] = ` +<div + className="overview-panel" + data-test="overview__measures-panel" +> + <div + className="display-flex-space-between display-flex-start" + > + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <withCurrentUserContext(withAppStateContext(ComponentReportActions)) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> + </div> + <BoxedTabs + onSelect={[Function]} + selected={0} + tabs={ + Array [ + Object { + "key": 0, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + > + overview.new_code + </span> + </div>, }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", + Object { + "key": 1, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + style={ + Object { + "position": "absolute", + "top": 16, + } + } + > + overview.overall_code + </span> + </div>, }, - "value": "1.0", - }, - ] - } -/> + ] + } + /> + <div + className="overview-panel-content flex-1 bordered" + > + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + isNewCodeTab={true} + key="BUG" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="BUG" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + isNewCodeTab={true} + key="VULNERABILITY" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="VULNERABILITY" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + isNewCodeTab={true} + key="SECURITY_HOTSPOT" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="SECURITY_HOTSPOT" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + isNewCodeTab={true} + key="CODE_SMELL" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="CODE_SMELL" + /> + <div + className="display-flex-row overview-measures-row" + > + <div + className="overview-panel-huge-padded flex-1 bordered-right display-flex-center" + data-test="overview__measures-coverage" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={true} + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="COVERAGE" + useDiffMetric={true} + /> + </div> + <div + className="overview-panel-huge-padded flex-1 display-flex-center" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={true} + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="DUPLICATION" + useDiffMetric={true} + /> + </div> + </div> + </div> +</div> `; -exports[`should render correctly for applications 2`] = ` -<Memo(MeasuresPanel) - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - component={ - Object { - "breadcrumbs": Array [], - "key": "my-project", - "name": "MyProject", - "qualifier": "APP", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": Array [], - } - } - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/path", - "query": Object {}, - "search": "", - "state": Object {}, - } - } - measures={ - Array [ - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_coverage", - "key": "new_coverage", - "name": "New_coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "bugs", - "key": "bugs", - "name": "Bugs", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_bugs", - "key": "new_bugs", - "name": "New_bugs", - "type": "PERCENT", +exports[`should render correctly for applications: overall 1`] = ` +<div + className="overview-panel" + data-test="overview__measures-panel" +> + <div + className="display-flex-space-between display-flex-start" + > + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <withCurrentUserContext(withAppStateContext(ComponentReportActions)) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> + </div> + <BoxedTabs + onSelect={[Function]} + selected={1} + tabs={ + Array [ + Object { + "key": 0, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + > + overview.new_code + </span> + </div>, }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", + Object { + "key": 1, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + style={ + Object { + "position": "absolute", + "top": 16, + } + } + > + overview.overall_code + </span> + </div>, }, - "value": "1.0", - }, - ] - } -/> + ] + } + /> + <div + className="overview-panel-content flex-1 bordered" + > + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + isNewCodeTab={false} + key="BUG" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="BUG" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + isNewCodeTab={false} + key="VULNERABILITY" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="VULNERABILITY" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + isNewCodeTab={false} + key="SECURITY_HOTSPOT" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="SECURITY_HOTSPOT" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + isNewCodeTab={false} + key="CODE_SMELL" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="CODE_SMELL" + /> + <div + className="display-flex-row overview-measures-row" + > + <div + className="overview-panel-huge-padded flex-1 bordered-right display-flex-center" + data-test="overview__measures-coverage" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={false} + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="COVERAGE" + useDiffMetric={false} + /> + <div + className="huge-spacer-left" + > + <DrilldownMeasureValue + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + metric="tests" + /> + </div> + </div> + <div + className="overview-panel-huge-padded flex-1 display-flex-center" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={false} + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="DUPLICATION" + useDiffMetric={false} + /> + <div + className="huge-spacer-left" + > + <DrilldownMeasureValue + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + metric="duplicated_blocks" + /> + </div> + </div> + </div> + </div> +</div> `; -exports[`should render correctly for projects 1`] = ` -<Memo(MeasuresPanel) - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - 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 [], - } - } - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/path", - "query": Object {}, - "search": "", - "state": Object {}, - } - } - measures={ - Array [ - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_coverage", - "key": "new_coverage", - "name": "New_coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "bugs", - "key": "bugs", - "name": "Bugs", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_bugs", - "key": "new_bugs", - "name": "New_bugs", - "type": "PERCENT", +exports[`should render correctly for projects: default 1`] = ` +<div + className="overview-panel" + data-test="overview__measures-panel" +> + <div + className="display-flex-space-between display-flex-start" + > + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <withCurrentUserContext(withAppStateContext(ComponentReportActions)) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + /> + </div> + <BoxedTabs + onSelect={[Function]} + selected={0} + tabs={ + Array [ + Object { + "key": 0, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + > + overview.new_code + </span> + </div>, }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", + Object { + "key": 1, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + style={ + Object { + "position": "absolute", + "top": 16, + } + } + > + overview.overall_code + </span> + </div>, }, - "value": "1.0", - }, - ] - } -/> + ] + } + /> + <div + className="overview-panel-content flex-1 bordered" + > + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={true} + key="BUG" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="BUG" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={true} + key="VULNERABILITY" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="VULNERABILITY" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={true} + key="SECURITY_HOTSPOT" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="SECURITY_HOTSPOT" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={true} + key="CODE_SMELL" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="CODE_SMELL" + /> + <div + className="display-flex-row overview-measures-row" + > + <div + className="overview-panel-huge-padded flex-1 bordered-right display-flex-center" + data-test="overview__measures-coverage" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={true} + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="COVERAGE" + useDiffMetric={true} + /> + </div> + <div + className="overview-panel-huge-padded flex-1 display-flex-center" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={true} + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="DUPLICATION" + useDiffMetric={true} + /> + </div> + </div> + </div> +</div> `; -exports[`should render correctly for projects 2`] = ` -<Memo(MeasuresPanel) - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - 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 [], - } - } - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/path", - "query": Object {}, - "search": "", - "state": Object {}, - } - } - measures={ - Array [ - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_coverage", - "key": "new_coverage", - "name": "New_coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "bugs", - "key": "bugs", - "name": "Bugs", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_bugs", - "key": "new_bugs", - "name": "New_bugs", - "type": "PERCENT", +exports[`should render correctly for projects: overall 1`] = ` +<div + className="overview-panel" + data-test="overview__measures-panel" +> + <div + className="display-flex-space-between display-flex-start" + > + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <withCurrentUserContext(withAppStateContext(ComponentReportActions)) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + /> + </div> + <BoxedTabs + onSelect={[Function]} + selected={1} + tabs={ + Array [ + Object { + "key": 0, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + > + overview.new_code + </span> + </div>, }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", + Object { + "key": 1, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + style={ + Object { + "position": "absolute", + "top": 16, + } + } + > + overview.overall_code + </span> + </div>, }, - "value": "1.0", - }, - ] - } -/> + ] + } + /> + <div + className="overview-panel-content flex-1 bordered" + > + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={false} + key="BUG" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="BUG" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={false} + key="VULNERABILITY" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="VULNERABILITY" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={false} + key="SECURITY_HOTSPOT" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="SECURITY_HOTSPOT" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={false} + key="CODE_SMELL" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="CODE_SMELL" + /> + <div + className="display-flex-row overview-measures-row" + > + <div + className="overview-panel-huge-padded flex-1 bordered-right display-flex-center" + data-test="overview__measures-coverage" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={false} + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="COVERAGE" + useDiffMetric={false} + /> + <div + className="huge-spacer-left" + > + <DrilldownMeasureValue + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + metric="tests" + /> + </div> + </div> + <div + className="overview-panel-huge-padded flex-1 display-flex-center" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={false} + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="DUPLICATION" + useDiffMetric={false} + /> + <div + className="huge-spacer-left" + > + <DrilldownMeasureValue + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + metric="duplicated_blocks" + /> + </div> + </div> + </div> + </div> +</div> `; exports[`should render correctly if branch is misconfigured: hide settings 1`] = ` -<Memo(MeasuresPanel) - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "own-reference", - } - } - 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 [], - } - } - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/path", - "query": Object {}, - "search": "", - "state": Object {}, - } - } - measures={ - Array [ - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "bugs", - "key": "bugs", - "name": "Bugs", - "type": "PERCENT", +<div + className="overview-panel" + data-test="overview__measures-panel" +> + <div + className="display-flex-space-between display-flex-start" + > + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <withCurrentUserContext(withAppStateContext(ComponentReportActions)) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "own-reference", + } + } + 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 [], + } + } + /> + </div> + <BoxedTabs + onSelect={[Function]} + selected={0} + tabs={ + Array [ + Object { + "key": 0, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + > + overview.new_code + </span> + <LeakPeriodInfo + leakPeriod={ + Object { + "date": undefined, + "index": 0, + "mode": "REFERENCE_BRANCH", + "parameter": "own-reference", + } + } + /> + </div>, }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", + Object { + "key": 1, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + style={ + Object { + "position": "absolute", + "top": 16, + } + } + > + overview.overall_code + </span> + </div>, }, - "value": "1.0", - }, - ] - } - period={ - Object { - "date": undefined, - "index": 0, - "mode": "REFERENCE_BRANCH", - "parameter": "own-reference", + ] } - } -/> + /> + <div + className="overview-panel-content flex-1 bordered" + > + <MeasuresPanelNoNewCode + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "own-reference", + } + } + 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 [], + } + } + period={ + Object { + "date": undefined, + "index": 0, + "mode": "REFERENCE_BRANCH", + "parameter": "own-reference", + } + } + /> + </div> +</div> `; exports[`should render correctly if branch is misconfigured: show settings 1`] = ` -<Memo(MeasuresPanel) - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "own-reference", - } - } - 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 [], - } - } - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/path", - "query": Object {}, - "search": "", - "state": Object {}, - } - } - measures={ - Array [ - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "bugs", - "key": "bugs", - "name": "Bugs", - "type": "PERCENT", +<div + className="overview-panel" + data-test="overview__measures-panel" +> + <div + className="display-flex-space-between display-flex-start" + > + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <withCurrentUserContext(withAppStateContext(ComponentReportActions)) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "own-reference", + } + } + 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 [], + } + } + /> + </div> + <BoxedTabs + onSelect={[Function]} + selected={0} + tabs={ + Array [ + Object { + "key": 0, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + > + overview.new_code + </span> + <LeakPeriodInfo + leakPeriod={ + Object { + "date": undefined, + "index": 0, + "mode": "REFERENCE_BRANCH", + "parameter": "own-reference", + } + } + /> + </div>, }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", + Object { + "key": 1, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + style={ + Object { + "position": "absolute", + "top": 16, + } + } + > + overview.overall_code + </span> + </div>, }, - "value": "1.0", - }, - ] - } - period={ - Object { - "date": undefined, - "index": 0, - "mode": "REFERENCE_BRANCH", - "parameter": "own-reference", + ] } - } -/> + /> + <div + className="overview-panel-content flex-1 bordered" + > + <MeasuresPanelNoNewCode + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "own-reference", + } + } + 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 [], + } + } + period={ + Object { + "date": undefined, + "index": 0, + "mode": "REFERENCE_BRANCH", + "parameter": "own-reference", + } + } + /> + </div> +</div> `; exports[`should render correctly if the data is still loading 1`] = ` -<Memo(MeasuresPanel) - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - 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 [], - } - } - loading={true} - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/path", - "query": Object {}, - "search": "", - "state": Object {}, - } - } - measures={ - Array [ - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_coverage", - "key": "new_coverage", - "name": "New_coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "bugs", - "key": "bugs", - "name": "Bugs", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_bugs", - "key": "new_bugs", - "name": "New_bugs", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - ] - } -/> +<div + className="overview-panel" + data-test="overview__measures-panel" +> + <div + className="display-flex-space-between display-flex-start" + > + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <withCurrentUserContext(withAppStateContext(ComponentReportActions)) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + /> + </div> + <div + className="overview-panel-content overview-panel-big-padded" + > + <DeferredSpinner + loading={true} + /> + </div> +</div> `; exports[`should render correctly if there is no coverage 1`] = ` -<Memo(MeasuresPanel) - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - 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 [], - } - } - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/path", - "query": Object {}, - "search": "", - "state": Object {}, - } - } - measures={ - Array [ - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "bugs", - "key": "bugs", - "name": "Bugs", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_bugs", - "key": "new_bugs", - "name": "New_bugs", - "type": "PERCENT", +<div + className="overview-panel" + data-test="overview__measures-panel" +> + <div + className="display-flex-space-between display-flex-start" + > + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <withCurrentUserContext(withAppStateContext(ComponentReportActions)) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + /> + </div> + <BoxedTabs + onSelect={[Function]} + selected={0} + tabs={ + Array [ + Object { + "key": 0, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + > + overview.new_code + </span> + </div>, }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", + Object { + "key": 1, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + style={ + Object { + "position": "absolute", + "top": 16, + } + } + > + overview.overall_code + </span> + </div>, }, - "value": "1.0", - }, - ] - } -/> + ] + } + /> + <div + className="overview-panel-content flex-1 bordered" + > + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={true} + key="BUG" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="BUG" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={true} + key="VULNERABILITY" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="VULNERABILITY" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={true} + key="SECURITY_HOTSPOT" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="SECURITY_HOTSPOT" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={true} + key="CODE_SMELL" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="CODE_SMELL" + /> + <div + className="display-flex-row overview-measures-row" + > + <div + className="overview-panel-huge-padded flex-1 display-flex-center" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={true} + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="DUPLICATION" + useDiffMetric={true} + /> + </div> + </div> + </div> +</div> `; exports[`should render correctly if there is no new code measures 1`] = ` -<Memo(MeasuresPanel) - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - 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 [], - } - } - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/path", - "query": Object {}, - "search": "", - "state": Object {}, - } - } - measures={ - Array [ - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "bugs", - "key": "bugs", - "name": "Bugs", - "type": "PERCENT", +<div + className="overview-panel" + data-test="overview__measures-panel" +> + <div + className="display-flex-space-between display-flex-start" + > + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <withCurrentUserContext(withAppStateContext(ComponentReportActions)) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + /> + </div> + <BoxedTabs + onSelect={[Function]} + selected={0} + tabs={ + Array [ + Object { + "key": 0, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + > + overview.new_code + </span> + </div>, }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", + Object { + "key": 1, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + style={ + Object { + "position": "absolute", + "top": 16, + } + } + > + overview.overall_code + </span> + </div>, }, - "value": "1.0", - }, - ] - } -/> + ] + } + /> + <div + className="overview-panel-content flex-1 bordered" + > + <MeasuresPanelNoNewCode + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + /> + </div> +</div> `; exports[`should render correctly when code scope is new code 1`] = ` -<Memo(MeasuresPanel) - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - 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 [], - } - } - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/dashboard", - "query": Object { - "code_scope": "new", - }, - "search": "", - "state": Object {}, - } - } - measures={ - Array [ - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_coverage", - "key": "new_coverage", - "name": "New_coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "bugs", - "key": "bugs", - "name": "Bugs", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_bugs", - "key": "new_bugs", - "name": "New_bugs", - "type": "PERCENT", +<div + className="overview-panel" + data-test="overview__measures-panel" +> + <div + className="display-flex-space-between display-flex-start" + > + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <withCurrentUserContext(withAppStateContext(ComponentReportActions)) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + /> + </div> + <BoxedTabs + onSelect={[Function]} + selected={0} + tabs={ + Array [ + Object { + "key": 0, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + > + overview.new_code + </span> + </div>, }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", + Object { + "key": 1, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + style={ + Object { + "position": "absolute", + "top": 16, + } + } + > + overview.overall_code + </span> + </div>, }, - "value": "1.0", - }, - ] - } -/> + ] + } + /> + <div + className="overview-panel-content flex-1 bordered" + > + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={true} + key="BUG" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="BUG" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={true} + key="VULNERABILITY" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="VULNERABILITY" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={true} + key="SECURITY_HOTSPOT" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="SECURITY_HOTSPOT" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={true} + key="CODE_SMELL" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="CODE_SMELL" + /> + <div + className="display-flex-row overview-measures-row" + > + <div + className="overview-panel-huge-padded flex-1 bordered-right display-flex-center" + data-test="overview__measures-coverage" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={true} + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="COVERAGE" + useDiffMetric={true} + /> + </div> + <div + className="overview-panel-huge-padded flex-1 display-flex-center" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={true} + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="DUPLICATION" + useDiffMetric={true} + /> + </div> + </div> + </div> +</div> `; exports[`should render correctly when code scope is overall code 1`] = ` -<Memo(MeasuresPanel) - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - 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 [], - } - } - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/dashboard", - "query": Object { - "code_scope": "overall", - }, - "search": "", - "state": Object {}, - } - } - measures={ - Array [ - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_coverage", - "key": "new_coverage", - "name": "New_coverage", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "bugs", - "key": "bugs", - "name": "Bugs", - "type": "PERCENT", - }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", - }, - "value": "1.0", - }, - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "new_bugs", - "key": "new_bugs", - "name": "New_bugs", - "type": "PERCENT", +<div + className="overview-panel" + data-test="overview__measures-panel" +> + <div + className="display-flex-space-between display-flex-start" + > + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <withCurrentUserContext(withAppStateContext(ComponentReportActions)) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + /> + </div> + <BoxedTabs + onSelect={[Function]} + selected={1} + tabs={ + Array [ + Object { + "key": 0, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + > + overview.new_code + </span> + </div>, }, - "period": Object { - "bestValue": true, - "index": 1, - "value": "1.0", + Object { + "key": 1, + "label": <div + className="text-left overview-measures-tab" + > + <span + className="text-bold" + style={ + Object { + "position": "absolute", + "top": 16, + } + } + > + overview.overall_code + </span> + </div>, }, - "value": "1.0", - }, - ] - } -/> + ] + } + /> + <div + className="overview-panel-content flex-1 bordered" + > + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={false} + key="BUG" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="BUG" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={false} + key="VULNERABILITY" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="VULNERABILITY" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={false} + key="SECURITY_HOTSPOT" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="SECURITY_HOTSPOT" + /> + <MeasuresPanelIssueMeasureRow + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + isNewCodeTab={false} + key="CODE_SMELL" + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="CODE_SMELL" + /> + <div + className="display-flex-row overview-measures-row" + > + <div + className="overview-panel-huge-padded flex-1 bordered-right display-flex-center" + data-test="overview__measures-coverage" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={false} + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="COVERAGE" + useDiffMetric={false} + /> + <div + className="huge-spacer-left" + > + <DrilldownMeasureValue + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + metric="tests" + /> + </div> + </div> + <div + className="overview-panel-huge-padded flex-1 display-flex-center" + > + <MeasurementLabel + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + centered={false} + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + type="DUPLICATION" + useDiffMetric={false} + /> + <div + className="huge-spacer-left" + > + <DrilldownMeasureValue + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + 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 [], + } + } + measures={ + Array [ + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "New_coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "bugs", + "key": "bugs", + "name": "Bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + Object { + "bestValue": true, + "leak": "1", + "metric": Object { + "id": "new_bugs", + "key": "new_bugs", + "name": "New_bugs", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, + "value": "1.0", + }, + ] + } + metric="duplicated_blocks" + /> + </div> + </div> + </div> + </div> +</div> `; diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanelNoNewCode-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanelNoNewCode-test.tsx.snap index 098db30ab90..17a5cf7abcc 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanelNoNewCode-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanelNoNewCode-test.tsx.snap @@ -35,8 +35,6 @@ exports[`should render "bad code setting" explanation: no link 1`] = ` values={ Object { "learn_more_link": <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/user-guide/clean-as-you-code/" > learn_more @@ -84,8 +82,6 @@ exports[`should render "bad code setting" explanation: with link 1`] = ` values={ Object { "learn_more_link": <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/user-guide/clean-as-you-code/" > learn_more @@ -165,8 +161,6 @@ exports[`should render the default message 6`] = ` values={ Object { "learn_more_link": <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/user-guide/clean-as-you-code/" > learn_more diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.tsx b/server/sonar-web/src/main/js/apps/overview/components/App.tsx index 91c19c67eb3..e31cee5531d 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/App.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; +import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; import { isPullRequest } from '../../../helpers/branch-like'; @@ -92,4 +93,4 @@ export class App extends React.PureComponent<Props> { } } -export default withAppStateContext(App); +export default withComponentContext(withAppStateContext(App)); diff --git a/server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx b/server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx index 4c16e59fa66..f1cad7fab13 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/IssueLabel.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 HelpTooltip from '../../../components/controls/HelpTooltip'; import { getLeakValue } from '../../../components/measure/utils'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx index 7a956c0249e..084d159d323 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx @@ -19,18 +19,14 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link, Path } from 'react-router-dom'; import IssueTypeIcon from '../../../components/icons/IssueTypeIcon'; import Measure from '../../../components/measure/Measure'; import DrilldownLink from '../../../components/shared/DrilldownLink'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric, localizeMetric } from '../../../helpers/measures'; -import { - getComponentIssuesUrl, - getComponentSecurityHotspotsUrl, - Location -} from '../../../helpers/urls'; +import { getComponentIssuesUrl, getComponentSecurityHotspotsUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; import { IssueType } from '../../../types/issues'; import { MetricKey } from '../../../types/metrics'; @@ -97,7 +93,7 @@ export default class QualityGateCondition extends React.PureComponent<Props> { const metricKey = condition.measure.metric.key; - const METRICS_TO_URL_MAPPING: Dict<() => Location> = { + const METRICS_TO_URL_MAPPING: Dict<() => Path> = { [MetricKey.reliability_rating]: () => this.getUrlForBugsOrVulnerabilities(IssueType.Bug, false), [MetricKey.new_reliability_rating]: () => diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap index a172ab650a8..ffd998312f2 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap @@ -4,18 +4,11 @@ exports[`should render correctly for bugs 1`] = ` <Fragment> <Link className="overview-measures-value text-light" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "my-project", - "pullRequest": "1001", - "resolved": "false", - "sinceLeakPeriod": "false", - "types": "BUG", - }, + "search": "?pullRequest=1001&resolved=false&types=BUG&sinceLeakPeriod=false&id=my-project", } } > @@ -32,18 +25,11 @@ exports[`should render correctly for bugs 2`] = ` <Fragment> <Link className="overview-measures-value text-light" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "my-project", - "pullRequest": "1001", - "resolved": "false", - "sinceLeakPeriod": "true", - "types": "BUG", - }, + "search": "?pullRequest=1001&resolved=false&types=BUG&sinceLeakPeriod=true&id=my-project", } } > @@ -60,18 +46,11 @@ exports[`should render correctly for code smells 1`] = ` <Fragment> <Link className="overview-measures-value text-light" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "my-project", - "pullRequest": "1001", - "resolved": "false", - "sinceLeakPeriod": "false", - "types": "CODE_SMELL", - }, + "search": "?pullRequest=1001&resolved=false&types=CODE_SMELL&sinceLeakPeriod=false&id=my-project", } } > @@ -88,18 +67,11 @@ exports[`should render correctly for code smells 2`] = ` <Fragment> <Link className="overview-measures-value text-light" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "my-project", - "pullRequest": "1001", - "resolved": "false", - "sinceLeakPeriod": "true", - "types": "CODE_SMELL", - }, + "search": "?pullRequest=1001&resolved=false&types=CODE_SMELL&sinceLeakPeriod=true&id=my-project", } } > @@ -116,20 +88,11 @@ exports[`should render correctly for hotspots 1`] = ` <Fragment> <Link className="overview-measures-value text-light" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/security_hotspots", - "query": Object { - "assignedToMe": undefined, - "branch": undefined, - "file": undefined, - "hotspots": undefined, - "id": "my-project", - "pullRequest": "1001", - "sinceLeakPeriod": "false", - }, + "search": "?id=my-project&pullRequest=1001&sinceLeakPeriod=false", } } > @@ -150,20 +113,11 @@ exports[`should render correctly for hotspots 2`] = ` <Fragment> <Link className="overview-measures-value text-light" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/security_hotspots", - "query": Object { - "assignedToMe": undefined, - "branch": undefined, - "file": undefined, - "hotspots": undefined, - "id": "my-project", - "pullRequest": "1001", - "sinceLeakPeriod": "true", - }, + "search": "?id=my-project&pullRequest=1001&sinceLeakPeriod=true", } } > @@ -184,18 +138,11 @@ exports[`should render correctly for vulnerabilities 1`] = ` <Fragment> <Link className="overview-measures-value text-light" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "my-project", - "pullRequest": "1001", - "resolved": "false", - "sinceLeakPeriod": "false", - "types": "VULNERABILITY", - }, + "search": "?pullRequest=1001&resolved=false&types=VULNERABILITY&sinceLeakPeriod=false&id=my-project", } } > @@ -212,18 +159,11 @@ exports[`should render correctly for vulnerabilities 2`] = ` <Fragment> <Link className="overview-measures-value text-light" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "my-project", - "pullRequest": "1001", - "resolved": "false", - "sinceLeakPeriod": "true", - "types": "VULNERABILITY", - }, + "search": "?pullRequest=1001&resolved=false&types=VULNERABILITY&sinceLeakPeriod=true&id=my-project", } } > diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/QualityGateCondition-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/QualityGateCondition-test.tsx.snap index eef919feded..b4eb8987e86 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/QualityGateCondition-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/QualityGateCondition-test.tsx.snap @@ -45,17 +45,11 @@ exports[`should render correclty 1`] = ` exports[`should render correclty 2`] = ` <Link className="overview-quality-gate-condition overview-quality-gate-condition-error" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "abcd-key", - "resolved": "false", - "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", - "types": "BUG", - }, + "search": "?resolved=false&types=BUG&severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR&id=abcd-key", } } > @@ -97,17 +91,11 @@ exports[`should render correclty 2`] = ` exports[`should render correclty 3`] = ` <Link className="overview-quality-gate-condition overview-quality-gate-condition-error" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "abcd-key", - "resolved": "false", - "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", - "types": "VULNERABILITY", - }, + "search": "?resolved=false&types=VULNERABILITY&severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR&id=abcd-key", } } > @@ -149,16 +137,11 @@ exports[`should render correclty 3`] = ` exports[`should render correclty 4`] = ` <Link className="overview-quality-gate-condition overview-quality-gate-condition-error" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "abcd-key", - "resolved": "false", - "types": "CODE_SMELL", - }, + "search": "?resolved=false&types=CODE_SMELL&id=abcd-key", } } > @@ -200,18 +183,11 @@ exports[`should render correclty 4`] = ` exports[`should render correclty 5`] = ` <Link className="overview-quality-gate-condition overview-quality-gate-condition-error" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "abcd-key", - "resolved": "false", - "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", - "sinceLeakPeriod": "true", - "types": "BUG", - }, + "search": "?resolved=false&types=BUG&severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR&sinceLeakPeriod=true&id=abcd-key", } } > @@ -253,18 +229,11 @@ exports[`should render correclty 5`] = ` exports[`should render correclty 6`] = ` <Link className="overview-quality-gate-condition overview-quality-gate-condition-error" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "abcd-key", - "resolved": "false", - "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", - "sinceLeakPeriod": "true", - "types": "VULNERABILITY", - }, + "search": "?resolved=false&types=VULNERABILITY&severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR&sinceLeakPeriod=true&id=abcd-key", } } > @@ -306,17 +275,11 @@ exports[`should render correclty 6`] = ` exports[`should render correclty 7`] = ` <Link className="overview-quality-gate-condition overview-quality-gate-condition-error" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "abcd-key", - "resolved": "false", - "sinceLeakPeriod": "true", - "types": "CODE_SMELL", - }, + "search": "?resolved=false&types=CODE_SMELL&sinceLeakPeriod=true&id=abcd-key", } } > @@ -358,20 +321,11 @@ exports[`should render correclty 7`] = ` exports[`should render correclty 8`] = ` <Link className="overview-quality-gate-condition overview-quality-gate-condition-error" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/security_hotspots", - "query": Object { - "assignedToMe": undefined, - "branch": undefined, - "file": undefined, - "hotspots": undefined, - "id": "abcd-key", - "pullRequest": undefined, - "sinceLeakPeriod": undefined, - }, + "search": "?id=abcd-key", } } > @@ -413,20 +367,11 @@ exports[`should render correclty 8`] = ` exports[`should render correclty 9`] = ` <Link className="overview-quality-gate-condition overview-quality-gate-condition-error" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/security_hotspots", - "query": Object { - "assignedToMe": undefined, - "branch": undefined, - "file": undefined, - "hotspots": undefined, - "id": "abcd-key", - "pullRequest": undefined, - "sinceLeakPeriod": "true", - }, + "search": "?id=abcd-key&sinceLeakPeriod=true", } } > @@ -468,18 +413,11 @@ exports[`should render correclty 9`] = ` exports[`should work with branch 1`] = ` <Link className="overview-quality-gate-condition overview-quality-gate-condition-error" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "branch": "branch-6.7", - "id": "abcd-key", - "resolved": "false", - "sinceLeakPeriod": "true", - "types": "CODE_SMELL", - }, + "search": "?resolved=false&branch=branch-6.7&types=CODE_SMELL&sinceLeakPeriod=true&id=abcd-key", } } > diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx index f82942cc59f..53460033875 100644 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx @@ -20,7 +20,7 @@ import classNames from 'classnames'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { colors } from '../../../app/theme'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import HelpIcon from '../../../components/icons/HelpIcon'; diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/LargeQualityGateBadge-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/LargeQualityGateBadge-test.tsx.snap index 517bbf540be..527e390ba6b 100644 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/LargeQualityGateBadge-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/LargeQualityGateBadge-test.tsx.snap @@ -19,8 +19,6 @@ exports[`should render correctly for SQ 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/quality_gates/show/30", @@ -67,8 +65,6 @@ exports[`should render correctly for SQ 2`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/quality_gates/show/30", diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/routes.ts b/server/sonar-web/src/main/js/apps/overview/routes.tsx index 9d8f2bd42e4..0b6e26d0770 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/routes.ts +++ b/server/sonar-web/src/main/js/apps/overview/routes.tsx @@ -17,12 +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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import App from './components/App'; -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/App')) } - } -]; +const routes = () => <Route path="dashboard" element={<App />} />; export default routes; diff --git a/server/sonar-web/src/main/js/apps/overview/utils.ts b/server/sonar-web/src/main/js/apps/overview/utils.ts index d3eff6be5fe..d49a26488bc 100644 --- a/server/sonar-web/src/main/js/apps/overview/utils.ts +++ b/server/sonar-web/src/main/js/apps/overview/utils.ts @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Query } from 'history'; import { memoize } from 'lodash'; import CoverageRating from '../../components/ui/CoverageRating'; import DuplicationsRating from '../../components/ui/DuplicationsRating'; @@ -183,10 +182,8 @@ export function getMeasurementLabelKeys(type: MeasurementType, useDiffMetric: bo }; } -export const parseQuery = memoize( - (urlQuery: RawQuery): Query => { - return { - codeScope: parseAsString(urlQuery['code_scope']) - }; - } -); +export const parseQuery = memoize((urlQuery: RawQuery): { codeScope: string } => { + return { + codeScope: parseAsString(urlQuery['code_scope']) + }; +}); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx index 40806703a81..da13a3a8d61 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx @@ -28,6 +28,7 @@ import ActionsDropdown, { ActionsDropdownItem } from '../../../components/contro import { Router, withRouter } from '../../../components/hoc/withRouter'; import QualifierIcon from '../../../components/icons/QualifierIcon'; import { translate } from '../../../helpers/l10n'; +import { queryToSearch } from '../../../helpers/urls'; import { PermissionTemplate } from '../../../types/types'; import { PERMISSION_TEMPLATES_PATH } from '../utils'; import DeleteForm from './DeleteForm'; @@ -37,7 +38,7 @@ interface Props { fromDetails?: boolean; permissionTemplate: PermissionTemplate; refresh: () => void; - router: Pick<Router, 'replace'>; + router: Router; topQualifiers: string[]; } @@ -160,7 +161,8 @@ export class ActionsCell extends React.PureComponent<Props, State> { {this.renderSetDefaultsControl()} {!this.props.fromDetails && ( - <ActionsDropdownItem to={{ pathname: PERMISSION_TEMPLATES_PATH, query: { id: t.id } }}> + <ActionsDropdownItem + to={{ pathname: PERMISSION_TEMPLATES_PATH, search: queryToSearch({ id: t.id }) }}> {translate('edit_permissions')} </ActionsDropdownItem> )} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/App.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/App.tsx index 7e16f3c2522..649ee420207 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/App.tsx @@ -17,12 +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 { Location } from 'history'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { getPermissionTemplates } from '../../../api/permissions'; import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; +import { Location, withRouter } from '../../../components/hoc/withRouter'; import { translate } from '../../../helpers/l10n'; import { AppState } from '../../../types/appstate'; import { Permission, PermissionTemplate } from '../../../types/types'; @@ -121,4 +121,4 @@ export class App extends React.PureComponent<Props, State> { } } -export default withAppStateContext(App); +export default withRouter(withAppStateContext(App)); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx index 7f58cc9ad0c..7494e4260da 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx @@ -28,7 +28,7 @@ import Form from './Form'; interface Props { ready?: boolean; refresh: () => Promise<void>; - router: Pick<Router, 'push'>; + router: Router; } interface State { diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/NameCell.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/NameCell.tsx index 2e471157090..02ead1cf2a2 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/NameCell.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/NameCell.tsx @@ -18,7 +18,8 @@ * 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 { queryToSearch } from '../../../helpers/urls'; import { PermissionTemplate } from '../../../types/types'; import { PERMISSION_TEMPLATES_PATH } from '../utils'; import Defaults from './Defaults'; @@ -32,7 +33,7 @@ export default function NameCell({ template }: Props) { return ( <td className="little-padded-left little-padded-right"> - <Link to={{ pathname, query: { id: template.id } }}> + <Link to={{ pathname, search: queryToSearch({ id: template.id }) }}> <strong className="js-name">{template.name}</strong> </Link> diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/TemplateHeader.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/TemplateHeader.tsx index 55e25524583..4a4addbb9ae 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/TemplateHeader.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/TemplateHeader.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 { PermissionTemplate } from '../../../types/types'; import { PERMISSION_TEMPLATES_PATH } from '../utils'; diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx index 030d3e594a6..f079f5b0477 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockRouter } from '../../../../helpers/testMocks'; import { ActionsCell } from '../ActionsCell'; const SAMPLE = { @@ -34,7 +35,7 @@ function renderActionsCell(props?: Partial<ActionsCell['props']>) { <ActionsCell permissionTemplate={SAMPLE} refresh={() => true} - router={{ replace: jest.fn() }} + router={mockRouter()} topQualifiers={['TRK', 'VW']} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/NameCell-test.tsx.snap b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/NameCell-test.tsx.snap index 66c34bcc15b..b1b66feeff5 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/NameCell-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/NameCell-test.tsx.snap @@ -5,14 +5,10 @@ exports[`render correctly 1`] = ` className="little-padded-left little-padded-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/permission_templates", - "query": Object { - "id": "1", - }, + "search": "?id=1", } } > diff --git a/server/sonar-web/src/main/js/apps/permission-templates/routes.ts b/server/sonar-web/src/main/js/apps/permission-templates/routes.tsx index 9d8f2bd42e4..5b57573de98 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/routes.ts +++ b/server/sonar-web/src/main/js/apps/permission-templates/routes.tsx @@ -17,12 +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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import App from './components/App'; -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/App')) } - } -]; +const routes = () => <Route path="permission_templates" element={<App />} />; export default routes; diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx index aab9f301032..d3b830873e3 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx @@ -21,6 +21,7 @@ import { without } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import * as api from '../../../../api/permissions'; +import withComponentContext from '../../../../app/components/componentContext/withComponentContext'; import VisibilitySelector from '../../../../components/common/VisibilitySelector'; import { translate } from '../../../../helpers/l10n'; import { Component, Paging, PermissionGroup, PermissionUser } from '../../../../types/types'; @@ -46,7 +47,7 @@ interface State { usersPaging?: Paging; } -export default class App extends React.PureComponent<Props, State> { +export class App extends React.PureComponent<Props, State> { mounted = false; constructor(props: Props) { @@ -389,3 +390,5 @@ export default class App extends React.PureComponent<Props, State> { ); } } + +export default withComponentContext(App); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx index d1ab71cc4f9..de0d08d4d63 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx @@ -27,7 +27,7 @@ import { } from '../../../../../api/permissions'; import { mockComponent } from '../../../../../helpers/mocks/component'; import { waitAndUpdate } from '../../../../../helpers/testUtils'; -import App from '../App'; +import { App } from '../App'; jest.mock('../../../../../api/permissions', () => ({ getPermissionsGroupsForComponent: jest.fn().mockResolvedValue({ diff --git a/server/sonar-web/src/main/js/apps/permissions/routes.ts b/server/sonar-web/src/main/js/apps/permissions/routes.ts deleted file mode 100644 index 98674c85e9f..00000000000 --- a/server/sonar-web/src/main/js/apps/permissions/routes.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -export const globalPermissionsRoutes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./global/components/App')) } - } -]; - -export const projectPermissionsRoutes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./project/components/App')) } - } -]; diff --git a/server/sonar-web/src/main/js/apps/permissions/routes.tsx b/server/sonar-web/src/main/js/apps/permissions/routes.tsx new file mode 100644 index 00000000000..c075a9dc57a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/routes.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 React from 'react'; +import { Route } from 'react-router-dom'; +import GlobalPermissionsApp from './global/components/App'; +import ProjectPermissionsApp from './project/components/App'; + +export const globalPermissionsRoutes = () => ( + <Route path="permissions" element={<GlobalPermissionsApp />} /> +); + +export const projectPermissionsRoutes = () => ( + <Route path="project_roles" element={<ProjectPermissionsApp />} /> +); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx index c238dcb58d7..7d3b7dc09db 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.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 { ButtonLink } from '../../../components/controls/buttons'; import BranchIcon from '../../../components/icons/BranchIcon'; import DropdownIcon from '../../../components/icons/DropdownIcon'; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx index 208f378d575..8b636c67b47 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { ComponentContext } from '../../../app/components/ComponentContext'; +import { ComponentContext } from '../../../app/components/componentContext/ComponentContext'; import Tooltip from '../../../components/controls/Tooltip'; import { translate } from '../../../helpers/l10n'; import { AnalysisEvent } from '../../../types/types'; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx index a41482e9796..399286545bc 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx @@ -17,9 +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 { Location } from 'history'; import * as React from 'react'; -import { InjectedRouter } from 'react-router'; +import { useSearchParams } from 'react-router-dom'; import { getAllMetrics } from '../../../api/metrics'; import { changeEvent, @@ -30,12 +29,14 @@ import { ProjectActivityStatuses } from '../../../api/projectActivity'; import { getAllTimeMachineData } from '../../../api/time-machine'; +import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import { DEFAULT_GRAPH, getActivityGraph, getHistoryMetrics, isCustomGraph } from '../../../components/activity-graph/utils'; +import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { parseDate } from '../../../helpers/dates'; import { serializeStringArray } from '../../../helpers/query'; @@ -57,7 +58,7 @@ interface Props { branchLike?: BranchLike; component: Component; location: Location; - router: Pick<InjectedRouter, 'push' | 'replace'>; + router: Router; } export interface State { @@ -75,7 +76,7 @@ export const PROJECT_ACTIVITY_GRAPH = 'sonar_project_activity.graph'; const ACTIVITY_PAGE_SIZE_FIRST_BATCH = 100; const ACTIVITY_PAGE_SIZE = 500; -export default class ProjectActivityAppContainer extends React.PureComponent<Props, State> { +export class ProjectActivityAppContainer extends React.PureComponent<Props, State> { mounted = false; constructor(props: Props) { @@ -93,25 +94,8 @@ export default class ProjectActivityAppContainer extends React.PureComponent<Pro componentDidMount() { this.mounted = true; - if (this.shouldRedirect()) { - const { graph, customGraphs } = getActivityGraph( - PROJECT_ACTIVITY_GRAPH, - this.props.component.key - ); - const newQuery = { ...this.state.query, graph }; - if (isCustomGraph(newQuery.graph)) { - newQuery.customMetrics = customGraphs; - } - this.props.router.replace({ - pathname: this.props.location.pathname, - query: { - ...serializeUrlQuery(newQuery), - ...getBranchLikeQuery(this.props.branchLike) - } - }); - } else { - this.firstLoadData(this.state.query, this.props.component); - } + + this.firstLoadData(this.state.query, this.props.component); } componentDidUpdate(prevProps: Props) { @@ -371,10 +355,6 @@ export default class ProjectActivityAppContainer extends React.PureComponent<Pro }; render() { - if (this.shouldRedirect()) { - return null; - } - return ( <ProjectActivityApp addCustomEvent={this.addCustomEvent} @@ -395,3 +375,42 @@ export default class ProjectActivityAppContainer extends React.PureComponent<Pro ); } } + +const isFiltered = (searchParams: URLSearchParams) => { + let filtered = false; + searchParams.forEach((value, key) => { + if (key !== 'id' && value !== '') { + filtered = true; + } + }); + return filtered; +}; + +function RedirectWrapper(props: Props) { + const [searchParams, setSearchParams] = useSearchParams(); + + const filtered = isFiltered(searchParams); + + const { graph, customGraphs } = getActivityGraph(PROJECT_ACTIVITY_GRAPH, props.component.key); + const emptyCustomGraph = isCustomGraph(graph) && customGraphs.length <= 0; + + // if there is no filter, but there are saved preferences in the localStorage + // also don't redirect to custom if there is no metrics selected for it + const shouldRedirect = !filtered && graph != null && graph !== DEFAULT_GRAPH && !emptyCustomGraph; + + React.useEffect(() => { + if (shouldRedirect) { + const query = parseQuery(searchParams); + const newQuery = { ...query, graph }; + if (isCustomGraph(newQuery.graph)) { + searchParams.set('custom_metrics', customGraphs.join(',')); + } + searchParams.set('graph', graph); + setSearchParams(searchParams, { replace: true }); + } + }, [customGraphs, graph, searchParams, setSearchParams, shouldRedirect]); + + return shouldRedirect ? null : <ProjectActivityAppContainer {...props} />; +} + +export default withComponentContext(withRouter(RedirectWrapper)); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx index b489191f394..542540066fb 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.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 { ResetButtonLink } from '../../../components/controls/buttons'; import DropdownIcon from '../../../components/icons/DropdownIcon'; import Level from '../../../components/ui/Level'; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/EventInner-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/EventInner-test.tsx index 3c733053808..e83d18c993f 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/EventInner-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/EventInner-test.tsx @@ -23,7 +23,7 @@ import { mockAnalysisEvent } from '../../../../helpers/testMocks'; import { BranchLike } from '../../../../types/branch-like'; import EventInner, { EventInnerProps } from '../EventInner'; -jest.mock('../../../../app/components/ComponentContext', () => { +jest.mock('../../../../app/components/componentContext/ComponentContext', () => { const { mockBranch } = jest.requireActual('../../../../helpers/mocks/branch-like'); return { ComponentContext: { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx new file mode 100644 index 00000000000..c981572da89 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx @@ -0,0 +1,120 @@ +/* + * 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 React from 'react'; +import { ComponentContext } from '../../../../app/components/componentContext/ComponentContext'; +import { getActivityGraph } from '../../../../components/activity-graph/utils'; +import { mockComponent } from '../../../../helpers/mocks/component'; +import { renderComponentApp } from '../../../../helpers/testReactTestingUtils'; +import { ComponentQualifier } from '../../../../types/component'; +import { Component } from '../../../../types/types'; +import ProjectActivityAppContainer from '../ProjectActivityAppContainer'; + +jest.mock('../../../../api/time-machine', () => { + const { mockPaging } = jest.requireActual('../../../../helpers/testMocks'); + return { + getAllTimeMachineData: jest.fn().mockResolvedValue({ + measures: [ + { + metric: 'bugs', + history: [{ date: '2022-01-01', value: '10' }] + } + ], + paging: mockPaging({ total: 1 }) + }) + }; +}); + +jest.mock('../../../../api/metrics', () => { + const { mockMetric } = jest.requireActual('../../../../helpers/testMocks'); + return { + getAllMetrics: jest.fn().mockResolvedValue([mockMetric()]) + }; +}); + +jest.mock('../../../../api/projectActivity', () => { + const { mockAnalysis, mockPaging } = jest.requireActual('../../../../helpers/testMocks'); + return { + ...jest.requireActual('../../../../api/projectActivity'), + createEvent: jest.fn(), + changeEvent: jest.fn(), + getProjectActivity: jest.fn().mockResolvedValue({ + analyses: [mockAnalysis({ key: 'foo' })], + paging: mockPaging({ total: 1 }) + }) + }; +}); + +jest.mock('../../../../components/activity-graph/utils', () => { + const actual = jest.requireActual('../../../../components/activity-graph/utils'); + return { + ...actual, + getActivityGraph: jest.fn() + }; +}); + +it('should render default graph', async () => { + (getActivityGraph as jest.Mock).mockImplementation(() => { + return { + graph: 'issues' + }; + }); + + renderProjectActivityAppContainer(); + + expect(await screen.findByText('project_activity.graphs.issues')).toBeInTheDocument(); +}); + +it('should reload custom graph from local storage', async () => { + (getActivityGraph as jest.Mock).mockImplementation(() => { + return { + graph: 'custom', + customGraphs: ['bugs', 'code_smells'] + }; + }); + + renderProjectActivityAppContainer(); + + expect(await screen.findByText('project_activity.graphs.custom')).toBeInTheDocument(); +}); + +function renderProjectActivityAppContainer( + { component, navigateTo }: { component: Component; navigateTo?: string } = { + component: mockComponent({ + breadcrumbs: [ + { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project } + ] + }) + } +) { + return renderComponentApp( + 'project/activity', + <ComponentContext.Provider + value={{ + branchLikes: [], + onBranchesChange: jest.fn(), + onComponentChange: jest.fn(), + component + }}> + <ProjectActivityAppContainer /> + </ComponentContext.Provider>, + { navigateTo } + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-test.tsx index 7ebc710a5cb..bb74b018308 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-test.tsx @@ -30,7 +30,7 @@ import { import { waitAndUpdate } from '../../../../helpers/testUtils'; import { ComponentQualifier } from '../../../../types/component'; import { MetricKey } from '../../../../types/metrics'; -import ProjectActivityAppContainer from '../ProjectActivityAppContainer'; +import { ProjectActivityAppContainer } from '../ProjectActivityAppContainer'; jest.mock('../../../../helpers/dates', () => ({ parseDate: jest.fn(date => `PARSED:${date}`) diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/DefinitionChangeEventInner-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/DefinitionChangeEventInner-test.tsx.snap index d76718a77e4..d2384be643a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/DefinitionChangeEventInner-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/DefinitionChangeEventInner-test.tsx.snap @@ -71,16 +71,11 @@ exports[`should render 2`] = ` </span>, "project": <Link onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} title="Foo" to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "master", - "id": "foo", - }, + "search": "?id=foo&branch=master", } } > @@ -114,16 +109,11 @@ exports[`should render 2`] = ` </span>, "project": <Link onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} title="Bar" to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "master", - "id": "bar", - }, + "search": "?id=bar&branch=master", } } > @@ -185,16 +175,11 @@ exports[`should render for a branch 1`] = ` </span>, "project": <Link onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} title="Foo" to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "feature-x", - "id": "foo", - }, + "search": "?id=foo&branch=feature-x", } } > @@ -234,16 +219,11 @@ exports[`should render for a branch 1`] = ` </span>, "project": <Link onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} title="Bar" to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "feature-y", - "id": "bar", - }, + "search": "?id=bar&branch=feature-y", } } > diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/RichQualityGateEventInner-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/RichQualityGateEventInner-test.tsx.snap index 6e77a28f596..c263d815f9c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/RichQualityGateEventInner-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/RichQualityGateEventInner-test.tsx.snap @@ -87,16 +87,11 @@ exports[`should render 2`] = ` > <Link onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} title="Foo" to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "master", - "id": "foo", - }, + "search": "?id=foo&branch=master", } } > @@ -123,16 +118,11 @@ exports[`should render 2`] = ` > <Link onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} title="Bar" to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "master", - "id": "bar", - }, + "search": "?id=bar&branch=master", } } > diff --git a/server/sonar-web/src/main/js/apps/projectActivity/routes.ts b/server/sonar-web/src/main/js/apps/projectActivity/routes.ts deleted file mode 100644 index 9ab389a7471..00000000000 --- a/server/sonar-web/src/main/js/apps/projectActivity/routes.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - indexRoute: { - component: lazyLoadComponent(() => import('./components/ProjectActivityAppContainer')) - } - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/routes.tsx b/server/sonar-web/src/main/js/apps/projectActivity/routes.tsx new file mode 100644 index 00000000000..f94de3ff633 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/routes.tsx @@ -0,0 +1,26 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import ProjectActivityAppContainer from './components/ProjectActivityAppContainer'; + +const routes = () => <Route path="project/activity" element={<ProjectActivityAppContainer />} />; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx index 5e28f80a41d..a9021768e71 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx @@ -22,6 +22,7 @@ import { debounce } from 'lodash'; import * as React from 'react'; import { getNewCodePeriod, resetNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod'; import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; +import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; @@ -320,4 +321,4 @@ export class App extends React.PureComponent<Props, State> { } } -export default withAppStateContext(App); +export default withComponentContext(withAppStateContext(App)); diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx index ff232c6c7f0..6adb1055806 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.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 { translate } from '../../../helpers/l10n'; export interface AppHeaderProps { diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap index e698388bcc1..f411099adfa 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap @@ -18,8 +18,6 @@ exports[`should render correctly: can admin 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/project-administration/new-code-period/" > project_baseline.page.description.link @@ -34,8 +32,6 @@ exports[`should render correctly: can admin 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to="/admin/settings?category=new_code_period" > project_baseline.page.description2.link @@ -65,8 +61,6 @@ exports[`should render correctly: cannot admin 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/project-administration/new-code-period/" > project_baseline.page.description.link diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/routes.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/routes.tsx new file mode 100644 index 00000000000..789fbbdee32 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/routes.tsx @@ -0,0 +1,26 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import App from './components/App'; + +const routes = () => <Route path="baseline" element={<App />} />; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/LifetimeInformationRenderer.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/LifetimeInformationRenderer.tsx index 189bc387adb..42c6826000e 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/LifetimeInformationRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/LifetimeInformationRenderer.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 DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/ProjectBranchesApp.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/ProjectBranchesApp.tsx index fbb5c222e65..6fdab4764ea 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/ProjectBranchesApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/ProjectBranchesApp.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import { translate } from '../../../helpers/l10n'; import { BranchLike } from '../../../types/branch-like'; import { Component } from '../../../types/types'; @@ -49,4 +50,4 @@ export function ProjectBranchesApp(props: ProjectBranchesAppProps) { ); } -export default React.memo(ProjectBranchesApp); +export default withComponentContext(React.memo(ProjectBranchesApp)); diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LifetimeInformationRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LifetimeInformationRenderer-test.tsx.snap index d28884d996f..810c4081524 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LifetimeInformationRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LifetimeInformationRenderer-test.tsx.snap @@ -48,8 +48,6 @@ exports[`should render correctly when user is admin 1`] = ` values={ Object { "settings": <Link - onlyActiveOnIndex={false} - style={Object {}} to="/admin/settings" > settings.page diff --git a/server/sonar-web/src/main/js/apps/projectBranches/routes.ts b/server/sonar-web/src/main/js/apps/projectBranches/routes.ts deleted file mode 100644 index 58d607edaa5..00000000000 --- a/server/sonar-web/src/main/js/apps/projectBranches/routes.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/ProjectBranchesApp')) } - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/background-tasks/routes.ts b/server/sonar-web/src/main/js/apps/projectBranches/routes.tsx index 3fa150dbc24..00e3f2825cc 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/routes.ts +++ b/server/sonar-web/src/main/js/apps/projectBranches/routes.tsx @@ -17,12 +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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import ProjectBranchesApp from './components/ProjectBranchesApp'; -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/BackgroundTasksApp')) } - } -]; +const routes = () => <Route path="branches" element={<ProjectBranchesApp />} />; export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectDeletion/App.tsx b/server/sonar-web/src/main/js/apps/projectDeletion/App.tsx index aa523500aa3..65f1954f956 100644 --- a/server/sonar-web/src/main/js/apps/projectDeletion/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectDeletion/App.tsx @@ -19,21 +19,23 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; +import { ComponentContext } from '../../app/components/componentContext/ComponentContext'; import { translate } from '../../helpers/l10n'; -import { Component } from '../../types/types'; import Form from './Form'; import Header from './Header'; -interface Props { - component: Pick<Component, 'key' | 'name' | 'qualifier'>; -} +export default function App() { + const { component } = React.useContext(ComponentContext); + + if (component === undefined) { + return null; + } -export default function App(props: Props) { return ( <div className="page page-limited"> <Helmet defer={false} title={translate('deletion.page')} /> - <Header component={props.component} /> - <Form component={props.component} /> + <Header component={component} /> + <Form component={component} /> </div> ); } diff --git a/server/sonar-web/src/main/js/apps/projectDeletion/Form.tsx b/server/sonar-web/src/main/js/apps/projectDeletion/Form.tsx index 6f2de530e76..c087dddab94 100644 --- a/server/sonar-web/src/main/js/apps/projectDeletion/Form.tsx +++ b/server/sonar-web/src/main/js/apps/projectDeletion/Form.tsx @@ -30,7 +30,7 @@ import { Component } from '../../types/types'; interface Props { component: Pick<Component, 'key' | 'name' | 'qualifier'>; - router: Pick<Router, 'replace'>; + router: Router; } export class Form extends React.PureComponent<Props> { diff --git a/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/App-test.tsx index 351b3a06ff8..aad3a0b30d7 100644 --- a/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/App-test.tsx @@ -17,12 +17,32 @@ * 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/react'; import * as React from 'react'; +import { ComponentContext } from '../../../app/components/componentContext/ComponentContext'; +import { mockComponent } from '../../../helpers/mocks/component'; +import { renderComponentApp } from '../../../helpers/testReactTestingUtils'; +import { ComponentContextShape } from '../../../types/component'; +import { Component } from '../../../types/types'; import App from '../App'; -it('should render', () => { - expect( - shallow(<App component={{ key: 'foo', name: 'Foo', qualifier: 'TRK' }} />) - ).toMatchSnapshot(); +it('should render with component', () => { + renderProjectDeletionApp(mockComponent({ key: 'foo', name: 'Foo', qualifier: 'TRK' })); + + expect(screen.getByText('deletion.page')).toBeInTheDocument(); }); + +it('should render with no component', () => { + renderProjectDeletionApp(); + + expect(screen.queryByText('deletion.page')).not.toBeInTheDocument(); +}); + +function renderProjectDeletionApp(component?: Component) { + renderComponentApp( + 'project-delete', + <ComponentContext.Provider value={{ component } as ComponentContextShape}> + <App /> + </ComponentContext.Provider> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/__snapshots__/App-test.tsx.snap deleted file mode 100644 index b42883c32a7..00000000000 --- a/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/__snapshots__/App-test.tsx.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render 1`] = ` -<div - className="page page-limited" -> - <Helmet - defer={false} - encodeSpecialCharacters={true} - prioritizeSeoTags={false} - title="deletion.page" - /> - <Header - component={ - Object { - "key": "foo", - "name": "Foo", - "qualifier": "TRK", - } - } - /> - <withRouter(Form) - component={ - Object { - "key": "foo", - "name": "Foo", - "qualifier": "TRK", - } - } - /> -</div> -`; diff --git a/server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx b/server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx index 43c17531ec2..6342f16940a 100644 --- a/server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { getActivity } from '../../api/ce'; import { getStatus } from '../../api/project-dump'; import withAppStateContext from '../../app/components/app-state/withAppStateContext'; +import withComponentContext from '../../app/components/componentContext/withComponentContext'; import { throwGlobalError } from '../../helpers/error'; import { translate } from '../../helpers/l10n'; import { AppState } from '../../types/appstate'; @@ -199,4 +200,4 @@ export class ProjectDumpApp extends React.Component<Props, State> { } } -export default withAppStateContext(ProjectDumpApp); +export default withComponentContext(withAppStateContext(ProjectDumpApp)); diff --git a/server/sonar-web/src/main/js/apps/projectDump/components/Import.tsx b/server/sonar-web/src/main/js/apps/projectDump/components/Import.tsx index 000906deebe..39a28c846ef 100644 --- a/server/sonar-web/src/main/js/apps/projectDump/components/Import.tsx +++ b/server/sonar-web/src/main/js/apps/projectDump/components/Import.tsx @@ -19,7 +19,7 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { doImport } from '../../../api/project-dump'; import { Button } from '../../../components/controls/buttons'; import DateFromNow from '../../../components/intl/DateFromNow'; diff --git a/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Import-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Import-test.tsx.snap index 6073edbe109..0c62310f98d 100644 --- a/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Import-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Import-test.tsx.snap @@ -43,16 +43,11 @@ exports[`should render correctly: failed 1`] = ` project_dump.failed_import <Link className="spacer-left" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/background_tasks", - "query": Object { - "id": "key", - "status": "FAILED", - "taskType": "PROJECT_IMPORT", - }, + "search": "?id=key&status=FAILED&taskType=PROJECT_IMPORT", } } > diff --git a/server/sonar-web/src/main/js/apps/projectDump/routes.ts b/server/sonar-web/src/main/js/apps/projectDump/routes.tsx index db889495b12..331c7789909 100644 --- a/server/sonar-web/src/main/js/apps/projectDump/routes.ts +++ b/server/sonar-web/src/main/js/apps/projectDump/routes.tsx @@ -17,12 +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 React from 'react'; +import { Route } from 'react-router-dom'; import ProjectDumpApp from './ProjectDumpApp'; -const routes = [ - { - indexRoute: { component: ProjectDumpApp } - } -]; +const routes = () => <Route path="import_export" element={<ProjectDumpApp />} />; export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectKey/Key.tsx b/server/sonar-web/src/main/js/apps/projectKey/Key.tsx index 39f2a8438b4..93a833b6e30 100644 --- a/server/sonar-web/src/main/js/apps/projectKey/Key.tsx +++ b/server/sonar-web/src/main/js/apps/projectKey/Key.tsx @@ -19,18 +19,20 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { withRouter, WithRouterProps } from 'react-router'; import { changeKey } from '../../api/components'; +import withComponentContext from '../../app/components/componentContext/withComponentContext'; import RecentHistory from '../../app/components/RecentHistory'; +import { Router, withRouter } from '../../components/hoc/withRouter'; import { translate } from '../../helpers/l10n'; import { Component } from '../../types/types'; import UpdateForm from './UpdateForm'; interface Props { - component: Pick<Component, 'key' | 'name'>; + component: Component; + router: Router; } -export class Key extends React.PureComponent<Props & WithRouterProps> { +export class Key extends React.PureComponent<Props> { handleChangeKey = (newKey: string) => { return changeKey({ from: this.props.component.key, to: newKey }).then(() => { RecentHistory.remove(this.props.component.key); @@ -53,4 +55,4 @@ export class Key extends React.PureComponent<Props & WithRouterProps> { } } -export default withRouter(Key); +export default withComponentContext(withRouter(Key)); diff --git a/server/sonar-web/src/main/js/apps/projectKey/__tests__/Key-test.tsx b/server/sonar-web/src/main/js/apps/projectKey/__tests__/Key-test.tsx index 75247121c5c..bc9ba0fa64f 100644 --- a/server/sonar-web/src/main/js/apps/projectKey/__tests__/Key-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectKey/__tests__/Key-test.tsx @@ -19,8 +19,8 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { WithRouterProps } from 'react-router'; import { changeKey } from '../../../api/components'; +import { mockComponent } from '../../../helpers/mocks/component'; import { Key } from '../Key'; jest.mock('../../../api/components', () => ({ @@ -28,8 +28,10 @@ jest.mock('../../../api/components', () => ({ })); it('should render and change key', async () => { - const withRouterProps = { router: { replace: jest.fn() } as any } as WithRouterProps; - const wrapper = shallow(<Key component={{ key: 'foo', name: 'Foo' }} {...withRouterProps} />); + const withRouterProps = { router: { replace: jest.fn() } as any }; + const wrapper = shallow( + <Key component={mockComponent({ key: 'foo', name: 'Foo' })} {...withRouterProps} /> + ); expect(wrapper).toMatchSnapshot(); wrapper.find('UpdateForm').prop<Function>('onKeyChange')('bar'); diff --git a/server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/Key-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/Key-test.tsx.snap index 24be29a2347..4d009e92500 100644 --- a/server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/Key-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/Key-test.tsx.snap @@ -28,8 +28,24 @@ exports[`should render and change key 1`] = ` <UpdateForm component={ Object { + "breadcrumbs": Array [], "key": "foo", "name": "Foo", + "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 [], } } onKeyChange={[Function]} diff --git a/server/sonar-web/src/main/js/apps/projectLinks/App.tsx b/server/sonar-web/src/main/js/apps/projectLinks/App.tsx index b28a709ab84..c70f6a1648e 100644 --- a/server/sonar-web/src/main/js/apps/projectLinks/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectLinks/App.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { createLink, deleteLink, getProjectLinks } from '../../api/projectLinks'; +import withComponentContext from '../../app/components/componentContext/withComponentContext'; import DeferredSpinner from '../../components/ui/DeferredSpinner'; import { translate } from '../../helpers/l10n'; import { Component, ProjectLink } from '../../types/types'; @@ -27,7 +28,7 @@ import Header from './Header'; import Table from './Table'; interface Props { - component: Pick<Component, 'key'>; + component: Component; } interface State { @@ -35,7 +36,7 @@ interface State { loading: boolean; } -export default class App extends React.PureComponent<Props, State> { +export class App extends React.PureComponent<Props, State> { mounted = false; state: State = { loading: true }; @@ -102,3 +103,5 @@ export default class App extends React.PureComponent<Props, State> { ); } } + +export default withComponentContext(App); diff --git a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/App-test.tsx index 04c7b55911f..6eabdd1a432 100644 --- a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/App-test.tsx @@ -20,8 +20,9 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { createLink, deleteLink, getProjectLinks } from '../../../api/projectLinks'; +import { mockComponent } from '../../../helpers/mocks/component'; import { waitAndUpdate } from '../../../helpers/testUtils'; -import App from '../App'; +import { App } from '../App'; // import { getProjectLinks, createLink, deleteLink } from '../../api/projectLinks'; jest.mock('../../../api/projectLinks', () => ({ @@ -36,14 +37,14 @@ jest.mock('../../../api/projectLinks', () => ({ })); it('should fetch links and render', async () => { - const wrapper = shallow(<App component={{ key: 'comp' }} />); + const wrapper = shallow(<App component={mockComponent({ key: 'comp' })} />); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); expect(getProjectLinks).toBeCalledWith('comp'); }); it('should fetch links when component changes', async () => { - const wrapper = shallow(<App component={{ key: 'comp' }} />); + const wrapper = shallow(<App component={mockComponent({ key: 'comp' })} />); await waitAndUpdate(wrapper); expect(getProjectLinks).lastCalledWith('comp'); @@ -52,7 +53,7 @@ it('should fetch links when component changes', async () => { }); it('should create link', async () => { - const wrapper = shallow(<App component={{ key: 'comp' }} />); + const wrapper = shallow(<App component={mockComponent({ key: 'comp' })} />); await waitAndUpdate(wrapper); wrapper.find('Header').prop<Function>('onCreate')('bar', 'http://example.com/bar'); @@ -66,7 +67,7 @@ it('should create link', async () => { }); it('should delete link', async () => { - const wrapper = shallow(<App component={{ key: 'comp' }} />); + const wrapper = shallow(<App component={mockComponent({ key: 'comp' })} />); await waitAndUpdate(wrapper); wrapper.find('Table').prop<Function>('onDelete')('foo'); diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx index 8f5df7ab9a1..64ba93947c4 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx @@ -26,6 +26,7 @@ import { getGateForProject, searchProjects } from '../../api/quality-gates'; +import withComponentContext from '../../app/components/componentContext/withComponentContext'; import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; import { addGlobalSuccessMessage } from '../../helpers/globalMessages'; import { translate } from '../../helpers/l10n'; @@ -46,7 +47,7 @@ interface State { submitting: boolean; } -export default class ProjectQualityGateApp extends React.PureComponent<Props, State> { +export class ProjectQualityGateApp extends React.PureComponent<Props, State> { mounted = false; state: State = { loading: true, @@ -201,3 +202,5 @@ export default class ProjectQualityGateApp extends React.PureComponent<Props, St ); } } + +export default withComponentContext(ProjectQualityGateApp); diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx index 9c211b81b73..3cae6090497 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { components, OptionProps } from 'react-select'; import A11ySkipTarget from '../../components/a11y/A11ySkipTarget'; import DisableableSelectOption from '../../components/common/DisableableSelectOption'; diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx index a45b3468083..525826ad426 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx @@ -31,7 +31,7 @@ import { mockComponent } from '../../../helpers/mocks/component'; import { mockQualityGate } from '../../../helpers/mocks/quality-gates'; import { waitAndUpdate } from '../../../helpers/testUtils'; import { USE_SYSTEM_DEFAULT } from '../constants'; -import ProjectQualityGateApp from '../ProjectQualityGateApp'; +import { ProjectQualityGateApp } from '../ProjectQualityGateApp'; jest.mock('../../../api/quality-gates', () => { const { mockQualityGate } = jest.requireActual('../../../helpers/mocks/quality-gates'); diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap index 2d639fb2fd9..d46f48ec678 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap @@ -479,8 +479,6 @@ exports[`should render correctly: show new code warning 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/quality_gates/show/3", diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts b/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts deleted file mode 100644 index 9d677aa62e1..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./ProjectQualityGateApp')) } - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/routes.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/routes.tsx new file mode 100644 index 00000000000..22a9040d379 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/routes.tsx @@ -0,0 +1,26 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import ProjectQualityGateApp from './ProjectQualityGateApp'; + +const routes = () => <Route path="project/quality_gate" element={<ProjectQualityGateApp />} />; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx index 25455df1466..03aa8b8a692 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx @@ -26,6 +26,7 @@ import { Profile, searchQualityProfiles } from '../../api/quality-profiles'; +import withComponentContext from '../../app/components/componentContext/withComponentContext'; import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; import { addGlobalSuccessMessage } from '../../helpers/globalMessages'; import { translateWithParameters } from '../../helpers/l10n'; @@ -46,7 +47,7 @@ interface State { showProjectProfileInModal?: ProjectProfile; } -export default class ProjectQualityProfilesApp extends React.PureComponent<Props, State> { +export class ProjectQualityProfilesApp extends React.PureComponent<Props, State> { mounted = false; state: State = { loading: true }; @@ -291,3 +292,5 @@ export default class ProjectQualityProfilesApp extends React.PureComponent<Props ); } } + +export default withComponentContext(ProjectQualityProfilesApp); diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesAppRenderer.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesAppRenderer.tsx index 28445dfe0e3..8fd0186beae 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesAppRenderer.tsx @@ -20,7 +20,7 @@ import { groupBy, orderBy } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { Profile } from '../../api/quality-profiles'; import A11ySkipTarget from '../../components/a11y/A11ySkipTarget'; import { Button } from '../../components/controls/buttons'; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx index 90777d416e7..ffc9eb0f6a0 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx @@ -29,7 +29,7 @@ import { import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; import { mockComponent } from '../../../helpers/mocks/component'; import { waitAndUpdate } from '../../../helpers/testUtils'; -import ProjectQualityProfilesApp from '../ProjectQualityProfilesApp'; +import { ProjectQualityProfilesApp } from '../ProjectQualityProfilesApp'; jest.mock('../../../api/quality-profiles', () => { const { mockQualityProfile } = jest.requireActual('../../../helpers/testMocks'); diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProjectQualityProfilesAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProjectQualityProfilesAppRenderer-test.tsx.snap index 838cb7835cf..08e1b441407 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProjectQualityProfilesAppRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProjectQualityProfilesAppRenderer-test.tsx.snap @@ -100,15 +100,10 @@ exports[`should render correctly: add language 1`] = ` className="nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "bar", - }, + "search": "?activation=true&qprofile=bar", } } > @@ -147,15 +142,10 @@ exports[`should render correctly: add language 1`] = ` className="nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "baz", - }, + "search": "?activation=true&qprofile=baz", } } > @@ -196,15 +186,10 @@ exports[`should render correctly: add language 1`] = ` className="nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "foo", - }, + "search": "?activation=true&qprofile=foo", } } > @@ -415,15 +400,10 @@ exports[`should render correctly: default 1`] = ` className="nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "bar", - }, + "search": "?activation=true&qprofile=bar", } } > @@ -462,15 +442,10 @@ exports[`should render correctly: default 1`] = ` className="nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "baz", - }, + "search": "?activation=true&qprofile=baz", } } > @@ -511,15 +486,10 @@ exports[`should render correctly: default 1`] = ` className="nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "foo", - }, + "search": "?activation=true&qprofile=foo", } } > @@ -751,15 +721,10 @@ exports[`should render correctly: open profile 1`] = ` className="nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "bar", - }, + "search": "?activation=true&qprofile=bar", } } > @@ -798,15 +763,10 @@ exports[`should render correctly: open profile 1`] = ` className="nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "baz", - }, + "search": "?activation=true&qprofile=baz", } } > @@ -847,15 +807,10 @@ exports[`should render correctly: open profile 1`] = ` className="nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "foo", - }, + "search": "?activation=true&qprofile=foo", } } > diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/LanguageProfileSelectOption.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/LanguageProfileSelectOption.tsx index 376f4b96316..b506a93878e 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/LanguageProfileSelectOption.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/LanguageProfileSelectOption.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 { components, OptionProps } from 'react-select'; import DisableableSelectOption from '../../../components/common/DisableableSelectOption'; import { BasicSelectOption } from '../../../components/controls/Select'; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/LanguageProfileSelectOption-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/LanguageProfileSelectOption-test.tsx.snap index 3c3a2ec0c26..534f179f976 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/LanguageProfileSelectOption-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/LanguageProfileSelectOption-test.tsx.snap @@ -34,15 +34,10 @@ exports[`tooltip should render correctly: default 1`] = ` project_quality_profile.add_language_modal.profile_unavailable_no_active_rules </p> <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/profiles/show", - "query": Object { - "language": "Java", - "name": "Profile 1", - }, + "search": "?name=Profile+1&language=Java", } } > diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts b/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts deleted file mode 100644 index 7e0a67bb37d..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - indexRoute: { - component: lazyLoadComponent(() => import('./ProjectQualityProfilesApp')) - } - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.tsx new file mode 100644 index 00000000000..35add6e93ff --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.tsx @@ -0,0 +1,28 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import ProjectQualityProfilesApp from './ProjectQualityProfilesApp'; + +const routes = () => ( + <Route path="project/quality_profiles" element={<ProjectQualityProfilesApp />} /> +); + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx index a60da0bc7b4..d7de7ef7727 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx @@ -20,6 +20,7 @@ import { omitBy } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; +import { useSearchParams } from 'react-router-dom'; import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; @@ -37,7 +38,7 @@ import { AppState } from '../../../types/appstate'; import { ComponentQualifier } from '../../../types/component'; import { RawQuery } from '../../../types/types'; import { CurrentUser, isLoggedIn } from '../../../types/users'; -import { hasFilterParams, hasViewParams, parseUrlQuery, Query } from '../query'; +import { hasFilterParams, parseUrlQuery, Query } from '../query'; import '../styles.css'; import { Facets, Project } from '../types'; import { fetchProjects, parseSorting, SORTING_SWITCH } from '../utils'; @@ -48,9 +49,9 @@ import ProjectsList from './ProjectsList'; interface Props { currentUser: CurrentUser; isFavorite: boolean; - location: Pick<Location, 'pathname' | 'query'>; + location: Location; appState: AppState; - router: Pick<Router, 'push' | 'replace'>; + router: Router; } interface State { @@ -80,13 +81,13 @@ export class AllProjects extends React.PureComponent<Props, State> { handleRequiredAuthentication(); return; } - this.handleQueryChange(true); + this.handleQueryChange(); addSideBarClass(); } componentDidUpdate(prevProps: Props) { if (prevProps.location.query !== this.props.location.query) { - this.handleQueryChange(false); + this.handleQueryChange(); } } @@ -128,20 +129,6 @@ export class AllProjects extends React.PureComponent<Props, State> { getSort = () => this.state.query.sort || 'name'; - getStorageOptions = () => { - const options: { - sort?: string; - view?: string; - } = {}; - if (get(LS_PROJECTS_SORT)) { - options.sort = get(LS_PROJECTS_SORT) || undefined; - } - if (get(LS_PROJECTS_VIEW)) { - options.view = get(LS_PROJECTS_VIEW) || undefined; - } - return options; - }; - getView = () => this.state.query.view || 'overall'; handleClearAll = () => { @@ -184,16 +171,10 @@ export class AllProjects extends React.PureComponent<Props, State> { save(LS_PROJECTS_VIEW, query.view); }; - handleQueryChange(initialMount: boolean) { + handleQueryChange() { const query = parseUrlQuery(this.props.location.query); - const savedOptions = this.getStorageOptions(); - const savedOptionsSet = savedOptions.sort || savedOptions.view; - if (initialMount && !hasViewParams(query) && savedOptionsSet) { - this.props.router.replace({ pathname: this.props.location.pathname, query: savedOptions }); - } else { - this.fetchProjects(query); - } + this.fetchProjects(query); } handleSortChange = (sort: string, desc: boolean) => { @@ -318,4 +299,40 @@ export class AllProjects extends React.PureComponent<Props, State> { } } -export default withRouter(withCurrentUserContext(withAppStateContext(AllProjects))); +function getStorageOptions() { + const options: { + sort?: string; + view?: string; + } = {}; + if (get(LS_PROJECTS_SORT)) { + options.sort = get(LS_PROJECTS_SORT) || undefined; + } + if (get(LS_PROJECTS_VIEW)) { + options.view = get(LS_PROJECTS_VIEW) || undefined; + } + return options; +} + +function SetSearchParamsWrapper(props: Props) { + const [searchParams, setSearchParams] = useSearchParams(); + const savedOptions = getStorageOptions(); + + React.useEffect( + () => { + const hasViewParams = searchParams.get('sort') || searchParams.get('view'); + const hasSavedOptions = savedOptions.sort || savedOptions.view; + + if (!hasViewParams && hasSavedOptions) { + setSearchParams(savedOptions); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + /* Run once on mount only */ + ] + ); + + return <AllProjects {...props} />; +} + +export default withRouter(withCurrentUserContext(withAppStateContext(SetSearchParamsWrapper))); diff --git a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx index d8886bd7be4..fa938e1a828 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx @@ -18,84 +18,84 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; import { searchProjects } from '../../../api/components'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; -import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; +import { useLocation } from '../../../components/hoc/withRouter'; import { get } from '../../../helpers/storage'; import { hasGlobalPermission } from '../../../helpers/users'; import { CurrentUser, isLoggedIn } from '../../../types/users'; import { PROJECTS_ALL, PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE } from '../utils'; import AllProjectsContainer from './AllProjectsContainer'; -interface Props { +export interface DefaultPageSelectorProps { currentUser: CurrentUser; - location: Pick<Location, 'pathname' | 'query'>; - router: Pick<Router, 'replace'>; } -interface State { - checking: boolean; -} - -export class DefaultPageSelector extends React.PureComponent<Props, State> { - state: State = { checking: true }; - - componentDidMount() { - this.checkIfNeedsRedirecting(); - } - - checkIfNeedsRedirecting = async () => { - const { currentUser, router, location } = this.props; - const setting = get(PROJECTS_DEFAULT_FILTER); +export function DefaultPageSelector(props: DefaultPageSelectorProps) { + const [checking, setChecking] = React.useState(true); + const navigate = useNavigate(); + const location = useLocation(); - // 1. Don't have to redirect if: - // 1.1 User is anonymous - // 1.2 There's a query, which means the user is interacting with the current page - // 1.3 The last interaction with the filter was to set it to "all" - if ( - !isLoggedIn(currentUser) || - Object.keys(location.query).length > 0 || - setting === PROJECTS_ALL - ) { - this.setState({ checking: false }); - return; - } + React.useEffect( + () => { + async function checkRedirect() { + const { currentUser } = props; + const setting = get(PROJECTS_DEFAULT_FILTER); - // 2. Redirect to the favorites page if: - // 2.1 The last interaction with the filter was to set it to "favorites" - // 2.2 The user has starred some projects - if ( - setting === PROJECTS_FAVORITE || - (await searchProjects({ filter: 'isFavorite', ps: 1 })).paging.total > 0 - ) { - router.replace('/projects/favorite'); - return; - } + // 1. Don't have to redirect if: + // 1.1 User is anonymous + // 1.2 There's a query, which means the user is interacting with the current page + // 1.3 The last interaction with the filter was to set it to "all" + if ( + !isLoggedIn(currentUser) || + Object.keys(location.query).length > 0 || + setting === PROJECTS_ALL + ) { + setChecking(false); + return; + } - // 3. Redirect to the create project page if: - // 3.1 The user has permission to provision projects, AND there are 0 projects on the instance - if ( - hasGlobalPermission(currentUser, 'provisioning') && - (await searchProjects({ ps: 1 })).paging.total === 0 - ) { - this.props.router.replace('/projects/create'); - } + // 2. Redirect to the favorites page if: + // 2.1 The last interaction with the filter was to set it to "favorites" + // 2.2 The user has starred some projects + if ( + setting === PROJECTS_FAVORITE || + (await searchProjects({ filter: 'isFavorite', ps: 1 })).paging.total > 0 + ) { + navigate('/projects/favorite', { replace: true }); + return; + } - // None of the above apply. Do not redirect, and stay on this page. - this.setState({ checking: false }); - }; + // 3. Redirect to the create project page if: + // 3.1 The user has permission to provision projects, AND there are 0 projects on the instance + if ( + hasGlobalPermission(currentUser, 'provisioning') && + (await searchProjects({ ps: 1 })).paging.total === 0 + ) { + navigate('/projects/create', { replace: true }); + return; + } - render() { - const { checking } = this.state; + // None of the above apply. Do not redirect, and stay on this page. + setChecking(false); + } - if (checking) { - // We don't return a loader here, on purpose. We don't want to show anything - // just yet. - return null; - } + checkRedirect(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + /* run only once on mount*/ + ] + ); - return <AllProjectsContainer isFavorite={false} />; + if (checking) { + // We don't return a loader here, on purpose. We don't want to show anything + // just yet. + return null; } + + return <AllProjectsContainer isFavorite={false} />; } -export default withCurrentUserContext(withRouter(DefaultPageSelector)); +export default withCurrentUserContext(DefaultPageSelector); diff --git a/server/sonar-web/src/main/js/apps/projects/components/EmptyFavoriteSearch.tsx b/server/sonar-web/src/main/js/apps/projects/components/EmptyFavoriteSearch.tsx index 8b3197d67e9..b5cde9bdd1d 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/EmptyFavoriteSearch.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/EmptyFavoriteSearch.tsx @@ -19,9 +19,11 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import '../../../components/common/EmptySearch.css'; import { translate } from '../../../helpers/l10n'; +import { queryToSearch } from '../../../helpers/urls'; +import { Dict } from '../../../types/types'; import { Query } from '../query'; export default function EmptyFavoriteSearch({ query }: { query: Query }) { @@ -33,7 +35,15 @@ export default function EmptyFavoriteSearch({ query }: { query: Query }) { defaultMessage={translate('no_results_search.favorites.2')} id="no_results_search.favorites.2" values={{ - url: <Link to={{ pathname: '/projects', query }}>{translate('all')}</Link> + url: ( + <Link + to={{ + pathname: '/projects', + search: queryToSearch(query as Dict<string | undefined | number>) + }}> + {translate('all')} + </Link> + ) }} /> </p> diff --git a/server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx b/server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx index 6a201c2eb1b..fe09b641a15 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx @@ -18,9 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { WithRouterProps } from 'react-router'; import { Button } from '../../../components/controls/buttons'; -import { withRouter } from '../../../components/hoc/withRouter'; +import { Router, withRouter } from '../../../components/hoc/withRouter'; import { translate } from '../../../helpers/l10n'; import { hasGlobalPermission } from '../../../helpers/users'; import { Permissions } from '../../../types/permissions'; @@ -28,7 +27,7 @@ import { CurrentUser, isLoggedIn } from '../../../types/users'; export interface EmptyInstanceProps { currentUser: CurrentUser; - router: WithRouterProps['router']; + router: Router; } export function EmptyInstance(props: EmptyInstanceProps) { diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx index 2b5374afe61..d6847d5756b 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx @@ -18,10 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { IndexLink, Link } from 'react-router'; +import { NavLink } from 'react-router-dom'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import { translate } from '../../../helpers/l10n'; import { save } from '../../../helpers/storage'; +import { queryToSearch } from '../../../helpers/urls'; import { RawQuery } from '../../../types/types'; import { CurrentUser, isLoggedIn } from '../../../types/users'; import { PROJECTS_ALL, PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE } from '../utils'; @@ -31,6 +32,9 @@ interface Props { query?: RawQuery; } +const linkClass = ({ isActive }: { isActive: boolean }) => + isActive ? 'button button-active' : 'button'; + export class FavoriteFilter extends React.PureComponent<Props> { handleSaveFavorite = () => { save(PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE); @@ -48,25 +52,26 @@ export class FavoriteFilter extends React.PureComponent<Props> { const pathnameForFavorite = '/projects/favorite'; const pathnameForAll = '/projects'; + const search = queryToSearch(this.props.query); + return ( <div className="page-header text-center"> <div className="button-group little-spacer-top"> - <Link - activeClassName="button-active" - className="button" + <NavLink + className={linkClass} id="favorite-projects" onClick={this.handleSaveFavorite} - to={{ pathname: pathnameForFavorite, query: this.props.query }}> + to={{ pathname: pathnameForFavorite, search }}> {translate('my_favorites')} - </Link> - <IndexLink - activeClassName="button-active" - className="button" + </NavLink> + <NavLink + end={true} + className={linkClass} id="all-projects" onClick={this.handleSaveAll} - to={{ pathname: pathnameForAll, query: this.props.query }}> + to={{ pathname: pathnameForAll, search }}> {translate('all')} - </IndexLink> + </NavLink> </div> </div> ); diff --git a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx index 77f23173f20..1cf78b4a997 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.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'; export default function NoFavoriteProjects() { diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx index b1cd167c7f5..3fa8a85e11d 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.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 { getAlmSettings } from '../../../api/alm-settings'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import { Button } from '../../../components/controls/buttons'; diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx index 32bb5c92a32..7d933de7d9d 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx @@ -18,10 +18,11 @@ * 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 ChevronsIcon from '../../../components/icons/ChevronsIcon'; import { translate } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/system'; +import { queryToSearch } from '../../../helpers/urls'; import { AlmKeys } from '../../../types/alm-settings'; export interface ProjectCreationMenuItemProps { @@ -37,7 +38,7 @@ export default function ProjectCreationMenuItem(props: ProjectCreationMenuItemPr return ( <Link className="display-flex-center" - to={{ pathname: '/projects/create', query: { mode: alm } }}> + to={{ pathname: '/projects/create', search: queryToSearch({ mode: alm }) }}> {alm === 'manual' ? ( <ChevronsIcon className="spacer-right" /> ) : ( diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx index 63b97a12434..77d28e87274 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx @@ -20,9 +20,8 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { get, save } from '../../../../helpers/storage'; -import { mockAppState } from '../../../../helpers/testMocks'; +import { mockAppState, mockLocation } from '../../../../helpers/testMocks'; import { ComponentQualifier } from '../../../../types/component'; -import { Dict } from '../../../../types/types'; import { AllProjects, LS_PROJECTS_SORT, LS_PROJECTS_VIEW } from '../AllProjects'; jest.mock( @@ -103,25 +102,6 @@ it('fetches projects', () => { ); }); -it('redirects to the saved search', () => { - const localeStorageMock: Dict<string> = { - [LS_PROJECTS_VIEW]: 'leak', - [LS_PROJECTS_SORT]: 'coverage' - }; - - (get as jest.Mock).mockImplementation((key: string) => localeStorageMock[key]); - const replace = jest.fn(); - shallowRender({}, jest.fn(), replace); - - expect(replace).lastCalledWith({ - pathname: '/projects', - query: { - view: localeStorageMock[LS_PROJECTS_VIEW], - sort: localeStorageMock[LS_PROJECTS_SORT] - } - }); -}); - it('changes sort', () => { const push = jest.fn(); const wrapper = shallowRender({}, push); @@ -174,7 +154,7 @@ function shallowRender( <AllProjects currentUser={{ isLoggedIn: true }} isFavorite={false} - location={{ pathname: '/projects', query: {} }} + location={mockLocation({ pathname: '/projects', query: {} })} appState={mockAppState({ qualifiers: [ComponentQualifier.Project, ComponentQualifier.Application] })} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ApplicationCreation-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ApplicationCreation-test.tsx index 4528e3afe64..ecdfa1b29fe 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ApplicationCreation-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ApplicationCreation-test.tsx @@ -23,6 +23,7 @@ import { getComponentNavigation } from '../../../../api/nav'; import CreateApplicationForm from '../../../../app/components/extensions/CreateApplicationForm'; import { Button } from '../../../../components/controls/buttons'; import { mockAppState, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks'; +import { queryToSearch } from '../../../../helpers/urls'; import { ComponentQualifier } from '../../../../types/component'; import { ApplicationCreation, ApplicationCreationProps } from '../ApplicationCreation'; @@ -51,9 +52,9 @@ it('should show form and callback when submitted - admin', async () => { expect(routerPush).toBeCalledWith({ pathname: '/project/admin/extension/developer-server/application-console', - query: { + search: queryToSearch({ id: 'new app' - } + }) }); }); @@ -68,9 +69,9 @@ it('should show form and callback when submitted - user', async () => { expect(routerPush).toBeCalledWith({ pathname: '/dashboard', - query: { + search: queryToSearch({ id: 'new app' - } + }) }); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx index 2dacccf17b2..2c35ecc0571 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx @@ -17,17 +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 { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import * as React from 'react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { searchProjects } from '../../../../api/components'; +import { useLocation } from '../../../../components/hoc/withRouter'; import { get } from '../../../../helpers/storage'; -import { - mockCurrentUser, - mockLocation, - mockLoggedInUser, - mockRouter -} from '../../../../helpers/testMocks'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks'; import { hasGlobalPermission } from '../../../../helpers/users'; import { CurrentUser } from '../../../../types/users'; import { DefaultPageSelector } from '../DefaultPageSelector'; @@ -37,7 +33,7 @@ jest.mock( () => // eslint-disable-next-line function AllProjectsContainer() { - return null; + return <div>All Projects</div>; } ); @@ -56,102 +52,82 @@ jest.mock('../../../../api/components', () => ({ beforeEach(jest.clearAllMocks); -it('renders correctly', () => { - expect(shallowRender({ currentUser: mockLoggedInUser() }).type()).toBeNull(); // checking - expect(shallowRender({ currentUser: mockCurrentUser() })).toMatchSnapshot('default'); -}); - it("1.1 doesn't redirect for anonymous users", async () => { - const replace = jest.fn(); - const wrapper = shallowRender({ - currentUser: mockCurrentUser(), - router: mockRouter({ replace }) - }); - await waitAndUpdate(wrapper); - expect(replace).not.toBeCalled(); + renderDefaultPageSelector({ currentUser: mockCurrentUser() }); + + expect(await screen.findByText('All Projects')).toBeInTheDocument(); }); it("1.2 doesn't redirect if there's an existing filter in location", async () => { - const replace = jest.fn(); - const wrapper = shallowRender({ - location: mockLocation({ query: { size: '1' } }), - router: mockRouter({ replace }) - }); + renderDefaultPageSelector({ path: '/projects?size=1' }); - await waitAndUpdate(wrapper); - - expect(replace).not.toBeCalled(); + expect(await screen.findByText('All Projects')).toBeInTheDocument(); }); it("1.3 doesn't redirect if the user previously used the 'all' filter", async () => { (get as jest.Mock).mockReturnValueOnce('all'); - const replace = jest.fn(); - const wrapper = shallowRender({ router: mockRouter({ replace }) }); - - await waitAndUpdate(wrapper); + renderDefaultPageSelector(); - expect(replace).not.toBeCalled(); + expect(await screen.findByText('All Projects')).toBeInTheDocument(); }); it('2.1 redirects to favorites if the user previously used the "favorites" filter', async () => { (get as jest.Mock).mockReturnValueOnce('favorite'); - const replace = jest.fn(); - const wrapper = shallowRender({ router: mockRouter({ replace }) }); - - await waitAndUpdate(wrapper); + renderDefaultPageSelector(); - expect(replace).toBeCalledWith('/projects/favorite'); + expect(await screen.findByText('/projects/favorite')).toBeInTheDocument(); }); it('2.2 redirects to favorites if the user has starred projects', async () => { (searchProjects as jest.Mock).mockResolvedValueOnce({ paging: { total: 3 } }); - const replace = jest.fn(); - const wrapper = shallowRender({ router: mockRouter({ replace }) }); - - await waitAndUpdate(wrapper); + renderDefaultPageSelector(); expect(searchProjects).toHaveBeenLastCalledWith({ filter: 'isFavorite', ps: 1 }); - expect(replace).toBeCalledWith('/projects/favorite'); + expect(await screen.findByText('/projects/favorite')).toBeInTheDocument(); }); it('3.1 redirects to create project page, if user has correct permissions AND there are 0 projects', async () => { (hasGlobalPermission as jest.Mock).mockReturnValueOnce(true); - const replace = jest.fn(); - const wrapper = shallowRender({ router: mockRouter({ replace }) }); - - await waitAndUpdate(wrapper); + renderDefaultPageSelector(); - expect(replace).toBeCalledWith('/projects/create'); + expect(await screen.findByText('/projects/create')).toBeInTheDocument(); }); it("3.1 doesn't redirect to create project page, if user has no permissions", async () => { - const replace = jest.fn(); - const wrapper = shallowRender({ router: mockRouter({ replace }) }); - - await waitAndUpdate(wrapper); + renderDefaultPageSelector(); - expect(replace).not.toBeCalled(); + expect(await screen.findByText('All Projects')).toBeInTheDocument(); }); it("3.1 doesn't redirect to create project page, if there's existing projects", async () => { (searchProjects as jest.Mock) .mockResolvedValueOnce({ paging: { total: 0 } }) // no favorites .mockResolvedValueOnce({ paging: { total: 3 } }); // existing projects - const replace = jest.fn(); - const wrapper = shallowRender({ router: mockRouter({ replace }) }); + renderDefaultPageSelector(); - await waitAndUpdate(wrapper); - - expect(replace).not.toBeCalled(); + expect(await screen.findByText('All Projects')).toBeInTheDocument(); }); -function shallowRender(props: Partial<DefaultPageSelector['props']> = {}) { - return shallow<DefaultPageSelector>( - <DefaultPageSelector - currentUser={mockLoggedInUser()} - location={mockLocation({ pathname: '/projects' })} - router={mockRouter()} - {...props} - /> +function RouteDisplayer() { + const location = useLocation(); + return <div>{location.pathname}</div>; +} + +function renderDefaultPageSelector({ + path = '/projects', + currentUser = mockLoggedInUser() +}: { + path?: string; + currentUser?: CurrentUser; +} = {}) { + return render( + <MemoryRouter initialEntries={[path]}> + <Routes> + <Route path="projects"> + <Route index={true} element={<DefaultPageSelector currentUser={currentUser} />} /> + <Route path="*" element={<RouteDisplayer />} /> + </Route> + </Routes> + </MemoryRouter> ); } diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx index 58da88c51a5..07ff5c33866 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx @@ -17,37 +17,47 @@ * 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/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { save } from '../../../../helpers/storage'; -import { click } from '../../../../helpers/testUtils'; +import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import { FavoriteFilter } from '../FavoriteFilter'; jest.mock('../../../../helpers/storage', () => ({ save: jest.fn() })); -const currentUser = { isLoggedIn: true }; -const query = { size: 1 }; - beforeEach(() => { (save as jest.Mock<any>).mockClear(); }); it('renders for logged in user', () => { - expect(shallow(<FavoriteFilter currentUser={currentUser} query={query} />)).toMatchSnapshot(); + renderFavoriteFilter(); + expect(screen.queryByText('my_favorites')).toBeInTheDocument(); + expect(screen.queryByText('all')).toBeInTheDocument(); }); -it('saves last selection', () => { - const wrapper = shallow(<FavoriteFilter currentUser={currentUser} query={query} />); - click(wrapper.find('#favorite-projects')); +it('saves last selection', async () => { + const user = userEvent.setup(); + + renderFavoriteFilter(); + + await user.click(screen.getByText('my_favorites')); expect(save).toBeCalledWith('sonarqube.projects.default', 'favorite'); - click(wrapper.find('#all-projects')); + await user.click(screen.getByText('all')); expect(save).toBeCalledWith('sonarqube.projects.default', 'all'); }); it('does not render for anonymous', () => { - expect( - shallow(<FavoriteFilter currentUser={{ isLoggedIn: false }} query={query} />).type() - ).toBeNull(); + renderFavoriteFilter({ currentUser: mockCurrentUser() }); + expect(screen.queryByText('my_favorites')).not.toBeInTheDocument(); }); + +function renderFavoriteFilter({ + currentUser = mockLoggedInUser(), + query = { size: 1 } +}: Partial<FavoriteFilter['props']> = {}) { + renderComponent(<FavoriteFilter currentUser={currentUser} query={query} />); +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/DefaultPageSelector-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/DefaultPageSelector-test.tsx.snap deleted file mode 100644 index 92729bd4dbd..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/DefaultPageSelector-test.tsx.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders correctly: default 1`] = ` -<AllProjectsContainer - isFavorite={false} -/> -`; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/FavoriteFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/FavoriteFilter-test.tsx.snap deleted file mode 100644 index 55d42cf6c82..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/FavoriteFilter-test.tsx.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders for logged in user 1`] = ` -<div - className="page-header text-center" -> - <div - className="button-group little-spacer-top" - > - <Link - activeClassName="button-active" - className="button" - id="favorite-projects" - onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/projects/favorite", - "query": Object { - "size": 1, - }, - } - } - > - my_favorites - </Link> - <IndexLink - activeClassName="button-active" - className="button" - id="all-projects" - onClick={[Function]} - to={ - Object { - "pathname": "/projects", - "query": Object { - "size": 1, - }, - } - } - > - all - </IndexLink> - </div> -</div> -`; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap index 0b9b729d5f0..9c59832d3e6 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap @@ -18,8 +18,6 @@ exports[`renders 1`] = ` > <Link className="button" - onlyActiveOnIndex={false} - style={Object {}} to="/projects/all" > projects.explore_projects diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenu-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenu-test.tsx.snap index 01db864b088..c2eea8f86d0 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenu-test.tsx.snap @@ -19,8 +19,6 @@ exports[`should render correctly: default 1`] = ` > <Link className="display-flex-center" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/projects/create", diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenuItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenuItem-test.tsx.snap index e66677e05de..b4e778a2161 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenuItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenuItem-test.tsx.snap @@ -3,14 +3,10 @@ exports[`should render correctly: bitbucket 1`] = ` <Link className="display-flex-center" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/projects/create", - "query": Object { - "mode": "bitbucket", - }, + "search": "?mode=bitbucket", } } > @@ -27,14 +23,10 @@ exports[`should render correctly: bitbucket 1`] = ` exports[`should render correctly: manual 1`] = ` <Link className="display-flex-center" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/projects/create", - "query": Object { - "mode": "manual", - }, + "search": "?mode=manual", } } > diff --git a/server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx b/server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx index 473850bda5f..e7d95020d66 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx @@ -20,7 +20,7 @@ import classNames from 'classnames'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import PrivacyBadgeContainer from '../../../../components/common/PrivacyBadgeContainer'; import Favorite from '../../../../components/controls/Favorite'; import Tooltip from '../../../../components/controls/Tooltip'; diff --git a/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/__snapshots__/ProjectCard-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/__snapshots__/ProjectCard-test.tsx.snap index a326b9ecfcd..af69b9a3c92 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/__snapshots__/ProjectCard-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/__snapshots__/ProjectCard-test.tsx.snap @@ -37,15 +37,10 @@ exports[`should display applications 1`] = ` title="Foo" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "foo", - }, + "search": "?id=foo", } } > @@ -145,15 +140,10 @@ exports[`should display applications: with project count 1`] = ` title="Foo" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "foo", - }, + "search": "?id=foo", } } > @@ -234,15 +224,10 @@ exports[`should display configure analysis button for logged in user: default 1` title="Foo" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "foo", - }, + "search": "?id=foo", } } > @@ -280,15 +265,10 @@ exports[`should display configure analysis button for logged in user: default 1` </span> <Link className="button spacer-left" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "foo", - }, + "search": "?id=foo", } } > @@ -458,15 +438,10 @@ exports[`should display not analyzed yet 1`] = ` title="Foo" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "foo", - }, + "search": "?id=foo", } } > @@ -532,15 +507,10 @@ exports[`should display the overall measures and quality gate 1`] = ` title="Foo" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "foo", - }, + "search": "?id=foo", } } > diff --git a/server/sonar-web/src/main/js/apps/projects/routes.ts b/server/sonar-web/src/main/js/apps/projects/routes.tsx index 6b996e74891..99a97bf603d 100644 --- a/server/sonar-web/src/main/js/apps/projects/routes.ts +++ b/server/sonar-web/src/main/js/apps/projects/routes.tsx @@ -17,30 +17,27 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { RedirectFunction, RouterState } from 'react-router'; -import { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import React from 'react'; +import { Navigate, Route } from 'react-router-dom'; import { save } from '../../helpers/storage'; +import CreateProjectPage from '../create/project/CreateProjectPage'; +import DefaultPageSelector from './components/DefaultPageSelector'; +import FavoriteProjectsContainer from './components/FavoriteProjectsContainer'; import { PROJECTS_ALL, PROJECTS_DEFAULT_FILTER } from './utils'; -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/DefaultPageSelector')) } - }, - { - path: 'all', - onEnter(_: RouterState, replace: RedirectFunction) { - save(PROJECTS_DEFAULT_FILTER, PROJECTS_ALL); - replace('/projects'); - } - }, - { - path: 'favorite', - component: lazyLoadComponent(() => import('./components/FavoriteProjectsContainer')) - }, - { - path: 'create', - component: lazyLoadComponent(() => import('../create/project/CreateProjectPage')) - } -]; +function PersistNavigate() { + save(PROJECTS_DEFAULT_FILTER, PROJECTS_ALL); + + return <Navigate to="/projects" replace={true} />; +} + +const routes = () => ( + <Route path="projects"> + <Route index={true} element={<DefaultPageSelector />} /> + <Route path="all" element={<PersistNavigate />} /> + <Route path="favorite" element={<FavoriteProjectsContainer />} /> + <Route path="create" element={<CreateProjectPage />} /> + </Route> +); export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx index b416ffa2c1c..622329fc954 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.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 { createProject } from '../../api/components'; import VisibilitySelector from '../../components/common/VisibilitySelector'; import { ResetButtonLink, SubmitButton } from '../../components/controls/buttons'; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx index 7f96d1b14ff..1cb7d26e0ba 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.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 { Project } from '../../api/components'; import PrivacyBadgeContainer from '../../components/common/PrivacyBadgeContainer'; import Checkbox from '../../components/controls/Checkbox'; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx index 70ab3aaae7e..36bbd9969e6 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx @@ -154,5 +154,5 @@ function mockComponents(n: number) { } function renderGlobalBackgroundTasksApp() { - renderAdminApp('admin/background_tasks', routes, {}); + renderAdminApp('admin/projects_management', routes, {}); } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap index d7c8b42fb8f..6e3daf3a9bf 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap @@ -313,15 +313,10 @@ exports[`creates project 4`] = ` values={ Object { "project": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "name", - }, + "search": "?id=name", } } > diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap index eb4b5da27bb..69db1456cc3 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap @@ -18,14 +18,10 @@ exports[`renders 1`] = ` > <Link className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "project", - }, + "search": "?id=project", } } > @@ -114,14 +110,10 @@ exports[`renders: portfolio 1`] = ` > <Link className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/portfolio", - "query": Object { - "id": "project", - }, + "search": "?id=project", } } > @@ -210,14 +202,10 @@ exports[`renders: with lastAnalysisDate 1`] = ` > <Link className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "id": "project", - }, + "search": "?id=project", } } > diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/routes.ts b/server/sonar-web/src/main/js/apps/projectsManagement/routes.ts deleted file mode 100644 index cabc724387d..00000000000 --- a/server/sonar-web/src/main/js/apps/projectsManagement/routes.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./ProjectManagementApp')) } - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/routes.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/routes.tsx new file mode 100644 index 00000000000..f3caf1e7d8c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/routes.tsx @@ -0,0 +1,27 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import ProjectManagementApp from './ProjectManagementApp'; + +export const routes = () => <Route path="projects_management" element={<ProjectManagementApp />} />; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx index c7cb7fcc3fb..2922b7371c1 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { WithRouterProps } from 'react-router'; +import { NavigateFunction, useNavigate, useParams } from 'react-router-dom'; import { fetchQualityGates } from '../../../api/quality-gates'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; @@ -39,13 +39,18 @@ import Details from './Details'; import List from './List'; import ListHeader from './ListHeader'; +interface Props { + id?: string; + navigate: NavigateFunction; +} + interface State { canCreate: boolean; loading: boolean; qualityGates: QualityGate[]; } -class App extends React.PureComponent<Pick<WithRouterProps, 'params' | 'router'>, State> { +class App extends React.PureComponent<Props, State> { mounted = false; state: State = { canCreate: false, loading: true, qualityGates: [] }; @@ -56,8 +61,8 @@ class App extends React.PureComponent<Pick<WithRouterProps, 'params' | 'router'> addSideBarClass(); } - componentDidUpdate(prevProps: WithRouterProps) { - if (prevProps.params.id !== undefined && this.props.params.id === undefined) { + componentDidUpdate(prevProps: Props) { + if (prevProps.id !== undefined && this.props.id === undefined) { this.openDefault(this.state.qualityGates); } } @@ -74,7 +79,7 @@ class App extends React.PureComponent<Pick<WithRouterProps, 'params' | 'router'> if (this.mounted) { this.setState({ canCreate: actions.create, loading: false, qualityGates }); - if (!this.props.params.id) { + if (!this.props.id) { this.openDefault(qualityGates); } } @@ -89,7 +94,7 @@ class App extends React.PureComponent<Pick<WithRouterProps, 'params' | 'router'> openDefault(qualityGates: QualityGate[]) { const defaultQualityGate = qualityGates.find(gate => Boolean(gate.isDefault))!; - this.props.router.replace(getQualityGateUrl(String(defaultQualityGate.id))); + this.props.navigate(getQualityGateUrl(String(defaultQualityGate.id)), { replace: true }); } handleSetDefault = (qualityGate: QualityGate) => { @@ -106,7 +111,7 @@ class App extends React.PureComponent<Pick<WithRouterProps, 'params' | 'router'> }; render() { - const { id } = this.props.params; + const { id } = this.props; const { canCreate, qualityGates } = this.state; const defaultTitle = translate('quality_gates.page'); @@ -148,4 +153,9 @@ class App extends React.PureComponent<Pick<WithRouterProps, 'params' | 'router'> } } -export default App; +export default function AppWrapper() { + const params = useParams(); + const navigate = useNavigate(); + + return <App id={params['id']} navigate={navigate} />; +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx index a76b8214625..d65b53ca2a7 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx @@ -31,7 +31,7 @@ interface Props { onClose: () => void; onCopy: () => Promise<void>; qualityGate: QualityGate; - router: Pick<Router, 'push'>; + router: Router; } interface State { diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx index d8d874bd9de..1dab545b08e 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx @@ -29,7 +29,7 @@ import { getQualityGateUrl } from '../../../helpers/urls'; interface Props { onClose: () => void; onCreate: () => Promise<void>; - router: Pick<Router, 'push'>; + router: Router; } interface State { diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx index d55af0c4c6a..dba0240f8de 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx @@ -29,7 +29,7 @@ import { QualityGate } from '../../../types/types'; interface Props { onDelete: () => Promise<void>; qualityGate: QualityGate; - router: Pick<Router, 'push'>; + router: Router; } export class DeleteQualityGateForm extends React.PureComponent<Props> { diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx index b6d63e089e1..e2fdcd567ab 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/List.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 { NavLink } from 'react-router-dom'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; import { QualityGate } from '../../../types/types'; @@ -32,8 +32,7 @@ export default function List({ qualityGates }: Props) { return ( <div className="list-group" role="menu"> {qualityGates.map(qualityGate => ( - <Link - activeClassName="active" + <NavLink className="list-group-item display-flex-center" role="menuitem" data-id={qualityGate.id} @@ -44,7 +43,7 @@ export default function List({ qualityGates }: Props) { <span className="badge little-spacer-left">{translate('default')}</span> )} {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="little-spacer-left" />} - </Link> + </NavLink> ))} </div> ); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx index fa001b90f96..a376ce36407 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx @@ -359,9 +359,16 @@ describe('The Project section', () => { }); describe('The Permissions section', () => { - it('should not show button to grant permission when user is not admin', () => { + it('should not show button to grant permission when user is not admin', async () => { renderQualityGateApp(); + // await just to make sure we've loaded the page + expect( + await screen.findByRole('menuitem', { + name: `${handler.getDefaultQualityGate().name} default` + }) + ).toBeInTheDocument(); + expect(screen.queryByText('quality_gates.permissions')).not.toBeInTheDocument(); }); it('should show button to grant permission when user is admin', async () => { diff --git a/server/sonar-web/src/main/js/apps/quality-gates/routes.tsx b/server/sonar-web/src/main/js/apps/quality-gates/routes.tsx new file mode 100644 index 00000000000..98f0a4fedd7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/routes.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 React from 'react'; +import { Route } from 'react-router-dom'; +import App from './components/App'; + +const routes = () => ( + <Route path="quality_gates"> + <Route index={true} element={<App />} /> + <Route path="show/:id" element={<App />} /> + </Route> +); + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.tsx index 227dfca2a77..367ee4be80b 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.tsx @@ -20,7 +20,7 @@ import { isSameMinute } from 'date-fns'; import { sortBy } from 'lodash'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import { parseDate } from '../../../helpers/dates'; import { translate } from '../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx index 4e042153c4a..8b4956b25d9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx @@ -18,19 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { WithRouterProps } from 'react-router'; import { getProfileChangelog } from '../../../api/quality-profiles'; -import { withRouter } from '../../../components/hoc/withRouter'; +import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { parseDate, toShortNotSoISOString } from '../../../helpers/dates'; import { translate } from '../../../helpers/l10n'; +import { withQualityProfilesContext } from '../qualityProfilesContext'; import { Profile, ProfileChangelogEvent } from '../types'; import { getProfileChangelogPath } from '../utils'; import Changelog from './Changelog'; import ChangelogEmpty from './ChangelogEmpty'; import ChangelogSearch from './ChangelogSearch'; -interface Props extends Pick<WithRouterProps, 'router' | 'location'> { +interface Props { profile: Profile; + location: Location; + router: Router; } interface State { @@ -166,4 +168,4 @@ export class ChangelogContainer extends React.PureComponent<Props, State> { } } -export default withRouter(ChangelogContainer); +export default withQualityProfilesContext(withRouter(ChangelogContainer)); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx index 0a84d0af275..0aa89d41eec 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx @@ -68,7 +68,7 @@ it('should render action', () => { it('should render rule', () => { const events = [createEvent()]; const changelog = shallow(<Changelog events={events} />); - expect(changelog.find('Link').prop('to')).toHaveProperty('query', { rule_key: 'squid1234' }); + expect(changelog.find('Link').prop('to')).toHaveProperty('search', '?rule_key=squid1234'); }); it('should render ChangesList', () => { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.tsx index fa27be17394..ee9d0ce8e91 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.tsx @@ -18,16 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { withRouter, WithRouterProps } from 'react-router'; import { compareProfiles, CompareResponse } from '../../../api/quality-profiles'; +import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; +import { withQualityProfilesContext } from '../qualityProfilesContext'; import { Profile } from '../types'; import { getProfileComparePath } from '../utils'; import ComparisonForm from './ComparisonForm'; import ComparisonResults from './ComparisonResults'; -interface Props extends WithRouterProps { +interface Props { profile: Profile; profiles: Profile[]; + location: Location; + router: Router; } type State = { loading: boolean } & Partial<CompareResponse>; @@ -123,4 +126,4 @@ class ComparisonContainer extends React.PureComponent<Props, State> { } } -export default withRouter(ComparisonContainer); +export default withQualityProfilesContext(withRouter(ComparisonContainer)); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.tsx index 118fcb95397..4ced3242a18 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.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 { CompareResponse, Profile } from '../../../api/quality-profiles'; import ChevronLeftIcon from '../../../components/icons/ChevronLeftIcon'; import ChevronRightIcon from '../../../components/icons/ChevronRightIcon'; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.tsx index 1e4f2a55f2d..bfab84c2975 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-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 { Profile } from '../../../../api/quality-profiles'; import ComparisonEmpty from '../ComparisonEmpty'; import ComparisonResults from '../ComparisonResults'; @@ -75,10 +75,7 @@ it('should compare', () => { const leftDiffs = output.find('.js-comparison-in-left'); expect(leftDiffs.length).toBe(1); expect(leftDiffs.find(Link).length).toBe(1); - expect(leftDiffs.find(Link).prop('to')).toHaveProperty('query', { - rule_key: 'rule1', - open: 'rule1' - }); + expect(leftDiffs.find(Link).prop('to')).toHaveProperty('search', '?rule_key=rule1&open=rule1'); expect(leftDiffs.find(Link).prop('children')).toContain('rule1'); expect(leftDiffs.find('SeverityIcon').length).toBe(1); expect(leftDiffs.find('SeverityIcon').prop('severity')).toBe('BLOCKER'); @@ -91,7 +88,7 @@ it('should compare', () => { .at(0) .find(Link) .prop('to') - ).toHaveProperty('query', { rule_key: 'rule2', open: 'rule2' }); + ).toHaveProperty('search', '?rule_key=rule2&open=rule2'); expect( rightDiffs .at(0) @@ -113,7 +110,7 @@ it('should compare', () => { .find(Link) .at(0) .prop('to') - ).toHaveProperty('query', { rule_key: 'rule4', open: 'rule4' }); + ).toHaveProperty('search', '?rule_key=rule4&open=rule4'); expect( modifiedDiffs .find(Link) diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx index 62fcbf30d0a..e3c36f91310 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx @@ -44,7 +44,7 @@ import ProfileModalForm from './ProfileModalForm'; interface Props { className?: string; profile: Profile; - router: Pick<Router, 'push' | 'replace'>; + router: Router; updateProfiles: () => Promise<void>; } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx index 94de28abc0c..a2a4be95c85 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx @@ -19,67 +19,53 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { WithRouterProps } from 'react-router'; +import { Outlet, useSearchParams } from 'react-router-dom'; +import { useLocation } from '../../../components/hoc/withRouter'; import ProfileHeader from '../details/ProfileHeader'; -import { Profile } from '../types'; +import { QualityProfilesContextProps, withQualityProfilesContext } from '../qualityProfilesContext'; import ProfileNotFound from './ProfileNotFound'; -interface Props { - children: React.ReactElement<any>; - profiles: Profile[]; - updateProfiles: () => Promise<void>; -} +export function ProfileContainer(props: QualityProfilesContextProps) { + const [_, setSearchParams] = useSearchParams(); + const location = useLocation(); -export default class ProfileContainer extends React.PureComponent<Props & WithRouterProps> { - componentDidMount() { - const { location, profiles, router } = this.props; - if (location.query.key) { - // try to find a quality profile with the given key - // if managed to find one, redirect to a new version - // otherwise do nothing, `render` will show not found page - const profile = profiles.find(profile => profile.key === location.query.key); - if (profile) { - router.replace({ - pathname: location.pathname, - query: { language: profile.language, name: profile.name } - }); - } - } - } + const { key, language, name } = location.query; - render() { - const { profiles, location, ...other } = this.props; - const { key, language, name } = location.query; - - if (key) { - // if there is a `key` parameter, - // then if we managed to find a quality profile with this key - // then we will be redirected in `componentDidMount` - // otherwise show `ProfileNotFound` - const profile = profiles.find(profile => profile.key === location.query.key); - return profile ? null : <ProfileNotFound />; - } + const { profiles } = props; - const profile = profiles.find( - profile => profile.language === language && profile.name === name - ); + // try to find a quality profile with the given key + // if managed to find one, redirect to a new version + // otherwise show not found page + const profileForKey = key && profiles.find(p => p.key === location.query.key); - if (!profile) { - return <ProfileNotFound />; + React.useEffect(() => { + if (profileForKey) { + setSearchParams({ language: profileForKey.language, name: profileForKey.name }); } + }); - const child = React.cloneElement(this.props.children, { - profile, - profiles, - ...other - }); + if (key) { + return profileForKey ? null : <ProfileNotFound />; + } + + const profile = profiles.find(p => p.language === language && p.name === name); - return ( - <div id="quality-profile"> - <Helmet defer={false} title={profile.name} /> - <ProfileHeader profile={profile} updateProfiles={this.props.updateProfiles} /> - {child} - </div> - ); + if (!profile) { + return <ProfileNotFound />; } + + const context: QualityProfilesContextProps = { + profile, + ...props + }; + + return ( + <div id="quality-profile"> + <Helmet defer={false} title={profile.name} /> + <ProfileHeader profile={profile} updateProfiles={props.updateProfiles} /> + <Outlet context={context} /> + </div> + ); } + +export default withQualityProfilesContext(ProfileContainer); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx index 9bfaf8f101a..f9bacdde9b5 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.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 { NavLink } from 'react-router-dom'; import { getProfilePath } from '../utils'; interface Props { @@ -30,8 +30,11 @@ interface Props { export default function ProfileLink({ name, language, children, ...other }: Props) { return ( - <Link activeClassName="link-no-underline" to={getProfilePath(name, language)} {...other}> + <NavLink + className={({ isActive }) => (isActive ? 'link-no-underline' : '')} + to={getProfilePath(name, language)} + {...other}> {children} - </Link> + </NavLink> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.tsx index 2f7462b95ba..8c4e6f2c318 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { IndexLink } from 'react-router'; +import { NavLink } from 'react-router-dom'; import { translate } from '../../../helpers/l10n'; import { PROFILE_PATH } from '../utils'; @@ -26,9 +26,9 @@ export default function ProfileNotFound() { return ( <div className="quality-profile-not-found"> <div className="note spacer-bottom"> - <IndexLink className="text-muted" to={PROFILE_PATH}> + <NavLink end={true} className="text-muted" to={PROFILE_PATH}> {translate('quality_profiles.page')} - </IndexLink> + </NavLink> </div> <div>{translate('quality_profiles.not_found')}</div> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.tsx index 7be27e7b99c..1603ff9e0f5 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.tsx @@ -19,17 +19,18 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; +import { Outlet } from 'react-router-dom'; import { Actions, getExporters, searchQualityProfiles } from '../../../api/quality-profiles'; import withLanguagesContext from '../../../app/components/languages/withLanguagesContext'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import { translate } from '../../../helpers/l10n'; import { Languages } from '../../../types/languages'; +import { QualityProfilesContextProps } from '../qualityProfilesContext'; import '../styles.css'; import { Exporter, Profile } from '../types'; import { sortProfiles } from '../utils'; interface Props { - children: React.ReactElement; languages: Languages; } @@ -87,18 +88,22 @@ export class QualityProfilesApp extends React.PureComponent<Props, State> { }; renderChild() { - if (this.state.loading) { + const { actions, loading, profiles, exporters } = this.state; + + if (loading) { return <i className="spinner" />; } const finalLanguages = Object.values(this.props.languages); - return React.cloneElement(this.props.children, { - actions: this.state.actions || {}, - profiles: this.state.profiles || [], + const context: QualityProfilesContextProps = { + actions: actions || {}, + profiles: profiles || [], languages: finalLanguages, - exporters: this.state.exporters, + exporters: exporters || [], updateProfiles: this.updateProfiles - }); + }; + + return <Outlet context={context} />; } render() { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx index 780be44ce42..6f657c9da7f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx @@ -29,6 +29,7 @@ import { } from '../../../../api/quality-profiles'; import { mockQualityProfile, mockRouter } from '../../../../helpers/testMocks'; import { click, waitAndUpdate } from '../../../../helpers/testUtils'; +import { queryToSearch } from '../../../../helpers/urls'; import { ProfileActionModals } from '../../types'; import { PROFILE_PATH } from '../../utils'; import DeleteProfileForm from '../DeleteProfileForm'; @@ -119,7 +120,7 @@ describe('copy a profile', () => { expect(updateProfiles).toBeCalled(); expect(push).toBeCalledWith({ pathname: '/profiles/show', - query: { language: 'js', name } + search: queryToSearch({ name, language: 'js' }) }); expect(wrapper.find(ProfileModalForm).exists()).toBe(false); }); @@ -182,7 +183,7 @@ describe('extend a profile', () => { expect(push).toBeCalledWith({ pathname: '/profiles/show', - query: { language: 'js', name } + search: queryToSearch({ name, language: 'js' }) }); expect(wrapper.find(ProfileModalForm).exists()).toBe(false); }); @@ -234,7 +235,7 @@ describe('rename a profile', () => { expect(updateProfiles).toBeCalled(); expect(push).toBeCalledWith({ pathname: '/profiles/show', - query: { language: 'js', name } + search: queryToSearch({ name, language: 'js' }) }); expect(wrapper.find(ProfileModalForm).exists()).toBe(false); }); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx index ba092cbfe3e..48886252019 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx @@ -17,57 +17,77 @@ * 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 { Helmet } from 'react-helmet-async'; -import { WithRouterProps } from 'react-router'; -import { mockLocation, mockQualityProfile, mockRouter } from '../../../../helpers/testMocks'; -import ProfileHeader from '../../details/ProfileHeader'; -import ProfileContainer from '../ProfileContainer'; -import ProfileNotFound from '../ProfileNotFound'; +import { HelmetProvider } from 'react-helmet-async'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { mockQualityProfile } from '../../../../helpers/testMocks'; +import { + QualityProfilesContextProps, + withQualityProfilesContext +} from '../../qualityProfilesContext'; +import { Profile } from '../../types'; +import { ProfileContainer } from '../ProfileContainer'; -it('should render ProfileHeader', () => { - const targetProfile = mockQualityProfile({ name: 'fake' }); - const profiles = [targetProfile, mockQualityProfile({ name: 'another' })]; - const updateProfiles = jest.fn(); - const location = mockLocation({ pathname: '', query: { language: 'js', name: 'fake' } }); +it('should render the header and child', () => { + const targetProfile = mockQualityProfile({ name: 'profile1' }); + renderProfileContainer('/?language=js&name=profile1', { + profiles: [mockQualityProfile({ language: 'Java', name: 'profile1' }), targetProfile] + }); - const output = shallowRender({ profiles, updateProfiles, location }); - - const header = output.find(ProfileHeader); - expect(header.length).toBe(1); - expect(header.prop('profile')).toBe(targetProfile); - expect(header.prop('updateProfiles')).toBe(updateProfiles); + expect(screen.getByText('profile1')).toBeInTheDocument(); }); -it('should render ProfileNotFound', () => { - const profiles = [mockQualityProfile({ name: 'fake' }), mockQualityProfile({ name: 'another' })]; - const location = mockLocation({ pathname: '', query: { language: 'js', name: 'random' } }); - - const output = shallowRender({ profiles, location }); +it('should render "not found"', () => { + renderProfileContainer('/?language=java&name=profile2', { + profiles: [mockQualityProfile({ name: 'profile1' }), mockQualityProfile({ name: 'profile2' })] + }); - expect(output.is(ProfileNotFound)).toBe(true); + expect(screen.getByText('quality_profiles.not_found')).toBeInTheDocument(); }); -it('should render Helmet', () => { - const name = 'First Profile'; - const profiles = [mockQualityProfile({ name })]; - const updateProfiles = jest.fn(); - const location = mockLocation({ pathname: '', query: { language: 'js', name } }); +it('should render "not found" for wrong key', () => { + renderProfileContainer('/?key=wrongKey', { + profiles: [mockQualityProfile({ key: 'profileKey' })] + }); + + expect(screen.getByText('quality_profiles.not_found')).toBeInTheDocument(); +}); - const output = shallowRender({ profiles, updateProfiles, location }); +it('should handle getting profile by key', () => { + renderProfileContainer('/?key=profileKey', { + profiles: [mockQualityProfile({ key: 'profileKey', name: 'found the profile' })] + }); - const helmet = output.find(Helmet); - expect(helmet.length).toBe(1); - expect(helmet.prop('title')).toContain(name); + expect(screen.getByText('found the profile')).toBeInTheDocument(); }); -function shallowRender(overrides: Partial<ProfileContainer['props']> = {}) { - const routerProps = { router: mockRouter(), ...overrides } as WithRouterProps; +function Child(props: { profile?: Profile }) { + return <div>{JSON.stringify(props.profile)}</div>; +} + +const WrappedChild = withQualityProfilesContext(Child); - return shallow( - <ProfileContainer profiles={[]} updateProfiles={jest.fn()} {...routerProps} {...overrides}> - <div /> - </ProfileContainer> +function renderProfileContainer(path: string, overrides: Partial<QualityProfilesContextProps>) { + return render( + <HelmetProvider context={{}}> + <MemoryRouter initialEntries={[path]}> + <Routes> + <Route + element={ + <ProfileContainer + actions={{}} + exporters={[]} + languages={[]} + profiles={[]} + updateProfiles={jest.fn()} + {...overrides} + /> + }> + <Route path="*" element={<WrappedChild />} /> + </Route> + </Routes> + </MemoryRouter> + </HelmetProvider> ); } diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/PortfolioPage-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileLink-test.tsx index 1e1166e1807..7c78daea16a 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/PortfolioPage-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileLink-test.tsx @@ -17,25 +17,23 @@ * 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/react'; import * as React from 'react'; -import { mockComponent } from '../../../../helpers/mocks/component'; -import { mockLocation, mockRouter } from '../../../../helpers/testMocks'; -import { PortfolioPage, PortfolioPageProps } from '../PortfolioPage'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; +import ProfileLink from '../ProfileLink'; -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); +it('should be active when on the right path', () => { + renderProfileLink('/profiles/show'); + + expect(screen.getByRole('link')).toHaveClass('link-no-underline'); +}); + +it('should be inactive when on a different path', () => { + renderProfileLink('/toto'); + + expect(screen.getByRole('link')).not.toHaveClass('link-no-underline'); }); -function shallowRender(props?: Partial<PortfolioPageProps>) { - return shallow<PortfolioPageProps>( - <PortfolioPage - component={mockComponent()} - location={mockLocation()} - router={mockRouter()} - routes={[]} - params={{}} - {...props} - /> - ); +function renderProfileLink(path: string) { + return renderComponent(<ProfileLink language="js" name="SonarWay" />, path); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/QualityProfilesApp-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/QualityProfilesApp-test.tsx index f7e68bfae59..b8acc4ca54e 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/QualityProfilesApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/QualityProfilesApp-test.tsx @@ -55,11 +55,13 @@ it('should render child with additional props', () => { wrapper.setState({ loading: false, actions, profiles, exporters }); expect(wrapper.childAt(2).props()).toEqual({ - actions, - profiles, - languages: [language], - exporters, - updateProfiles: wrapper.instance().updateProfiles + context: { + actions, + profiles, + languages: [language], + exporters, + updateProfiles: wrapper.instance().updateProfiles + } }); }); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap index ffb04e929d3..d131a69701d 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap @@ -8,10 +8,7 @@ exports[`renders correctly: all permissions 1`] = ` to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "false", - "qprofile": "key", - }, + "search": "?qprofile=key&activation=false", } } > @@ -29,10 +26,7 @@ exports[`renders correctly: all permissions 1`] = ` to={ Object { "pathname": "/profiles/compare", - "query": Object { - "language": "js", - "name": "name", - }, + "search": "?language=js&name=name", } } > @@ -89,10 +83,7 @@ exports[`renders correctly: copy modal 1`] = ` to={ Object { "pathname": "/profiles/compare", - "query": Object { - "language": "js", - "name": "name", - }, + "search": "?language=js&name=name", } } > @@ -141,10 +132,7 @@ exports[`renders correctly: delete modal 1`] = ` to={ Object { "pathname": "/profiles/compare", - "query": Object { - "language": "js", - "name": "name", - }, + "search": "?language=js&name=name", } } > @@ -184,10 +172,7 @@ exports[`renders correctly: edit only 1`] = ` to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "false", - "qprofile": "key", - }, + "search": "?qprofile=key&activation=false", } } > @@ -205,10 +190,7 @@ exports[`renders correctly: edit only 1`] = ` to={ Object { "pathname": "/profiles/compare", - "query": Object { - "language": "js", - "name": "name", - }, + "search": "?language=js&name=name", } } > @@ -239,10 +221,7 @@ exports[`renders correctly: extend modal 1`] = ` to={ Object { "pathname": "/profiles/compare", - "query": Object { - "language": "js", - "name": "name", - }, + "search": "?language=js&name=name", } } > @@ -291,10 +270,7 @@ exports[`renders correctly: no permissions 1`] = ` to={ Object { "pathname": "/profiles/compare", - "query": Object { - "language": "js", - "name": "name", - }, + "search": "?language=js&name=name", } } > @@ -319,10 +295,7 @@ exports[`renders correctly: rename modal 1`] = ` to={ Object { "pathname": "/profiles/compare", - "query": Object { - "language": "js", - "name": "name", - }, + "search": "?language=js&name=name", } } > @@ -371,10 +344,7 @@ exports[`should not allow to set a profile as the default if the profile has no to={ Object { "pathname": "/profiles/compare", - "query": Object { - "language": "js", - "name": "name", - }, + "search": "?language=js&name=name", } } > diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/QualityProfilesApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/QualityProfilesApp-test.tsx.snap index 891a21afe2d..e362fd0a400 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/QualityProfilesApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/QualityProfilesApp-test.tsx.snap @@ -13,12 +13,16 @@ exports[`should render correctly: full 1`] = ` prioritizeSeoTags={false} title="quality_profiles.page" /> - <div - actions={Object {}} - exporters={Array []} - languages={Array []} - profiles={Array []} - updateProfiles={[Function]} + <Outlet + context={ + Object { + "actions": Object {}, + "exporters": Array [], + "languages": Array [], + "profiles": Array [], + "updateProfiles": [Function], + } + } /> </div> `; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx index 534ab5c4480..91f9445f768 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { Alert } from '../../../components/ui/Alert'; import { translate } from '../../../helpers/l10n'; +import { withQualityProfilesContext } from '../qualityProfilesContext'; import { Exporter, Profile } from '../types'; import ProfileExporters from './ProfileExporters'; import ProfileInheritance from './ProfileInheritance'; @@ -34,7 +35,7 @@ export interface ProfileDetailsProps { updateProfiles: () => Promise<void>; } -export default function ProfileDetails(props: ProfileDetailsProps) { +export function ProfileDetails(props: ProfileDetailsProps) { const { profile } = props; return ( <div> @@ -69,3 +70,5 @@ export default function ProfileDetails(props: ProfileDetailsProps) { </div> ); } + +export default withQualityProfilesContext(ProfileDetails); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx index 61a2caccda8..1d4b6ed5d4b 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { IndexLink, Link } from 'react-router'; +import { Link, NavLink } from 'react-router-dom'; import Tooltip from '../../../components/controls/Tooltip'; import DateFromNow from '../../../components/intl/DateFromNow'; import { translate } from '../../../helpers/l10n'; @@ -40,9 +40,9 @@ export default class ProfileHeader extends React.PureComponent<Props> { return ( <header className="page-header quality-profile-header"> <div className="note spacer-bottom"> - <IndexLink className="text-muted" to={PROFILE_PATH}> + <NavLink end={true} className="text-muted" to={PROFILE_PATH}> {translate('quality_profiles.page')} - </IndexLink> + </NavLink> {' / '} <Link className="text-muted" to={getProfilesForLanguagePath(profile.language)}> {profile.languageName} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx index 087bfb2c45d..0dcc970ab01 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.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 { getProfileProjects } from '../../../api/quality-profiles'; import { Button } from '../../../components/controls/buttons'; import ListFooter from '../../../components/controls/ListFooter'; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx index da78df791fc..cd5113bf17c 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx @@ -19,7 +19,7 @@ */ import { keyBy } from 'lodash'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { getQualityProfile } from '../../../api/quality-profiles'; import { searchRules, takeFacet } from '../../../api/rules'; import { Button } from '../../../components/controls/buttons'; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx index 97ab2bba0b4..bde0c6a01f4 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.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 HelpTooltip from '../../../components/controls/HelpTooltip'; import { translate } from '../../../helpers/l10n'; import { getDeprecatedActiveRulesUrl } from '../../../helpers/urls'; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowOfType.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowOfType.tsx index 668efb5b377..2243f339ffe 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowOfType.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowOfType.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 IssueTypeIcon from '../../../components/icons/IssueTypeIcon'; import { translate } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowTotal.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowTotal.tsx index 488c6aa85fa..45bd13b3f6c 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowTotal.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowTotal.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 { formatMeasure } from '../../../helpers/measures'; import { getRulesUrl } from '../../../helpers/urls'; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx index dd2eab3bd6d..94831180faa 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.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 HelpTooltip from '../../../components/controls/HelpTooltip'; import { translate } from '../../../helpers/l10n'; import { getRulesUrl } from '../../../helpers/urls'; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx index 870e7d13da1..6d6b9d8eebf 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx @@ -20,7 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockQualityProfile } from '../../../../helpers/testMocks'; -import ProfileDetails, { ProfileDetailsProps } from '../ProfileDetails'; +import { ProfileDetails, ProfileDetailsProps } from '../ProfileDetails'; it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot('default'); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileHeader-test.tsx.snap index 41bb8222774..db6386f4230 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileHeader-test.tsx.snap @@ -7,23 +7,20 @@ exports[`should render correctly 1`] = ` <div className="note spacer-bottom" > - <IndexLink + <NavLink className="text-muted" + end={true} to="/profiles" > quality_profiles.page - </IndexLink> + </NavLink> / <Link className="text-muted" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/profiles", - "query": Object { - "language": "js", - }, + "search": "?language=js", } } > @@ -71,15 +68,10 @@ exports[`should render correctly 1`] = ` <li> <Link className="button" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/profiles/changelog", - "query": Object { - "language": "js", - "name": "name", - }, + "search": "?language=js&name=name", } } > @@ -120,23 +112,20 @@ exports[`should render correctly: for default profile 1`] = ` <div className="note spacer-bottom" > - <IndexLink + <NavLink className="text-muted" + end={true} to="/profiles" > quality_profiles.page - </IndexLink> + </NavLink> / <Link className="text-muted" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/profiles", - "query": Object { - "language": "js", - }, + "search": "?language=js", } } > @@ -193,15 +182,10 @@ exports[`should render correctly: for default profile 1`] = ` <li> <Link className="button" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/profiles/changelog", - "query": Object { - "language": "js", - "name": "name", - }, + "search": "?language=js&name=name", } } > diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap index d69b1c67fa8..f7310b99075 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap @@ -37,15 +37,10 @@ exports[`should render correctly: default 1`] = ` > <Link className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "org.sonarsource.xml:xml", - }, + "search": "?id=org.sonarsource.xml%3Axml", } } > @@ -142,15 +137,10 @@ exports[`should render correctly: no active rules, but associated projects 1`] = > <Link className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "org.sonarsource.xml:xml", - }, + "search": "?id=org.sonarsource.xml%3Axml", } } > @@ -232,15 +222,10 @@ exports[`should render correctly: no rights 1`] = ` > <Link className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "org.sonarsource.xml:xml", - }, + "search": "?id=org.sonarsource.xml%3Axml", } } > diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap index c2af3488e98..2bfdc44d131 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap @@ -74,15 +74,10 @@ exports[`should render the quality profiles rules with sonarway comparison 1`] = exports[`should show a button to activate more rules for admins 1`] = ` <Link className="button js-activate-rules" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "false", - "qprofile": "key", - }, + "search": "?qprofile=key&activation=false", } } > diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesDeprecatedWarning-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesDeprecatedWarning-test.tsx.snap index 4a03b4e8a41..9b9d0902b5f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesDeprecatedWarning-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesDeprecatedWarning-test.tsx.snap @@ -19,16 +19,10 @@ exports[`should render correctly 1`] = ` </span> <Link className="pull-right" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "bar", - "statuses": "DEPRECATED", - }, + "search": "?qprofile=bar&activation=true&statuses=DEPRECATED", } } > diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowOfType-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowOfType-test.tsx.snap index c9bbffe4449..74a4d1abcf2 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowOfType-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowOfType-test.tsx.snap @@ -15,16 +15,10 @@ exports[`should render correctly 1`] = ` className="thin nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "bar", - "types": "BUG", - }, + "search": "?qprofile=bar&activation=true&types=BUG", } } > @@ -36,16 +30,10 @@ exports[`should render correctly 1`] = ` > <Link className="small text-muted" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "false", - "qprofile": "bar", - "types": "BUG", - }, + "search": "?qprofile=bar&activation=false&types=BUG", } } > @@ -70,16 +58,10 @@ exports[`should render correctly if there is 0 rules 1`] = ` className="thin nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "bar", - "types": "VULNERABILITY", - }, + "search": "?qprofile=bar&activation=true&types=VULNERABILITY", } } > @@ -113,16 +95,10 @@ exports[`should render correctly if there is missing data 1`] = ` className="thin nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "bar", - "types": "VULNERABILITY", - }, + "search": "?qprofile=bar&activation=true&types=VULNERABILITY", } } > diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowTotal-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowTotal-test.tsx.snap index 226dd57670d..5e18680e3b4 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowTotal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowTotal-test.tsx.snap @@ -11,15 +11,10 @@ exports[`should render correctly 1`] = ` className="thin nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "bar", - }, + "search": "?qprofile=bar&activation=true", } } > @@ -33,15 +28,10 @@ exports[`should render correctly 1`] = ` > <Link className="small text-muted" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "false", - "qprofile": "bar", - }, + "search": "?qprofile=bar&activation=false", } } > @@ -64,15 +54,10 @@ exports[`should render correctly if there is 0 rules 1`] = ` className="thin nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "bar", - }, + "search": "?qprofile=bar&activation=true", } } > @@ -104,15 +89,10 @@ exports[`should render correctly if there is missing data 1`] = ` className="thin nowrap text-right" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "bar", - }, + "search": "?qprofile=bar&activation=true", } } > diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesSonarWayComparison-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesSonarWayComparison-test.tsx.snap index 6567a60b75e..83633732e7b 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesSonarWayComparison-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesSonarWayComparison-test.tsx.snap @@ -20,17 +20,10 @@ exports[`should render correctly 1`] = ` <Link className="pull-right" data-test="rules" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "false", - "compareToProfile": "baz", - "languages": "Java", - "qprofile": "bar", - }, + "search": "?qprofile=bar&activation=false&compareToProfile=baz&languages=Java", } } > diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.tsx index 8013f536b3f..18a7b1f33e3 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.tsx @@ -19,7 +19,7 @@ */ import { sortBy } from 'lodash'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getDeprecatedActiveRulesUrl } from '../../../helpers/urls'; import ProfileLink from '../components/ProfileLink'; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx index 64bfa6654cf..155543ad1b0 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx @@ -19,7 +19,7 @@ */ import { sortBy } from 'lodash'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { searchRules } from '../../../api/rules'; import { toShortNotSoISOString } from '../../../helpers/dates'; import { translate, translateWithParameters } from '../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.tsx index 13acc9e8a08..793033e8e6a 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.tsx @@ -18,32 +18,28 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Actions } from '../../../api/quality-profiles'; -import { Location } from '../../../components/hoc/withRouter'; -import { Profile } from '../types'; +import { useOutletContext, useSearchParams } from 'react-router-dom'; +import { QualityProfilesContextProps } from '../qualityProfilesContext'; import Evolution from './Evolution'; import PageHeader from './PageHeader'; import ProfilesList from './ProfilesList'; -interface Props { - actions: Actions; - languages: Array<{ key: string; name: string }>; - location: Location; - profiles: Profile[]; - updateProfiles: () => Promise<void>; -} +export default function HomeContainer() { + const context = useOutletContext<QualityProfilesContextProps>(); + const [searchParams] = useSearchParams(); + + const selectedLanguage = searchParams.get('language') ?? undefined; -export default function HomeContainer(props: Props) { return ( <div> - <PageHeader {...props} /> + <PageHeader {...context} /> <div className="page-with-sidebar"> <div className="page-main"> - <ProfilesList {...props} /> + <ProfilesList {...context} language={selectedLanguage} /> </div> <div className="page-sidebar"> - <Evolution {...props} /> + <Evolution {...context} /> </div> </div> </div> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx index 87a490fddb5..006b70a3490 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.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 { Actions } from '../../../api/quality-profiles'; import { Button } from '../../../components/controls/buttons'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; @@ -34,7 +34,7 @@ interface Props { languages: Array<{ key: string; name: string }>; location: Location; profiles: Profile[]; - router: Pick<Router, 'push'>; + router: Router; updateProfiles: () => Promise<void>; } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx index 02fa9f9aeac..e749dcc9e12 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx @@ -17,7 +17,6 @@ * 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 { groupBy, pick, sortBy } from 'lodash'; import * as React from 'react'; import HelpTooltip from '../../../components/controls/HelpTooltip'; @@ -30,8 +29,8 @@ import ProfilesListHeader from './ProfilesListHeader'; import ProfilesListRow from './ProfilesListRow'; interface Props { + language?: string; languages: Language[]; - location: Pick<Location, 'query'>; profiles: Profile[]; updateProfiles: () => Promise<void>; } @@ -94,8 +93,7 @@ export default class ProfilesList extends React.PureComponent<Props> { }; render() { - const { profiles, languages } = this.props; - const { language } = this.props.location.query; + const { profiles, languages, language } = this.props; const profilesIndex: Dict<Profile[]> = groupBy<Profile>(profiles, profile => profile.language); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx index b605ba81f5b..fe2bf6d96ff 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx @@ -26,7 +26,7 @@ import { getProfilesForLanguagePath, PROFILE_PATH } from '../utils'; interface Props { currentFilter?: string; languages: Array<{ key: string; name: string }>; - router: Pick<Router, 'replace'>; + router: Router; } export class ProfilesListHeader extends React.PureComponent<Props> { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx index 6bf8c8a38ec..72408e1905e 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.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 DateFromNow from '../../../components/intl/DateFromNow'; import { translate } from '../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesList-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesList-test.tsx index f835ff48328..3061318eac0 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesList-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesList-test.tsx @@ -19,26 +19,21 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { mockLanguage, mockLocation, mockQualityProfile } from '../../../../helpers/testMocks'; +import { mockLanguage, mockQualityProfile } from '../../../../helpers/testMocks'; import ProfilesList from '../ProfilesList'; it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); - expect( - shallowRender({ location: mockLocation({ query: { language: 'css' } }) }) - ).toMatchSnapshot(); + expect(shallowRender({ language: 'css' })).toMatchSnapshot(); - expect( - shallowRender({ location: mockLocation({ query: { language: 'unknown' } }) }) - ).toMatchSnapshot(); + expect(shallowRender({ language: 'unknown' })).toMatchSnapshot(); }); function shallowRender(props: Partial<ProfilesList['props']> = {}) { return shallow( <ProfilesList languages={[mockLanguage(), mockLanguage({ key: 'js', name: 'JS' })]} - location={mockLocation()} profiles={[ mockQualityProfile(), mockQualityProfile({ language: 'css', languageName: 'CSS' }) diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/EvolutionDeprecated-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/EvolutionDeprecated-test.tsx.snap index 62a80256ecb..292172b31e5 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/EvolutionDeprecated-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/EvolutionDeprecated-test.tsx.snap @@ -39,16 +39,10 @@ exports[`should render correctly 1`] = ` , <Link className="text-muted" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "qp-5", - "statuses": "DEPRECATED", - }, + "search": "?qprofile=qp-5&activation=true&statuses=DEPRECATED", } } > @@ -92,16 +86,10 @@ exports[`should render correctly 1`] = ` , <Link className="text-muted" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "qp-4", - "statuses": "DEPRECATED", - }, + "search": "?qprofile=qp-4&activation=true&statuses=DEPRECATED", } } > @@ -138,16 +126,10 @@ exports[`should render correctly 1`] = ` , <Link className="text-muted" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "qp-2", - "statuses": "DEPRECATED", - }, + "search": "?qprofile=qp-2&activation=true&statuses=DEPRECATED", } } > @@ -177,16 +159,10 @@ exports[`should render correctly 1`] = ` , <Link className="text-muted" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "qp-3", - "statuses": "DEPRECATED", - }, + "search": "?qprofile=qp-3&activation=true&statuses=DEPRECATED", } } > diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/PageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/PageHeader-test.tsx.snap index d880cd231c8..4cabe302e26 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/PageHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/PageHeader-test.tsx.snap @@ -17,8 +17,6 @@ exports[`should render correctly 1`] = ` quality_profiles.intro2 <Link className="spacer-left" - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to={ Object { @@ -67,8 +65,6 @@ exports[`should render correctly 2`] = ` quality_profiles.intro2 <Link className="spacer-left" - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to={ Object { @@ -123,8 +119,6 @@ exports[`should render correctly 3`] = ` quality_profiles.intro2 <Link className="spacer-left" - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to={ Object { @@ -173,8 +167,6 @@ exports[`should show a create form 1`] = ` quality_profiles.intro2 <Link className="spacer-left" - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to={ Object { @@ -196,7 +188,6 @@ exports[`should show a create form 1`] = ` } location={ Object { - "action": "PUSH", "hash": "", "key": "key", "pathname": "/path", @@ -264,8 +255,6 @@ exports[`should show a restore form 1`] = ` quality_profiles.intro2 <Link className="spacer-left" - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to={ Object { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesListRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesListRow-test.tsx.snap index a19c915a89c..85d7ef07a13 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesListRow-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesListRow-test.tsx.snap @@ -49,16 +49,10 @@ exports[`should render correctly: built-in profile 1`] = ` > <Link className="badge badge-error" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "key", - "statuses": "DEPRECATED", - }, + "search": "?qprofile=key&activation=true&statuses=DEPRECATED", } } > @@ -67,15 +61,10 @@ exports[`should render correctly: built-in profile 1`] = ` </Tooltip> </span> <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "key", - }, + "search": "?qprofile=key&activation=true", } } > @@ -158,15 +147,10 @@ exports[`should render correctly: default 1`] = ` > <div> <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "key", - }, + "search": "?qprofile=key&activation=true", } } > @@ -262,16 +246,10 @@ exports[`should render correctly: default profile 1`] = ` > <Link className="badge badge-error" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "key", - "statuses": "DEPRECATED", - }, + "search": "?qprofile=key&activation=true&statuses=DEPRECATED", } } > @@ -280,15 +258,10 @@ exports[`should render correctly: default profile 1`] = ` </Tooltip> </span> <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "key", - }, + "search": "?qprofile=key&activation=true", } } > @@ -378,16 +351,10 @@ exports[`should render correctly: with deprecated rules 1`] = ` > <Link className="badge badge-error" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "key", - "statuses": "DEPRECATED", - }, + "search": "?qprofile=key&activation=true&statuses=DEPRECATED", } } > @@ -396,15 +363,10 @@ exports[`should render correctly: with deprecated rules 1`] = ` </Tooltip> </span> <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/coding_rules", - "query": Object { - "activation": "true", - "qprofile": "key", - }, + "search": "?qprofile=key&activation=true", } } > diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/qualityProfilesContext.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/qualityProfilesContext.tsx new file mode 100644 index 00000000000..2aca327c296 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/qualityProfilesContext.tsx @@ -0,0 +1,54 @@ +/* + * 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 React from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { Actions } from '../../api/quality-profiles'; +import { Exporter, Profile } from '../../apps/quality-profiles/types'; +import { getWrappedDisplayName } from '../../components/hoc/utils'; +import { Language } from '../../types/languages'; + +export interface QualityProfilesContextProps { + actions: Actions; + exporters: Exporter[]; + languages: Language[]; + profile?: Profile; + profiles: Profile[]; + updateProfiles: () => Promise<void>; +} + +export function withQualityProfilesContext<P extends Partial<QualityProfilesContextProps>>( + WrappedComponent: React.ComponentType<P> +): React.ComponentType<Omit<P, keyof QualityProfilesContextProps>> { + function ComponentWithQualityProfilesProps(props: P) { + const context = useOutletContext<QualityProfilesContextProps>(); + return <WrappedComponent {...props} {...context} />; + } + + (ComponentWithQualityProfilesProps as React.FC<P>).displayName = getWrappedDisplayName( + WrappedComponent, + 'withQualityProfilesContext' + ); + + return ComponentWithQualityProfilesProps; +} + +export function useQualityProfilesContext() { + return useOutletContext<QualityProfilesContextProps>(); +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/routes.ts b/server/sonar-web/src/main/js/apps/quality-profiles/routes.ts deleted file mode 100644 index 6e14d1a75af..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/routes.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - component: lazyLoadComponent(() => import('./components/QualityProfilesApp')), - indexRoute: { component: lazyLoadComponent(() => import('./home/HomeContainer')) }, - childRoutes: [ - { - component: lazyLoadComponent(() => import('./components/ProfileContainer')), - childRoutes: [ - { - path: 'show', - component: lazyLoadComponent(() => import('./details/ProfileDetails')) - }, - { - path: 'changelog', - component: lazyLoadComponent(() => import('./changelog/ChangelogContainer')) - }, - { - path: 'compare', - component: lazyLoadComponent(() => import('./compare/ComparisonContainer')) - } - ] - } - ] - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/routes.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/routes.tsx new file mode 100644 index 00000000000..71b57cbee0f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/routes.tsx @@ -0,0 +1,40 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import ChangelogContainer from './changelog/ChangelogContainer'; +import ComparisonContainer from './compare/ComparisonContainer'; +import ProfileContainer from './components/ProfileContainer'; +import QualityProfilesApp from './components/QualityProfilesApp'; +import ProfileDetails from './details/ProfileDetails'; +import HomeContainer from './home/HomeContainer'; + +const routes = () => ( + <Route path="profiles" element={<QualityProfilesApp />}> + <Route index={true} element={<HomeContainer />} /> + <Route element={<ProfileContainer />}> + <Route path="show" element={<ProfileDetails />} /> + <Route path="changelog" element={<ChangelogContainer />} /> + <Route path="compare" element={<ComparisonContainer />} /> + </Route> + </Route> +); + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts b/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts index 7e3040f20fd..5bed7860f04 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts +++ b/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts @@ -21,6 +21,7 @@ import { differenceInYears } from 'date-fns'; import { sortBy } from 'lodash'; import { Profile as BaseProfile } from '../../api/quality-profiles'; import { isValidDate, parseDate } from '../../helpers/dates'; +import { queryToSearch } from '../../helpers/urls'; import { Profile } from './types'; export function sortProfiles(profiles: BaseProfile[]): Profile[] { @@ -66,12 +67,12 @@ export const PROFILE_PATH = '/profiles'; export const getProfilesForLanguagePath = (language: string) => ({ pathname: PROFILE_PATH, - query: { language } + search: queryToSearch({ language }) }); export const getProfilePath = (name: string, language: string) => ({ pathname: `${PROFILE_PATH}/show`, - query: { name, language } + search: queryToSearch({ name, language }) }); export const getProfileComparePath = (name: string, language: string, withKey?: string) => { @@ -81,7 +82,7 @@ export const getProfileComparePath = (name: string, language: string, withKey?: } return { pathname: `${PROFILE_PATH}/compare`, - query + search: queryToSearch(query) }; }; @@ -101,6 +102,6 @@ export const getProfileChangelogPath = ( } return { pathname: `${PROFILE_PATH}/changelog`, - query + search: queryToSearch(query) }; }; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx index fce568a6aed..83b805bd143 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx @@ -17,14 +17,14 @@ * 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 { flatMap, range } from 'lodash'; import * as React from 'react'; import { getMeasures } from '../../api/measures'; import { getSecurityHotspotList, getSecurityHotspots } from '../../api/security-hotspots'; import withBranchStatusActions from '../../app/components/branch-status/withBranchStatusActions'; +import withComponentContext from '../../app/components/componentContext/withComponentContext'; import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; -import { Router } from '../../components/hoc/withRouter'; +import { Location, Router, withRouter } from '../../components/hoc/withRouter'; import { getLeakValue } from '../../components/measure/utils'; import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../helpers/branch-like'; import { KeyboardKeys } from '../../helpers/keycodes'; @@ -550,4 +550,6 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { } } -export default withCurrentUserContext(withBranchStatusActions(SecurityHotspotsApp)); +export default withRouter( + withComponentContext(withCurrentUserContext(withBranchStatusActions(SecurityHotspotsApp))) +); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx index a4c025191a9..92c6afdd267 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.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'; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx index 6e3aa2bfd65..7dd943896f6 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx @@ -19,7 +19,7 @@ */ import React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { translate } from '../../../helpers/l10n'; import { getRuleUrl } from '../../../helpers/urls'; import { Hotspot, HotspotStatusOption } from '../../../types/security-hotspots'; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap index 9ea274c206a..5ca002b074f 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap @@ -22,8 +22,6 @@ exports[`should render correctly 1`] = ` </div> <Link className="big-spacer-top" - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to={ Object { @@ -58,8 +56,6 @@ exports[`should render correctly: file 1`] = ` </div> <Link className="big-spacer-top" - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to={ Object { diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotHeader-test.tsx.snap index eb76f5685b9..47b62f17a02 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotHeader-test.tsx.snap @@ -22,16 +22,11 @@ exports[`should render correctly 1`] = ` </span> <Link className="small" - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to={ Object { "pathname": "/coding_rules", - "query": Object { - "open": "squid:S2077", - "rule_key": "squid:S2077", - }, + "search": "?open=squid%3AS2077&rule_key=squid%3AS2077", } } > diff --git a/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx b/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx index bb7fcf719c3..460ce54cd4a 100644 --- a/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx +++ b/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx @@ -17,10 +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 { Location } from 'history'; import * as React from 'react'; import { logIn } from '../../../api/auth'; import { getIdentityProviders } from '../../../api/users'; +import { Location, withRouter } from '../../../components/hoc/withRouter'; import { addGlobalErrorMessage } from '../../../helpers/globalMessages'; import { translate } from '../../../helpers/l10n'; import { getReturnUrl } from '../../../helpers/urls'; @@ -28,9 +28,7 @@ import { IdentityProvider } from '../../../types/types'; import Login from './Login'; interface Props { - location: Pick<Location, 'hash' | 'pathname' | 'query'> & { - query: { advanced?: string; return_to?: string }; - }; + location: Location; } interface State { identityProviders?: IdentityProvider[]; @@ -90,4 +88,4 @@ export class LoginContainer extends React.PureComponent<Props, State> { } } -export default LoginContainer; +export default withRouter(LoginContainer); diff --git a/server/sonar-web/src/main/js/apps/sessions/routes.ts b/server/sonar-web/src/main/js/apps/sessions/routes.ts deleted file mode 100644 index a465a04c533..00000000000 --- a/server/sonar-web/src/main/js/apps/sessions/routes.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - path: 'new', - component: lazyLoadComponent(() => import('./components/LoginContainer')) - }, - { - path: 'logout', - component: lazyLoadComponent(() => import('./components/Logout')) - }, - { - path: 'unauthorized', - component: lazyLoadComponent(() => import('./components/Unauthorized')) - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/sessions/routes.tsx b/server/sonar-web/src/main/js/apps/sessions/routes.tsx new file mode 100644 index 00000000000..2d82a8a4e0c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/sessions/routes.tsx @@ -0,0 +1,35 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import SimpleSessionsContainer from '../../app/components/SimpleSessionsContainer'; +import LoginContainer from './components/LoginContainer'; +import Logout from './components/Logout'; +import Unauthorized from './components/Unauthorized'; + +const routes = () => ( + <Route path="sessions" element={<SimpleSessionsContainer />}> + <Route path="new" element={<LoginContainer />} /> + <Route path="logout" element={<Logout />} /> + <Route path="unauthorized" element={<Unauthorized />} /> + </Route> +); + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts index 8314209db8c..417758c8932 100644 --- a/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts @@ -92,7 +92,7 @@ describe('buildSettingLink', () => { [ mockDefinition({ key: 'anykey' }), undefined, - { hash: '#anykey', pathname: '/admin/settings', query: { category: 'foo category' } } + { hash: '#anykey', pathname: '/admin/settings', search: '?category=foo+category' } ], [ mockDefinition({ key: 'sonar.auth.gitlab.name' }), @@ -100,7 +100,7 @@ describe('buildSettingLink', () => { { hash: '#sonar.auth.gitlab.name', pathname: '/admin/settings', - query: { alm: 'gitlab', category: 'foo category' } + search: '?category=foo+category&alm=gitlab' } ], [ @@ -109,7 +109,7 @@ describe('buildSettingLink', () => { { hash: '#sonar.auth.github.token', pathname: '/admin/settings', - query: { alm: 'github', category: 'foo category' } + search: '?category=foo+category&alm=github' } ], [ @@ -118,7 +118,7 @@ describe('buildSettingLink', () => { { hash: '#sonar.almintegration.azure', pathname: '/admin/settings', - query: { alm: 'azure', category: 'foo category' } + search: '?category=foo+category&alm=azure' } ], [ @@ -127,7 +127,7 @@ describe('buildSettingLink', () => { { hash: '#defKey', pathname: '/project/settings', - query: { id: 'componentKey', category: 'foo category' } + search: '?id=componentKey&category=foo+category' } ] ])('should work as expected', (definition, component, expectedUrl) => { diff --git a/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx b/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx index 91c8ced89c1..d0b0c01a8a7 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx @@ -20,7 +20,7 @@ import classNames from 'classnames'; import { sortBy } from 'lodash'; import * as React from 'react'; -import { IndexLink } from 'react-router'; +import { NavLink } from 'react-router-dom'; import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import { getGlobalSettingsUrl, getProjectSettingsUrl } from '../../../helpers/urls'; import { AppState } from '../../../types/appstate'; @@ -65,10 +65,13 @@ export function CategoriesList(props: CategoriesListProps) { const category = c.key !== defaultCategory ? c.key.toLowerCase() : undefined; return ( <li key={c.key}> - <IndexLink - className={classNames({ - active: c.key.toLowerCase() === selectedCategory.toLowerCase() - })} + <NavLink + end={true} + className={_ => + classNames({ + active: c.key.toLowerCase() === selectedCategory.toLowerCase() + }) + } title={c.name} to={ component @@ -76,7 +79,7 @@ export function CategoriesList(props: CategoriesListProps) { : getGlobalSettingsUrl(category) }> {c.name} - </IndexLink> + </NavLink> </li> ); })} diff --git a/server/sonar-web/src/main/js/apps/settings/components/AnalysisScope.tsx b/server/sonar-web/src/main/js/apps/settings/components/AnalysisScope.tsx index eb196609ea8..0fc1f42e352 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/AnalysisScope.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/AnalysisScope.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 { AdditionalCategoryComponentProps } from './AdditionalCategories'; import CategoryDefinitionsList from './CategoryDefinitionsList'; diff --git a/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx b/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx index 197bafeba93..22068cf074b 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.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 { getNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod'; import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; diff --git a/server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx b/server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx index 9018e7d9264..4315e5f760e 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { getDefinitions } from '../../../api/settings'; +import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import { addSideBarClass, addWhitePageClass, @@ -39,7 +40,7 @@ interface State { loading: boolean; } -export default class SettingsApp extends React.PureComponent<Props, State> { +export class SettingsApp extends React.PureComponent<Props, State> { mounted = false; state: State = { definitions: [], loading: true }; @@ -79,3 +80,5 @@ export default class SettingsApp extends React.PureComponent<Props, State> { return <SettingsAppRenderer component={component} {...this.state} />; } } + +export default withComponentContext(SettingsApp); diff --git a/server/sonar-web/src/main/js/apps/settings/components/SettingsSearch.tsx b/server/sonar-web/src/main/js/apps/settings/components/SettingsSearch.tsx index b78a17052e3..21af1e9ac28 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/SettingsSearch.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/SettingsSearch.tsx @@ -20,8 +20,7 @@ import { debounce, keyBy } from 'lodash'; import lunr, { LunrIndex } from 'lunr'; import * as React from 'react'; -import { InjectedRouter } from 'react-router'; -import { withRouter } from '../../../components/hoc/withRouter'; +import { Router, withRouter } from '../../../components/hoc/withRouter'; import { KeyboardKeys } from '../../../helpers/keycodes'; import { ExtendedSettingDefinition } from '../../../types/settings'; import { Component, Dict } from '../../../types/types'; @@ -36,7 +35,7 @@ interface Props { className?: string; component?: Component; definitions: ExtendedSettingDefinition[]; - router: InjectedRouter; + router: Router; } interface State { diff --git a/server/sonar-web/src/main/js/apps/settings/components/SettingsSearchRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/SettingsSearchRenderer.tsx index 29e268b6e63..fa9dac8fb38 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/SettingsSearchRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/SettingsSearchRenderer.tsx @@ -19,7 +19,7 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { DropdownOverlay } from '../../../components/controls/Dropdown'; import OutsideClickHandler from '../../../components/controls/OutsideClickHandler'; import SearchBox from '../../../components/controls/SearchBox'; diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/AllCategoriesList-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/AllCategoriesList-test.tsx index 3d7a1401872..94ff4c16fcc 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/AllCategoriesList-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/AllCategoriesList-test.tsx @@ -17,10 +17,11 @@ * 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 { mockComponent } from '../../../../helpers/mocks/component'; import { mockAppState } from '../../../../helpers/testMocks'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import { AdditionalCategory } from '../AdditionalCategories'; import { CategoriesList, CategoriesListProps } from '../AllCategoriesList'; @@ -63,16 +64,34 @@ jest.mock('../AdditionalCategories', () => ({ })); it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot('global mode'); - expect(shallowRender({ selectedCategory: 'CAT_2' })).toMatchSnapshot('selected category'); - expect(shallowRender({ component: mockComponent() })).toMatchSnapshot('project mode'); - expect(shallowRender({ appState: mockAppState({ branchesEnabled: false }) })).toMatchSnapshot( - 'branches disabled' - ); + renderCategoriesList({ selectedCategory: 'CAT_2' }); + + expect(screen.getByText('CAT_1_NAME')).toBeInTheDocument(); + expect(screen.getByText('CAT_2_NAME')).toBeInTheDocument(); + expect(screen.queryByText('CAT_3_NAME')).not.toBeInTheDocument(); + expect(screen.queryByText('CAT_4_NAME')).not.toBeInTheDocument(); + expect(screen.getByText('CAT_2_NAME').className).toBe('active'); +}); + +it('should correctly for project', () => { + renderCategoriesList({ component: mockComponent() }); + + expect(screen.getByText('CAT_1_NAME')).toBeInTheDocument(); + expect(screen.queryByText('CAT_2_NAME')).not.toBeInTheDocument(); + expect(screen.getByText('CAT_3_NAME')).toBeInTheDocument(); + expect(screen.queryByText('CAT_4_NAME')).not.toBeInTheDocument(); +}); + +it('should render correctly when branches are disabled', () => { + renderCategoriesList({ appState: mockAppState({ branchesEnabled: false }) }); + + expect(screen.queryByText('CAT_1_NAME')).not.toBeInTheDocument(); + expect(screen.getByText('CAT_2_NAME')).toBeInTheDocument(); + expect(screen.queryByText('CAT_4_NAME')).not.toBeInTheDocument(); }); -function shallowRender(props?: Partial<CategoriesListProps>) { - return shallow<CategoriesListProps>( +function renderCategoriesList(props?: Partial<CategoriesListProps>) { + return renderComponent( <CategoriesList appState={mockAppState({ branchesEnabled: true })} categories={['general']} diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-test.tsx index 72bb8ce2bef..005d1e1a961 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-test.tsx @@ -28,7 +28,7 @@ import { removeWhitePageClass } from '../../../../helpers/pages'; import { waitAndUpdate } from '../../../../helpers/testUtils'; -import SettingsApp from '../SettingsApp'; +import { SettingsApp } from '../SettingsApp'; jest.mock('../../../../helpers/pages', () => ({ addSideBarClass: jest.fn(), diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsSearch-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsSearch-test.tsx index 5cc275bc881..ba04565ca31 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsSearch-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsSearch-test.tsx @@ -24,6 +24,7 @@ import { mockComponent } from '../../../../helpers/mocks/component'; import { mockDefinition } from '../../../../helpers/mocks/settings'; import { mockRouter } from '../../../../helpers/testMocks'; import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils'; +import { queryToSearch } from '../../../../helpers/urls'; import { SettingsSearch } from '../SettingsSearch'; jest.mock('lunr', () => @@ -107,7 +108,7 @@ describe('instance', () => { expect(router.push).toBeCalledWith({ hash: '#foo', pathname: '/admin/settings', - query: { category: 'foo category' } + search: queryToSearch({ category: 'foo category' }) }); }); diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AllCategoriesList-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AllCategoriesList-test.tsx.snap deleted file mode 100644 index 1b46b821b3f..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AllCategoriesList-test.tsx.snap +++ /dev/null @@ -1,230 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: branches disabled 1`] = ` -<ul - className="side-tabs-menu" -> - <li - key="CAT_2" - > - <IndexLink - className="" - title="CAT_2_NAME" - to={ - Object { - "pathname": "/admin/settings", - "query": Object { - "category": "cat_2", - }, - } - } - > - CAT_2_NAME - </IndexLink> - </li> - <li - key="general" - > - <IndexLink - className="" - title="property.category.general" - to={ - Object { - "pathname": "/admin/settings", - "query": Object { - "category": undefined, - }, - } - } - > - property.category.general - </IndexLink> - </li> -</ul> -`; - -exports[`should render correctly: global mode 1`] = ` -<ul - className="side-tabs-menu" -> - <li - key="CAT_1" - > - <IndexLink - className="" - title="CAT_1_NAME" - to={ - Object { - "pathname": "/admin/settings", - "query": Object { - "category": "cat_1", - }, - } - } - > - CAT_1_NAME - </IndexLink> - </li> - <li - key="CAT_2" - > - <IndexLink - className="" - title="CAT_2_NAME" - to={ - Object { - "pathname": "/admin/settings", - "query": Object { - "category": "cat_2", - }, - } - } - > - CAT_2_NAME - </IndexLink> - </li> - <li - key="general" - > - <IndexLink - className="" - title="property.category.general" - to={ - Object { - "pathname": "/admin/settings", - "query": Object { - "category": undefined, - }, - } - } - > - property.category.general - </IndexLink> - </li> -</ul> -`; - -exports[`should render correctly: project mode 1`] = ` -<ul - className="side-tabs-menu" -> - <li - key="CAT_1" - > - <IndexLink - className="" - title="CAT_1_NAME" - to={ - Object { - "pathname": "/project/settings", - "query": Object { - "category": "cat_1", - "id": "my-project", - }, - } - } - > - CAT_1_NAME - </IndexLink> - </li> - <li - key="CAT_3" - > - <IndexLink - className="" - title="CAT_3_NAME" - to={ - Object { - "pathname": "/project/settings", - "query": Object { - "category": "cat_3", - "id": "my-project", - }, - } - } - > - CAT_3_NAME - </IndexLink> - </li> - <li - key="general" - > - <IndexLink - className="" - title="property.category.general" - to={ - Object { - "pathname": "/project/settings", - "query": Object { - "category": undefined, - "id": "my-project", - }, - } - } - > - property.category.general - </IndexLink> - </li> -</ul> -`; - -exports[`should render correctly: selected category 1`] = ` -<ul - className="side-tabs-menu" -> - <li - key="CAT_1" - > - <IndexLink - className="" - title="CAT_1_NAME" - to={ - Object { - "pathname": "/admin/settings", - "query": Object { - "category": "cat_1", - }, - } - } - > - CAT_1_NAME - </IndexLink> - </li> - <li - key="CAT_2" - > - <IndexLink - className="active" - title="CAT_2_NAME" - to={ - Object { - "pathname": "/admin/settings", - "query": Object { - "category": "cat_2", - }, - } - } - > - CAT_2_NAME - </IndexLink> - </li> - <li - key="general" - > - <IndexLink - className="" - title="property.category.general" - to={ - Object { - "pathname": "/admin/settings", - "query": Object { - "category": undefined, - }, - } - } - > - property.category.general - </IndexLink> - </li> -</ul> -`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AnalysisScope-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AnalysisScope-test.tsx.snap index 5e237ce1b23..7fccfe01e2f 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AnalysisScope-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AnalysisScope-test.tsx.snap @@ -8,8 +8,6 @@ exports[`should render correctly 1`] = ` settings.analysis_scope.wildcards.introduction <Link className="spacer-left" - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/project-administration/narrowing-the-focus/" > learn_more diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-test.tsx.snap index 1154b4e91f5..d5aaaf819a7 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-test.tsx.snap @@ -30,8 +30,6 @@ exports[`should render correctly 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/project-administration/new-code-period/" > learn_more diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsSearchRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsSearchRenderer-test.tsx.snap index ba2d014d06b..334d84a3098 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsSearchRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsSearchRenderer-test.tsx.snap @@ -76,15 +76,11 @@ exports[`should render correctly when open: results 1`] = ` <Link onClick={[MockFunction]} onMouseEnter={[Function]} - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "hash": "#foo", "pathname": "/admin/settings", - "query": Object { - "category": "foo category", - }, + "search": "?category=foo+category", } } > @@ -109,15 +105,11 @@ exports[`should render correctly when open: results 1`] = ` <Link onClick={[MockFunction]} onMouseEnter={[Function]} - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "hash": "#bar", "pathname": "/admin/settings", - "query": Object { - "category": "foo category", - }, + "search": "?category=foo+category", } } > diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx index 6327c6fa11a..1e5cd961d7d 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.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 { Button } from '../../../../components/controls/buttons'; import HelpTooltip from '../../../../components/controls/HelpTooltip'; import Tooltip from '../../../../components/controls/Tooltip'; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx index ade370d1e75..0fdefb5e2dc 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.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 { ButtonLink } from '../../../../components/controls/buttons'; import ValidationInput, { ValidationInputErrorPlacement diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegration.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegration.tsx index dd57a2c7464..127c0c1ca5b 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegration.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegration.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { WithRouterProps } from 'react-router'; import { countBindedProjects, deleteConfiguration, @@ -26,7 +25,7 @@ import { validateAlmSettings } from '../../../../api/alm-settings'; import withAppStateContext from '../../../../app/components/app-state/withAppStateContext'; -import { withRouter } from '../../../../components/hoc/withRouter'; +import { Location, Router, withRouter } from '../../../../components/hoc/withRouter'; import { AlmBindingDefinitionBase, AlmKeys, @@ -39,9 +38,11 @@ import { ExtendedSettingDefinition } from '../../../../types/settings'; import { Dict } from '../../../../types/types'; import AlmIntegrationRenderer from './AlmIntegrationRenderer'; -interface Props extends Pick<WithRouterProps, 'location' | 'router'> { +interface Props { appState: AppState; definitions: ExtendedSettingDefinition[]; + location: Location; + router: Router; } export type AlmTabs = AlmKeys.Azure | AlmKeys.GitHub | AlmKeys.GitLab | AlmKeys.BitbucketServer; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AzureForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AzureForm.tsx index ae8d550eb5c..3751017ab6a 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AzureForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AzureForm.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 { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; import { AlmKeys, AzureBindingDefinition } from '../../../../types/alm-settings'; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx index cdbcbfdda4b..e281ea2f167 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.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 { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketServerForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketServerForm.tsx index 6db54060f6d..cf472ab7740 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketServerForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketServerForm.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 { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; import { AlmKeys, BitbucketServerBindingDefinition } from '../../../../types/alm-settings'; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GithubForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GithubForm.tsx index b196f643606..4aacacfba9d 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GithubForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GithubForm.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 { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GitlabForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GitlabForm.tsx index d3ed87922bd..ac2ab98c35d 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GitlabForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GitlabForm.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 { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; import { AlmKeys, GitlabBindingDefinition } from '../../../../types/alm-settings'; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionBox-test.tsx.snap index 1937189579d..6ea758b9ec2 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionBox-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionBox-test.tsx.snap @@ -523,8 +523,6 @@ exports[`should render correctly: success with alert 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/github-integration/" > @@ -652,8 +650,6 @@ exports[`should render correctly: success with branches disabled 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/github-integration/" > diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap index daf6cf08860..0b37d9683d2 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap @@ -82,8 +82,6 @@ exports[`should render correctly: encryptable 1`] = ` values={ Object { "learn_more": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to={ Object { diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap index a13b6b6ead1..cade0289c9e 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap @@ -42,8 +42,6 @@ exports[`should render correctly: create 1`] = ` values={ Object { "doc_link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/azuredevops-integration/" > @@ -117,8 +115,6 @@ exports[`should render correctly: edit 1`] = ` values={ Object { "doc_link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/azuredevops-integration/" > diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap index 64876591a64..9d990ce41b2 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap @@ -46,8 +46,6 @@ exports[`should render correctly: default 1`] = ` values={ Object { "doc_link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/bitbucket-cloud-integration/" > @@ -135,8 +133,6 @@ exports[`should render correctly: invalid workspace ID 1`] = ` values={ Object { "doc_link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/bitbucket-cloud-integration/" > diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketServerForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketServerForm-test.tsx.snap index f8bf5e2ce02..ff7ec2649b4 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketServerForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketServerForm-test.tsx.snap @@ -37,8 +37,6 @@ exports[`should render correctly 1`] = ` values={ Object { "doc_link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/bitbucket-integration/" > diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GithubForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GithubForm-test.tsx.snap index e921eb46af9..2945edf3857 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GithubForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GithubForm-test.tsx.snap @@ -44,8 +44,6 @@ exports[`should render correctly 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/github-integration/" > @@ -139,8 +137,6 @@ exports[`should render correctly 2`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/github-integration/" > diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GitlabForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GitlabForm-test.tsx.snap index 335a2f039bc..cec311aa194 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GitlabForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GitlabForm-test.tsx.snap @@ -35,8 +35,6 @@ exports[`should render correctly 1`] = ` values={ Object { "doc_link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/gitlab-integration/" > @@ -106,8 +104,6 @@ exports[`should render correctly 2`] = ` values={ Object { "doc_link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/gitlab-integration/" > diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx index 2da3d4765a7..6f05ba109f1 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.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 withAppStateContext from '../../../../app/components/app-state/withAppStateContext'; import Toggle from '../../../../components/controls/Toggle'; import { Alert } from '../../../../components/ui/Alert'; diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx index 3a4f0b7d07c..cf65a0a9d5e 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.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 { components, OptionProps, SingleValueProps } from 'react-select'; import { Button, SubmitButton } from '../../../../components/controls/buttons'; import Select from '../../../../components/controls/Select'; diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap index 77a82861402..82c1f025f65 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap @@ -819,8 +819,6 @@ exports[`should render the monorepo field when the feature is supported 1`] = ` values={ Object { "doc_link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/azuredevops-integration/" > diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap index b72c9f4121e..958ef5a7534 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap @@ -265,15 +265,10 @@ exports[`should render correctly: when there are configuration errors (admin use values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/admin/settings", - "query": Object { - "alm": "github", - "category": "almintegration", - }, + "search": "?category=almintegration&alm=github", } } > @@ -911,14 +906,10 @@ exports[`should render correctly: with no ALM instances (admin user) 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/admin/settings", - "query": Object { - "category": "almintegration", - }, + "search": "?category=almintegration", } } > diff --git a/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionForm.tsx b/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionForm.tsx index 065dccac260..381a843057b 100644 --- a/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionForm.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 { encryptValue } from '../../../api/settings'; import { SubmitButton } from '../../../components/controls/buttons'; import { ClipboardButton } from '../../../components/controls/clipboard'; diff --git a/server/sonar-web/src/main/js/apps/settings/encryption/GenerateSecretKeyForm.tsx b/server/sonar-web/src/main/js/apps/settings/encryption/GenerateSecretKeyForm.tsx index 0201bba7b40..6874d6563b7 100644 --- a/server/sonar-web/src/main/js/apps/settings/encryption/GenerateSecretKeyForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/encryption/GenerateSecretKeyForm.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 { SubmitButton } from '../../../components/controls/buttons'; import { ClipboardButton } from '../../../components/controls/clipboard'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; diff --git a/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/EncryptionForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/EncryptionForm-test.tsx.snap index 29a34bb6671..c6af518e5fb 100644 --- a/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/EncryptionForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/EncryptionForm-test.tsx.snap @@ -51,8 +51,6 @@ exports[`should render correctly 1`] = ` values={ Object { "moreInformationLink": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/instance-administration/security/" > diff --git a/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/GenerateSecretKeyForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/GenerateSecretKeyForm-test.tsx.snap index 53251a408ba..4eed71722b2 100644 --- a/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/GenerateSecretKeyForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/GenerateSecretKeyForm-test.tsx.snap @@ -17,8 +17,6 @@ exports[`should render correctly 1`] = ` values={ Object { "moreInformationLink": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/instance-administration/security/" > diff --git a/server/sonar-web/src/main/js/apps/settings/routes.ts b/server/sonar-web/src/main/js/apps/settings/routes.ts deleted file mode 100644 index a16832835e3..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/routes.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/SettingsApp')) } - }, - { - path: 'encryption', - component: lazyLoadComponent(() => import('./encryption/EncryptionApp')) - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/settings/routes.tsx b/server/sonar-web/src/main/js/apps/settings/routes.tsx new file mode 100644 index 00000000000..67e534f5e6f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/routes.tsx @@ -0,0 +1,32 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import SettingsApp from './components/SettingsApp'; +import EncryptionApp from './encryption/EncryptionApp'; + +const routes = () => ( + <Route path="settings"> + <Route index={true} element={<SettingsApp />} /> + <Route path="encryption" element={<EncryptionApp />} /> + </Route> +); + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/settings/utils.ts b/server/sonar-web/src/main/js/apps/settings/utils.ts index 3cec4487f83..fd26233e62d 100644 --- a/server/sonar-web/src/main/js/apps/settings/utils.ts +++ b/server/sonar-web/src/main/js/apps/settings/utils.ts @@ -17,8 +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 { LocationDescriptor } from 'history'; import { sortBy } from 'lodash'; +import { Path } from 'react-router-dom'; import { hasMessage, translate } from '../../helpers/l10n'; import { getGlobalSettingsUrl, getProjectSettingsUrl } from '../../helpers/urls'; import { AlmKeys } from '../../types/alm-settings'; @@ -212,7 +212,7 @@ export function isRealSettingKey(key: string) { export function buildSettingLink( definition: ExtendedSettingDefinition, component?: Component -): LocationDescriptor { +): Partial<Path> { const { category, key } = definition; if (component !== undefined) { diff --git a/server/sonar-web/src/main/js/apps/system/components/App.tsx b/server/sonar-web/src/main/js/apps/system/components/App.tsx index 9beb5cee6c6..8ba55c260de 100644 --- a/server/sonar-web/src/main/js/apps/system/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/App.tsx @@ -19,10 +19,10 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { withRouter, WithRouterProps } from 'react-router'; import { getSystemInfo } from '../../../api/system'; import UpdateNotification from '../../../app/components/update-notification/UpdateNotification'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; +import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { translate } from '../../../helpers/l10n'; import { SysInfoCluster, SysInfoStandalone } from '../../../types/types'; import '../styles.css'; @@ -40,7 +40,10 @@ import ClusterSysInfos from './ClusterSysInfos'; import PageHeader from './PageHeader'; import StandaloneSysInfos from './StandaloneSysInfos'; -type Props = WithRouterProps; +interface Props { + location: Location; + router: Router; +} interface State { loading: boolean; diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/App-test.tsx index dd866fc1692..e231ef41d7d 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/App-test.tsx @@ -80,7 +80,5 @@ it('should toggle cards and update the URL', () => { }); function shallowRender(props: Partial<App['props']> = {}) { - return shallow<App>( - <App location={mockLocation()} params={{}} router={mockRouter()} routes={[]} {...props} /> - ); + return shallow<App>(<App location={mockLocation()} router={mockRouter()} {...props} />); } diff --git a/server/sonar-web/src/main/js/apps/system/routes.ts b/server/sonar-web/src/main/js/apps/system/routes.ts deleted file mode 100644 index 9d8f2bd42e4..00000000000 --- a/server/sonar-web/src/main/js/apps/system/routes.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/App')) } - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/system/routes.tsx b/server/sonar-web/src/main/js/apps/system/routes.tsx new file mode 100644 index 00000000000..5087a4ec38a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/system/routes.tsx @@ -0,0 +1,26 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import App from './components/App'; + +export const routes = () => <Route path="system" element={<App />} />; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/tutorials/components/TutorialsApp.tsx b/server/sonar-web/src/main/js/apps/tutorials/components/TutorialsApp.tsx index bce81dc134d..4174e269a3e 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/components/TutorialsApp.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/components/TutorialsApp.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import TutorialSelection from '../../../components/tutorials/TutorialSelection'; import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication'; @@ -50,4 +51,4 @@ export function TutorialsApp(props: TutorialsAppProps) { ); } -export default withCurrentUserContext(TutorialsApp); +export default withComponentContext(withCurrentUserContext(TutorialsApp)); diff --git a/server/sonar-web/src/main/js/apps/tutorials/routes.ts b/server/sonar-web/src/main/js/apps/tutorials/routes.ts deleted file mode 100644 index 6f1e2f0be31..00000000000 --- a/server/sonar-web/src/main/js/apps/tutorials/routes.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/TutorialsApp')) } - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/audit-logs/routes.ts b/server/sonar-web/src/main/js/apps/tutorials/routes.tsx index cf3bd5ca8ef..59c48641c5f 100644 --- a/server/sonar-web/src/main/js/apps/audit-logs/routes.ts +++ b/server/sonar-web/src/main/js/apps/tutorials/routes.tsx @@ -17,12 +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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import TutorialsApp from './components/TutorialsApp'; -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/AuditApp')) } - } -]; +const routes = () => <Route path="tutorials" element={<TutorialsApp />} />; export default routes; diff --git a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx index e788902f316..3a1b0cf85eb 100644 --- a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx +++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx @@ -34,8 +34,8 @@ import { parseQuery, Query, serializeQuery } from './utils'; interface Props { currentUser: { isLoggedIn: boolean; login?: string }; - location: Pick<Location, 'query'>; - router: Pick<Router, 'push'>; + location: Location; + router: Router; } interface State { diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx index 96cd752c072..0855aa622f0 100644 --- a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { Location } from '../../../components/hoc/withRouter'; +import { mockRouter } from '../../../helpers/testMocks'; import { waitAndUpdate } from '../../../helpers/testUtils'; import { UsersApp } from '../UsersApp'; @@ -78,11 +79,6 @@ it('should render correctly', async () => { function getWrapper(props: Partial<UsersApp['props']> = {}) { return shallow( - <UsersApp - currentUser={currentUser} - location={location} - router={{ push: jest.fn() }} - {...props} - /> + <UsersApp currentUser={currentUser} location={location} router={mockRouter()} {...props} /> ); } diff --git a/server/sonar-web/src/main/js/apps/users/routes.tsx b/server/sonar-web/src/main/js/apps/users/routes.tsx new file mode 100644 index 00000000000..5805fafc9e9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/routes.tsx @@ -0,0 +1,26 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import UsersApp from './UsersApp'; + +export const routes = () => <Route path="users" element={<UsersApp />} />; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx index 47ab504988c..1b7cfafa7ff 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx @@ -19,9 +19,10 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import LinkIcon from '../../../components/icons/LinkIcon'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { queryToSearch } from '../../../helpers/urls'; import { WebApi } from '../../../types/types'; import { getActionKey, serializeQuery } from '../utils'; import ActionChangelog from './ActionChangelog'; @@ -136,10 +137,12 @@ export default class Action extends React.PureComponent<Props, State> { className="spacer-right link-no-underline" to={{ pathname: '/web_api/' + actionKey, - query: serializeQuery({ - deprecated: Boolean(action.deprecatedSince), - internal: Boolean(action.internal) - }) + search: queryToSearch( + serializeQuery({ + deprecated: Boolean(action.deprecatedSince), + internal: Boolean(action.internal) + }) + ) }}> <LinkIcon /> </Link> diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx index a70bfa86e8a..8cf0c9623ff 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx @@ -19,7 +19,8 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; +import { queryToSearch } from '../../../helpers/urls'; import { WebApi } from '../../../types/types'; import { actionsFilter, isDomainPathActive, Query, serializeQuery } from '../utils'; import DeprecatedBadge from './DeprecatedBadge'; @@ -48,7 +49,7 @@ export default function Menu(props: Props) { active: isDomainPathActive(domain.path, splat) })} key={domain.path} - to={{ pathname: '/web_api/' + domain.path, query: serializeQuery(query) }}> + to={{ pathname: '/web_api/' + domain.path, search: queryToSearch(serializeQuery(query)) }}> <h3 className="list-group-item-heading"> {domain.path} {domain.deprecatedSince && <DeprecatedBadge since={domain.deprecatedSince} />} diff --git a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx index c295e6438c2..b872548ae65 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx @@ -20,11 +20,12 @@ import { maxBy } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { Link, withRouter, WithRouterProps } from 'react-router'; +import { Link, Params, useParams } from 'react-router-dom'; import { fetchWebApi } from '../../../api/web-api'; import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; +import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { translate } from '../../../helpers/l10n'; import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages'; import { scrollToElement } from '../../../helpers/scrolling'; @@ -42,7 +43,11 @@ import Domain from './Domain'; import Menu from './Menu'; import Search from './Search'; -type Props = WithRouterProps; +interface Props { + location: Location; + params: Params; + router: Router; +} interface State { domains: WebApi.Domain[]; @@ -197,7 +202,13 @@ export class WebApiApp extends React.PureComponent<Props, State> { } } -export default withRouter(WebApiApp); +function WebApiAppWithParams(props: { router: Router; location: Location }) { + const params = useParams(); + + return <WebApiApp {...props} params={{ splat: params['*'] }} />; +} + +export default withRouter(WebApiAppWithParams); /** Checks if all actions are deprecated, and returns the latest deprecated one */ function getLatestDeprecatedAction(domain: Pick<WebApi.Domain, 'actions'>) { diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/WebApiApp-test.tsx b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/WebApiApp-test.tsx index 774e5bc5cfb..faa0f6ce70e 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/WebApiApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/WebApiApp-test.tsx @@ -66,7 +66,6 @@ function shallowRender(props: Partial<WebApiApp['props']> = {}) { location={mockLocation()} params={{ splat: 'foo/bar' }} router={mockRouter()} - routes={[]} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Action-test.tsx.snap b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Action-test.tsx.snap index 5d5142d1c66..af905ee7f55 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Action-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Action-test.tsx.snap @@ -99,12 +99,10 @@ exports[`should render correctly 1`] = ` > <Link className="spacer-right link-no-underline" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/web_api/foo/foo", - "query": Object {}, + "search": "?", } } > diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap index 64b8d0e4bd2..629742ef918 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap @@ -10,14 +10,10 @@ exports[`should also render domains with an actions description matching the que <Link className="list-group-item" key="bar" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/web_api/bar", - "query": Object { - "query": "Bar", - }, + "search": "?query=Bar", } } > @@ -30,14 +26,10 @@ exports[`should also render domains with an actions description matching the que <Link className="list-group-item" key="baz" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/web_api/baz", - "query": Object { - "query": "Bar", - }, + "search": "?query=Bar", } } > @@ -61,12 +53,10 @@ exports[`should not render deprecated domains 1`] = ` <Link className="list-group-item" key="foo" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/web_api/foo", - "query": Object {}, + "search": "?", } } > @@ -90,12 +80,10 @@ exports[`should not render internal domains 1`] = ` <Link className="list-group-item" key="foo" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/web_api/foo", - "query": Object {}, + "search": "?", } } > @@ -119,14 +107,10 @@ exports[`should render deprecated domains 1`] = ` <Link className="list-group-item" key="foo" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/web_api/foo", - "query": Object { - "deprecated": true, - }, + "search": "?deprecated=true", } } > @@ -139,14 +123,10 @@ exports[`should render deprecated domains 1`] = ` <Link className="list-group-item" key="bar" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/web_api/bar", - "query": Object { - "deprecated": true, - }, + "search": "?deprecated=true", } } > @@ -173,14 +153,10 @@ exports[`should render internal domains 1`] = ` <Link className="list-group-item" key="foo" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/web_api/foo", - "query": Object { - "internal": true, - }, + "search": "?internal=true", } } > @@ -193,14 +169,10 @@ exports[`should render internal domains 1`] = ` <Link className="list-group-item" key="bar" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/web_api/bar", - "query": Object { - "internal": true, - }, + "search": "?internal=true", } } > @@ -225,14 +197,10 @@ exports[`should render only domains with an action matching the query 1`] = ` <Link className="list-group-item" key="foo" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/web_api/foo", - "query": Object { - "query": "Foo", - }, + "search": "?query=Foo", } } > diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/WebApiApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/WebApiApp-test.tsx.snap index 1ff1ab2615a..1fc0062503f 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/WebApiApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/WebApiApp-test.tsx.snap @@ -71,8 +71,6 @@ exports[`should render correctly 2`] = ` className="web-api-page-header" > <Link - onlyActiveOnIndex={false} - style={Object {}} to="/web_api/" > <h1> diff --git a/server/sonar-web/src/main/js/apps/web-api/routes.ts b/server/sonar-web/src/main/js/apps/web-api/routes.ts deleted file mode 100644 index f950de012bb..00000000000 --- a/server/sonar-web/src/main/js/apps/web-api/routes.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/WebApiApp')) } - }, - { - path: '**', - component: lazyLoadComponent(() => import('./components/WebApiApp')) - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/documentation/routes.ts b/server/sonar-web/src/main/js/apps/web-api/routes.tsx index f396663b15b..56be0a16a19 100644 --- a/server/sonar-web/src/main/js/apps/documentation/routes.ts +++ b/server/sonar-web/src/main/js/apps/web-api/routes.tsx @@ -17,10 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { lazyLoadComponent } from '../../components/lazyLoadComponent'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import WebApiApp from './components/WebApiApp'; -const App = lazyLoadComponent(() => import(/* webpackChunkName: "docs" */ './components/App')); - -const routes = [{ indexRoute: { component: App } }, { path: '**', indexRoute: { component: App } }]; +const routes = () => ( + <Route path="web_api"> + <Route index={true} element={<WebApiApp />} /> + <Route path="*" element={<WebApiApp />} /> + </Route> +); export default routes; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx index d2875d66824..d495d911cd2 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx @@ -20,16 +20,17 @@ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { createWebhook, deleteWebhook, searchWebhooks, updateWebhook } from '../../../api/webhooks'; +import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import { translate } from '../../../helpers/l10n'; -import { LightComponent, Webhook } from '../../../types/types'; +import { Component, Webhook } from '../../../types/types'; import PageActions from './PageActions'; import PageHeader from './PageHeader'; import WebhooksList from './WebhooksList'; interface Props { // eslint-disable-next-line react/no-unused-prop-types - component?: LightComponent; + component?: Component; } interface State { @@ -37,7 +38,7 @@ interface State { webhooks: Webhook[]; } -export default class App extends React.PureComponent<Props, State> { +export class App extends React.PureComponent<Props, State> { mounted = false; state: State = { loading: true, webhooks: [] }; @@ -148,3 +149,5 @@ export default class App extends React.PureComponent<Props, State> { ); } } + +export default withComponentContext(App); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx index 0a6c7d92b61..aa994d2db64 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.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 { translate } from '../../../helpers/l10n'; interface Props { diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx index c3ba2f55ede..96d3cd899cc 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx @@ -25,7 +25,8 @@ import { searchWebhooks, updateWebhook } from '../../../../api/webhooks'; -import App from '../App'; +import { mockComponent } from '../../../../helpers/mocks/component'; +import { App } from '../App'; jest.mock('../../../../api/webhooks', () => ({ createWebhook: jest.fn(() => @@ -43,7 +44,7 @@ jest.mock('../../../../api/webhooks', () => ({ updateWebhook: jest.fn(() => Promise.resolve()) })); -const component = { key: 'bar', qualifier: 'TRK' }; +const component = mockComponent({ key: 'bar', qualifier: 'TRK' }); beforeEach(() => { (createWebhook as jest.Mock<any>).mockClear(); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap index f8a71f5b264..62a0abeeed9 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap @@ -22,8 +22,6 @@ exports[`should render correctly 1`] = ` values={ Object { "url": <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/project-administration/webhooks/" > webhooks.documentation_link diff --git a/server/sonar-web/src/main/js/apps/webhooks/routes.ts b/server/sonar-web/src/main/js/apps/webhooks/routes.ts deleted file mode 100644 index 9d8f2bd42e4..00000000000 --- a/server/sonar-web/src/main/js/apps/webhooks/routes.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { lazyLoadComponent } from '../../components/lazyLoadComponent'; - -const routes = [ - { - indexRoute: { component: lazyLoadComponent(() => import('./components/App')) } - } -]; - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/webhooks/routes.tsx b/server/sonar-web/src/main/js/apps/webhooks/routes.tsx new file mode 100644 index 00000000000..b472e935d45 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/routes.tsx @@ -0,0 +1,26 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import App from './components/App'; + +export const routes = () => <Route path="webhooks" element={<App />} />; + +export default routes; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx index 569775ece00..c04ccc4f6fd 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.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 { ButtonIcon } from '../../components/controls/buttons'; import { ClipboardIconButton } from '../../components/controls/clipboard'; import Dropdown from '../../components/controls/Dropdown'; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeaderSlim.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeaderSlim.tsx index 38ebd8cbce5..b6a6ac4774f 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeaderSlim.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeaderSlim.tsx @@ -19,7 +19,7 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { ButtonIcon } from '../../components/controls/buttons'; import { ClipboardIconButton } from '../../components/controls/clipboard'; import ExpandSnippetIcon from '../../components/icons/ExpandSnippetIcon'; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeader-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeader-test.tsx.snap index 58ecb2f6e0b..f5c25d061ba 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeader-test.tsx.snap @@ -71,18 +71,12 @@ exports[`should render correctly for a regular file 1`] = ` <li> <Link className="js-new-window" - onlyActiveOnIndex={false} rel="noopener noreferrer" - style={Object {}} target="_blank" to={ Object { "pathname": "/code", - "query": Object { - "id": "project", - "line": undefined, - "selected": "project:foo/bar.ts", - }, + "search": "?id=project&selected=project%3Afoo%2Fbar.ts", } } > @@ -210,18 +204,12 @@ exports[`should render correctly for a unit test 1`] = ` <li> <Link className="js-new-window" - onlyActiveOnIndex={false} rel="noopener noreferrer" - style={Object {}} target="_blank" to={ Object { "pathname": "/code", - "query": Object { - "id": "my-project", - "line": undefined, - "selected": "my-project:foo/bar.ts", - }, + "search": "?id=my-project&selected=my-project%3Afoo%2Fbar.ts", } } > @@ -374,17 +362,11 @@ exports[`should render correctly if issue details are passed 1`] = ` className="source-viewer-header-measure-value" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "files": "foo/bar.ts", - "id": "project", - "resolved": "false", - "types": "BUG", - }, + "search": "?files=foo%2Fbar.ts&resolved=false&types=BUG&id=project", } } > @@ -405,17 +387,11 @@ exports[`should render correctly if issue details are passed 1`] = ` className="source-viewer-header-measure-value" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "files": "foo/bar.ts", - "id": "project", - "resolved": "false", - "types": "VULNERABILITY", - }, + "search": "?files=foo%2Fbar.ts&resolved=false&types=VULNERABILITY&id=project", } } > @@ -436,17 +412,11 @@ exports[`should render correctly if issue details are passed 1`] = ` className="source-viewer-header-measure-value" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "files": "foo/bar.ts", - "id": "project", - "resolved": "false", - "types": "CODE_SMELL", - }, + "search": "?files=foo%2Fbar.ts&resolved=false&types=CODE_SMELL&id=project", } } > @@ -467,17 +437,11 @@ exports[`should render correctly if issue details are passed 1`] = ` className="source-viewer-header-measure-value" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "files": "foo/bar.ts", - "id": "project", - "resolved": "false", - "types": "SECURITY_HOTSPOT", - }, + "search": "?files=foo%2Fbar.ts&resolved=false&types=SECURITY_HOTSPOT&id=project", } } > @@ -504,18 +468,12 @@ exports[`should render correctly if issue details are passed 1`] = ` <li> <Link className="js-new-window" - onlyActiveOnIndex={false} rel="noopener noreferrer" - style={Object {}} target="_blank" to={ Object { "pathname": "/code", - "query": Object { - "id": "project", - "line": undefined, - "selected": "project:foo/bar.ts", - }, + "search": "?id=project&selected=project%3Afoo%2Fbar.ts", } } > diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeaderSlim-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeaderSlim-test.tsx.snap index 1a2badc509e..0b6e42a0fb3 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeaderSlim-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeaderSlim-test.tsx.snap @@ -52,16 +52,11 @@ exports[`should render correctly 1`] = ` className="flex-0 big-spacer-left" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "files": "foo/bar.ts", - "id": "my-project", - "resolved": "false", - }, + "search": "?files=foo%2Fbar.ts&resolved=false&id=my-project", } } > @@ -132,16 +127,11 @@ exports[`should render correctly: no link to project 1`] = ` className="flex-0 big-spacer-left" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "files": "foo/bar.ts", - "id": "my-project", - "resolved": "false", - }, + "search": "?files=foo%2Fbar.ts&resolved=false&id=my-project", } } > @@ -201,16 +191,11 @@ exports[`should render correctly: no project name 1`] = ` className="flex-0 big-spacer-left" > <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "files": "foo/bar.ts", - "id": "my-project", - "resolved": "false", - }, + "search": "?files=foo%2Fbar.ts&resolved=false&id=my-project", } } > diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx index cb0778b2fef..2716924b6c2 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx @@ -19,7 +19,7 @@ */ import { groupBy, sortBy } 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 { Alert } from '../../../components/ui/Alert'; import { isPullRequest } from '../../../helpers/branch-like'; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx index 3456b9b3577..86c8a90e4fd 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx @@ -19,7 +19,7 @@ */ import { groupBy, keyBy, sortBy } from 'lodash'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { getFacets } from '../../../api/issues'; import { getMeasures } from '../../../api/measures'; import { getAllMetrics } from '../../../api/metrics'; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap index eaa091a84af..6b09bf1ea0b 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap @@ -25,15 +25,10 @@ exports[`should render source file 1`] = ` qualifier="TRK" /> <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "feature", - "id": "project-key", - }, + "search": "?branch=feature&id=project-key", } } > @@ -594,15 +589,10 @@ exports[`should render source file 2`] = ` qualifier="TRK" /> <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "feature", - "id": "project-key", - }, + "search": "?branch=feature&id=project-key", } } > @@ -1746,15 +1736,10 @@ exports[`should render test file 1`] = ` qualifier="TRK" /> <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/dashboard", - "query": Object { - "branch": "feature", - "id": "project-key", - }, + "search": "?branch=feature&id=project-key", } } > diff --git a/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx b/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx index 89af3278c2f..bdb4504a50b 100644 --- a/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx +++ b/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx @@ -24,10 +24,10 @@ import { event, select } from 'd3-selection'; import { zoom, ZoomBehavior, zoomIdentity } from 'd3-zoom'; import { sortBy, uniq } from 'lodash'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'; import { translate } from '../../helpers/l10n'; -import { Location } from '../../helpers/urls'; +import { convertToTo, Location } from '../../helpers/urls'; import Tooltip from '../controls/Tooltip'; import './BubbleChart.css'; @@ -117,7 +117,7 @@ export default class BubbleChart<T> extends React.PureComponent<Props<T>, State> }); }; - resetZoom = (e: React.MouseEvent<Link>) => { + resetZoom = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); if (this.zoom && this.node) { @@ -377,7 +377,7 @@ function Bubble<T>(props: BubbleProps<T>) { ); if (props.link && !props.onClick) { - circle = <Link to={props.link}>{circle}</Link>; + circle = <Link to={convertToTo(props.link)}>{circle}</Link>; } return ( diff --git a/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx b/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx index 49b8825bce4..3e10e369ac0 100644 --- a/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx +++ b/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx @@ -20,8 +20,8 @@ import classNames from 'classnames'; import { scaleLinear } from 'd3-scale'; import * as React from 'react'; -import { Link } from 'react-router'; -import { Location } from '../../helpers/urls'; +import { Link } from 'react-router-dom'; +import { convertToTo, Location } from '../../helpers/urls'; import Tooltip, { Placement } from '../controls/Tooltip'; import LinkIcon from '../icons/LinkIcon'; @@ -69,8 +69,9 @@ export default class TreeMapRect extends React.PureComponent<Props> { if (!hasMinSize || link == null) { return null; } + return ( - <Link className="treemap-link" onClick={this.handleLinkClick} to={link}> + <Link className="treemap-link" onClick={this.handleLinkClick} to={convertToTo(link)}> <LinkIcon /> </Link> ); diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx b/server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx index f83cff62df4..d47c0ebfcd1 100644 --- a/server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx +++ b/server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx @@ -21,7 +21,7 @@ import { select } from 'd3-selection'; import { zoom } from 'd3-zoom'; import { shallow } from 'enzyme'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { AutoSizer, AutoSizerProps } from 'react-virtualized/dist/commonjs/AutoSizer'; import { mockComponentMeasureEnhanced } from '../../../helpers/mocks/component'; import { mockHtmlElement } from '../../../helpers/mocks/dom'; diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap index 8adfc888cd0..1e0feb80ef5 100644 --- a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap @@ -40,8 +40,6 @@ exports[`should render bubble links 1`] = ` <Tooltip> <g> <Link - onlyActiveOnIndex={false} - style={Object {}} to="foo" > <circle @@ -64,8 +62,6 @@ exports[`should render bubble links 2`] = ` <Tooltip> <g> <Link - onlyActiveOnIndex={false} - style={Object {}} to="bar" > <circle diff --git a/server/sonar-web/src/main/js/components/common/ActivityLink.tsx b/server/sonar-web/src/main/js/components/common/ActivityLink.tsx index b231d18438d..0a4f8fc8698 100644 --- a/server/sonar-web/src/main/js/components/common/ActivityLink.tsx +++ b/server/sonar-web/src/main/js/components/common/ActivityLink.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 HistoryIcon from '../../components/icons/HistoryIcon'; import { translate } from '../../helpers/l10n'; import { getActivityUrl, getMeasureHistoryUrl } from '../../helpers/urls'; diff --git a/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx b/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx index 7d9b51b0744..e2150e74794 100644 --- a/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx +++ b/server/sonar-web/src/main/js/components/common/DocumentationTooltip.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 { isWebUri } from 'valid-url'; import HelpTooltip from '../../components/controls/HelpTooltip'; import DetachIcon from '../../components/icons/DetachIcon'; diff --git a/server/sonar-web/src/main/js/components/common/MeasuresLink.tsx b/server/sonar-web/src/main/js/components/common/MeasuresLink.tsx index aeb02bc74e1..c3515fc8bfa 100644 --- a/server/sonar-web/src/main/js/components/common/MeasuresLink.tsx +++ b/server/sonar-web/src/main/js/components/common/MeasuresLink.tsx @@ -19,7 +19,7 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import MeasuresIcon from '../../components/icons/MeasuresIcon'; import { translate } from '../../helpers/l10n'; import { getComponentDrilldownUrl } from '../../helpers/urls'; diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ActivityLink-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ActivityLink-test.tsx.snap index e61f19c53df..3a792609d92 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ActivityLink-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ActivityLink-test.tsx.snap @@ -3,15 +3,10 @@ exports[`renders correctly 1`] = ` <Link className="activity-link" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/project/activity", - "query": Object { - "graph": undefined, - "id": "foo", - }, + "search": "?id=foo", } } > @@ -28,16 +23,10 @@ exports[`renders correctly 1`] = ` exports[`renders correctly 2`] = ` <Link className="activity-link" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/project/activity", - "query": Object { - "branch": "branch-6.7", - "graph": undefined, - "id": "foo", - }, + "search": "?id=foo&branch=branch-6.7", } } > @@ -54,15 +43,10 @@ exports[`renders correctly 2`] = ` exports[`renders correctly 3`] = ` <Link className="activity-link" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/project/activity", - "query": Object { - "graph": "coverage", - "id": "foo", - }, + "search": "?id=foo&graph=coverage", } } > @@ -79,16 +63,10 @@ exports[`renders correctly 3`] = ` exports[`renders correctly 4`] = ` <Link className="activity-link" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/project/activity", - "query": Object { - "custom_metrics": "new_bugs,bugs", - "graph": "custom", - "id": "foo", - }, + "search": "?id=foo&graph=custom&custom_metrics=new_bugs%2Cbugs", } } > diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DocumentationTooltip-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DocumentationTooltip-test.tsx.snap index 6dcf00334b3..3755fb97cf0 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DocumentationTooltip-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DocumentationTooltip-test.tsx.snap @@ -78,9 +78,7 @@ exports[`renders correctly: with links 1`] = ` > <Link className="display-inline-flex-center link-with-icon" - onlyActiveOnIndex={false} rel="noopener noreferrer" - style={Object {}} target="_blank" to="http://link.tosome.place" > @@ -98,9 +96,7 @@ exports[`renders correctly: with links 1`] = ` > <Link className="display-inline-flex-center link-with-icon" - onlyActiveOnIndex={false} rel="noopener noreferrer" - style={Object {}} target="_blank" to="/documentation/guide" > @@ -113,8 +109,6 @@ exports[`renders correctly: with links 1`] = ` className="little-spacer-bottom" > <Link - onlyActiveOnIndex={false} - style={Object {}} to="/projects" > <span> diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MeasuresLink-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MeasuresLink-test.tsx.snap index e874481330c..80224fda5a4 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MeasuresLink-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MeasuresLink-test.tsx.snap @@ -3,15 +3,10 @@ exports[`renders 1`] = ` <Link className="measures-link" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/component_measures", - "query": Object { - "id": "foo", - "metric": "security_rating", - }, + "search": "?id=foo&metric=security_rating", } } > @@ -28,15 +23,10 @@ exports[`renders 1`] = ` exports[`renders 2`] = ` <Link className="measures-link" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/component_measures", - "query": Object { - "id": "foo", - "metric": "security_rating", - }, + "search": "?id=foo&metric=security_rating", } } > @@ -53,15 +43,10 @@ exports[`renders 2`] = ` exports[`renders 3`] = ` <Link className="measures-link" - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/component_measures", - "query": Object { - "id": "foo", - "metric": "security_rating", - }, + "search": "?id=foo&metric=security_rating", } } > diff --git a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx index ac124ce01ad..1a87832a2f2 100644 --- a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx +++ b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx @@ -18,9 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; -import { LocationDescriptor } from 'history'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link, To } from 'react-router-dom'; import { translate } from '../../helpers/l10n'; import DropdownIcon from '../icons/DropdownIcon'; import SettingsIcon from '../icons/SettingsIcon'; @@ -68,7 +67,7 @@ interface ItemProps { download?: string; id?: string; onClick?: () => void; - to?: LocationDescriptor; + to?: To; } export class ActionsDropdownItem extends React.PureComponent<ItemProps> { diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ActionsDropdown-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ActionsDropdown-test.tsx.snap index 42636f02080..dbb20219cbe 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ActionsDropdown-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ActionsDropdown-test.tsx.snap @@ -81,8 +81,6 @@ exports[`ActionsDropdownItem should render correctly 2`] = ` <Link className="foo text-danger" id="baz" - onlyActiveOnIndex={false} - style={Object {}} to="path/name" > <span> diff --git a/server/sonar-web/src/main/js/components/docs/DocLink.tsx b/server/sonar-web/src/main/js/components/docs/DocLink.tsx index 1e1c556665a..493a652f0fc 100644 --- a/server/sonar-web/src/main/js/components/docs/DocLink.tsx +++ b/server/sonar-web/src/main/js/components/docs/DocLink.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 withAppStateContext from '../../app/components/app-state/withAppStateContext'; import DetachIcon from '../../components/icons/DetachIcon'; import { AppState } from '../../types/appstate'; diff --git a/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx b/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx index eb29f9be7aa..4074ac970aa 100644 --- a/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx +++ b/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx @@ -19,7 +19,7 @@ */ import { forEach } from 'lodash'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import DetachIcon from '../../components/icons/DetachIcon'; import { Dict } from '../../types/types'; diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap index a9918c0b752..2ec3dbe8289 100644 --- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap @@ -17,8 +17,6 @@ exports[`should render documentation anchor 1`] = ` exports[`should render documentation link 1`] = ` <Link - onlyActiveOnIndex={false} - style={Object {}} to="/documentation/foo/bar" > link text @@ -52,8 +50,6 @@ exports[`should render sonarqube admin link on sonarqube for admin 1`] = ` exports[`should render sonarqube admin link on sonarqube for admin 2`] = ` <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/foo/bar" > @@ -71,8 +67,6 @@ exports[`should render sonarqube link on sonarqube 1`] = ` exports[`should render sonarqube link on sonarqube 2`] = ` <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/foo/bar" > diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap index 4d7250e364e..851e357803a 100644 --- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap @@ -2,9 +2,7 @@ exports[`should render internal link 1`] = ` <Link - onlyActiveOnIndex={false} rel="noopener noreferrer" - style={Object {}} target="_blank" to="/documentation/foo/bar" /> @@ -12,9 +10,7 @@ exports[`should render internal link 1`] = ` exports[`should render links with custom props 1`] = ` <Link - onlyActiveOnIndex={false} rel="noopener noreferrer" - style={Object {}} target="_blank" to="/documentation/foo/baz" /> diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx index 0f8d1db103c..699c03b9521 100644 --- a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.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 { SuggestionLink } from '../../types/types'; diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap index 29a379acaca..da58a8ccb00 100644 --- a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap @@ -11,8 +11,6 @@ exports[`should render 1`] = ` <li> <Link onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation" > @@ -22,8 +20,6 @@ exports[`should render 1`] = ` <li> <Link onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} to="/web_api" > api_documentation.page diff --git a/server/sonar-web/src/main/js/components/hoc/withLocation.tsx b/server/sonar-web/src/main/js/components/hoc/withLocation.tsx new file mode 100644 index 00000000000..4b6f25401a7 --- /dev/null +++ b/server/sonar-web/src/main/js/components/hoc/withLocation.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 { Location, useLocation } from 'react-router-dom'; + +export default function withLocation<P>( + WrappedComponent: React.ComponentType<P & { location: Location }> +) { + return function WithLocation(props: Omit<P, 'location'>) { + const location = useLocation(); + + return <WrappedComponent location={location} {...(props as P)} />; + }; +} diff --git a/server/sonar-web/src/main/js/components/hoc/withRouter.tsx b/server/sonar-web/src/main/js/components/hoc/withRouter.tsx index ca8509036b1..650d6db725c 100644 --- a/server/sonar-web/src/main/js/components/hoc/withRouter.tsx +++ b/server/sonar-web/src/main/js/components/hoc/withRouter.tsx @@ -18,18 +18,78 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { withRouter as originalWithRouter, WithRouterProps } from 'react-router'; +import { + Location as LocationRouter, + Params, + useLocation as useLocationRouter, + useNavigate, + useParams, + useSearchParams +} from 'react-router-dom'; +import { queryToSearch, searchParamsToQuery } from '../../helpers/urls'; +import { RawQuery } from '../../types/types'; +import { getWrappedDisplayName } from './utils'; -export type Location = WithRouterProps['location']; -export type Router = WithRouterProps['router']; +export interface Location extends LocationRouter { + query: RawQuery; +} + +export interface Router { + replace: (location: string | Partial<Location>) => void; + push: (location: string | Partial<Location>) => void; +} -interface InjectedProps { - location?: Partial<Location>; - router?: Partial<Router>; +export interface WithRouterProps { + location: Location; + params: Params; + router: Router; } -export function withRouter<P extends InjectedProps>( - WrappedComponent: React.ComponentType<P & InjectedProps> -): React.ComponentType<Omit<P, keyof InjectedProps>> { - return originalWithRouter(WrappedComponent as any); +export function withRouter<P extends Partial<WithRouterProps>>( + WrappedComponent: React.ComponentType<P> +): React.ComponentType<Omit<P, keyof WithRouterProps>> { + function ComponentWithRouterProp(props: P) { + const locationRouter = useLocationRouter(); + const navigate = useNavigate(); + const params = useParams(); + const [searchParams] = useSearchParams(); + + const router = React.useMemo( + () => ({ + replace: (path: string | Partial<Location>) => { + if ((path as Location).query) { + path.search = queryToSearch((path as Location).query); + } + navigate(path, { replace: true }); + }, + push: (path: string | Partial<Location>) => { + if ((path as Location).query) { + path.search = queryToSearch((path as Location).query); + } + navigate(path); + } + }), + [navigate] + ); + + const location = { + ...locationRouter, + query: searchParamsToQuery(searchParams) + }; + + return <WrappedComponent {...props} location={location} params={params} router={router} />; + } + + (ComponentWithRouterProp as React.FC<P>).displayName = getWrappedDisplayName( + WrappedComponent, + 'withRouter' + ); + + return ComponentWithRouterProp; +} + +export function useLocation() { + const location = useLocationRouter(); + + return { ...location, query: searchParamsToQuery(new URLSearchParams(location.search)) }; } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx index b010d31c688..1e39ea8bd65 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.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 LinkIcon from '../../../components/icons/LinkIcon'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap index 8b2455b7c45..3cb4d435d76 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap @@ -73,19 +73,13 @@ exports[`should render correctly: default 1`] = ` > <Link className="js-issue-permalink link-no-underline" - onlyActiveOnIndex={false} - style={Object {}} target="_blank" title="permalink" to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "myproject", - "issues": "AVsae-CQS-9G3txfbFN2", - "open": "AVsae-CQS-9G3txfbFN2", - "types": undefined, - }, + "search": "?issues=AVsae-CQS-9G3txfbFN2&open=AVsae-CQS-9G3txfbFN2&id=myproject", } } > @@ -170,19 +164,13 @@ exports[`should render correctly: with filter 1`] = ` > <Link className="js-issue-permalink link-no-underline" - onlyActiveOnIndex={false} - style={Object {}} target="_blank" title="permalink" to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "myproject", - "issues": "AVsae-CQS-9G3txfbFN2", - "open": "AVsae-CQS-9G3txfbFN2", - "types": undefined, - }, + "search": "?issues=AVsae-CQS-9G3txfbFN2&open=AVsae-CQS-9G3txfbFN2&id=myproject", } } > @@ -386,19 +374,13 @@ exports[`should render correctly: with multi locations 1`] = ` > <Link className="js-issue-permalink link-no-underline" - onlyActiveOnIndex={false} - style={Object {}} target="_blank" title="permalink" to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "myproject", - "issues": "AVsae-CQS-9G3txfbFN2", - "open": "AVsae-CQS-9G3txfbFN2", - "types": undefined, - }, + "search": "?issues=AVsae-CQS-9G3txfbFN2&open=AVsae-CQS-9G3txfbFN2&id=myproject", } } > @@ -549,19 +531,12 @@ exports[`should render correctly: with multi locations and link 1`] = ` className="issue-meta" > <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "branch": "branch-6.7", - "id": "myproject", - "issues": "AVsae-CQS-9G3txfbFN2", - "open": "AVsae-CQS-9G3txfbFN2", - "types": undefined, - }, + "search": "?branch=branch-6.7&issues=AVsae-CQS-9G3txfbFN2&open=AVsae-CQS-9G3txfbFN2&id=myproject", } } > @@ -579,20 +554,13 @@ exports[`should render correctly: with multi locations and link 1`] = ` > <Link className="js-issue-permalink link-no-underline" - onlyActiveOnIndex={false} - style={Object {}} target="_blank" title="permalink" to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "branch": "branch-6.7", - "id": "myproject", - "issues": "AVsae-CQS-9G3txfbFN2", - "open": "AVsae-CQS-9G3txfbFN2", - "types": undefined, - }, + "search": "?branch=branch-6.7&issues=AVsae-CQS-9G3txfbFN2&open=AVsae-CQS-9G3txfbFN2&id=myproject", } } > diff --git a/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx b/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx index e4a36009c39..bd368a08cf7 100644 --- a/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx +++ b/server/sonar-web/src/main/js/components/shared/DrilldownLink.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 { getBranchLikeQuery } from '../../helpers/branch-like'; import { getComponentDrilldownUrl, getComponentIssuesUrl } from '../../helpers/urls'; import { BranchLike } from '../../types/branch-like'; diff --git a/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/DrilldownLink-test.tsx.snap b/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/DrilldownLink-test.tsx.snap index cb653af58a1..e09cd6d4cd1 100644 --- a/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/DrilldownLink-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/DrilldownLink-test.tsx.snap @@ -2,17 +2,10 @@ exports[`should render correctly 1`] = ` <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { "pathname": "/component_measures", - "query": Object { - "asc": undefined, - "id": "project123", - "metric": "other", - "view": "list", - }, + "search": "?id=project123&metric=other&view=list", } } > @@ -22,15 +15,11 @@ exports[`should render correctly 1`] = ` exports[`should render issuesLink correctly 1`] = ` <Link - onlyActiveOnIndex={false} - style={Object {}} to={ Object { + "hash": "", "pathname": "/project/issues", - "query": Object { - "id": "project123", - "resolved": "false", - }, + "search": "?resolved=false&id=project123", } } > diff --git a/server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx b/server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx index beaccfa713e..ae972acfd0e 100644 --- a/server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { WithRouterProps } from 'react-router'; import { getAlmSettingsNoCatch } from '../../api/alm-settings'; import { getScannableProjects } from '../../api/components'; import { getValues } from '../../api/settings'; @@ -29,15 +28,17 @@ import { Permissions } from '../../types/permissions'; import { SettingsKey } from '../../types/settings'; import { Component } from '../../types/types'; import { LoggedInUser } from '../../types/users'; -import { withRouter } from '../hoc/withRouter'; +import { Location, Router, withRouter } from '../hoc/withRouter'; import TutorialSelectionRenderer from './TutorialSelectionRenderer'; import { TutorialModes } from './types'; -interface Props extends Pick<WithRouterProps, 'router' | 'location'> { +interface Props { component: Component; currentUser: LoggedInUser; projectBinding?: ProjectAlmBindingResponse; willRefreshAutomatically?: boolean; + location: Location; + router: Router; } interface State { diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/AlertClassicEditor.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/AlertClassicEditor.tsx index cd23d453df5..ae64c89960d 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/AlertClassicEditor.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/AlertClassicEditor.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 { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/PublishSteps.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/PublishSteps.tsx index 13d10fb06ec..665d729a0d5 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/PublishSteps.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/PublishSteps.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 withAppStateContext from '../../../../app/components/app-state/withAppStateContext'; import { Alert } from '../../../../components/ui/Alert'; import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/AlertClassicEditor-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/AlertClassicEditor-test.tsx.snap index 287d3d1a78a..a9a6864ee29 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/AlertClassicEditor-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/AlertClassicEditor-test.tsx.snap @@ -11,8 +11,6 @@ exports[`should render correctly 1`] = ` values={ Object { "doc_link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/azuredevops-integration/" > diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/PublishSteps-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/PublishSteps-test.tsx.snap index 346a354101d..13c0f03e6f9 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/PublishSteps-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/PublishSteps-test.tsx.snap @@ -68,8 +68,6 @@ exports[`should render correctly 2`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/azuredevops-integration/" > diff --git a/server/sonar-web/src/main/js/components/tutorials/components/EditTokenModal.tsx b/server/sonar-web/src/main/js/components/tutorials/components/EditTokenModal.tsx index 9d5774dd572..f60578becaa 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/EditTokenModal.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/EditTokenModal.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 { generateToken, getTokens, revokeToken } from '../../../api/user-tokens'; import { Button, DeleteButton } from '../../../components/controls/buttons'; import { ClipboardIconButton } from '../../../components/controls/clipboard'; diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/PreRequisitesStep.tsx b/server/sonar-web/src/main/js/components/tutorials/jenkins/PreRequisitesStep.tsx index 0f391b0bf53..6e068484065 100644 --- a/server/sonar-web/src/main/js/components/tutorials/jenkins/PreRequisitesStep.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/PreRequisitesStep.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 { rawSizes } from '../../../app/theme'; import { Button } from '../../../components/controls/buttons'; import ChevronRightIcon from '../../../components/icons/ChevronRightIcon'; diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/__snapshots__/PreRequisitesStep-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/__snapshots__/PreRequisitesStep-test.tsx.snap index 33d44479d74..091b72035d8 100644 --- a/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/__snapshots__/PreRequisitesStep-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/__snapshots__/PreRequisitesStep-test.tsx.snap @@ -47,8 +47,6 @@ exports[`should render correctly: content 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/jenkins/" > @@ -109,8 +107,6 @@ exports[`should render correctly: content for branches disabled 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/jenkins/" > @@ -174,8 +170,6 @@ exports[`should render correctly: content for branches disabled, gitlab 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/jenkins/" > diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/DoneNextSteps.tsx b/server/sonar-web/src/main/js/components/tutorials/manual/DoneNextSteps.tsx index 4cfcf0dc1b0..df037f76a29 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/DoneNextSteps.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/manual/DoneNextSteps.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 { translate } from '../../../helpers/l10n'; import { Component } from '../../../types/types'; diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/TokenStep.tsx b/server/sonar-web/src/main/js/components/tutorials/manual/TokenStep.tsx index c9ba982bada..e0f47ce2425 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/TokenStep.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/manual/TokenStep.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 { generateToken, getTokens, revokeToken } from '../../../api/user-tokens'; import { Button, DeleteButton, SubmitButton } from '../../../components/controls/buttons'; import Radio from '../../../components/controls/Radio'; diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/__tests__/__snapshots__/DoneNextSteps-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/manual/__tests__/__snapshots__/DoneNextSteps-test.tsx.snap index 782a7ba254b..31694bcce7b 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/__tests__/__snapshots__/DoneNextSteps-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/manual/__tests__/__snapshots__/DoneNextSteps-test.tsx.snap @@ -26,18 +26,14 @@ exports[`should render correctly: default 1`] = ` values={ Object { "link_branches": <Link - onlyActiveOnIndex={false} rel="noopener noreferrer" - style={Object {}} target="_blank" to="/documentation/branches/overview/" > onboarding.analysis.auto_refresh_after_analysis.check_these_links.branches </Link>, "link_pr_analysis": <Link - onlyActiveOnIndex={false} rel="noopener noreferrer" - style={Object {}} target="_blank" to="/documentation/analysis/pull-request/" > @@ -76,18 +72,14 @@ exports[`should render correctly: project admin 1`] = ` values={ Object { "link_branches": <Link - onlyActiveOnIndex={false} rel="noopener noreferrer" - style={Object {}} target="_blank" to="/documentation/branches/overview/" > onboarding.analysis.auto_refresh_after_analysis.check_these_links.branches </Link>, "link_pr_analysis": <Link - onlyActiveOnIndex={false} rel="noopener noreferrer" - style={Object {}} target="_blank" to="/documentation/analysis/pull-request/" > diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/__tests__/__snapshots__/TokenStep-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/manual/__tests__/__snapshots__/TokenStep-test.tsx.snap index 269c233855d..e5217b8c6f2 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/__tests__/__snapshots__/TokenStep-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/manual/__tests__/__snapshots__/TokenStep-test.tsx.snap @@ -97,8 +97,6 @@ exports[`generates token 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/account/security" > @@ -207,8 +205,6 @@ exports[`generates token 2`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/account/security" > @@ -273,8 +269,6 @@ exports[`generates token 3`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/account/security" > @@ -349,8 +343,6 @@ exports[`revokes token 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/account/security" > @@ -424,8 +416,6 @@ exports[`revokes token 2`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/account/security" > @@ -547,8 +537,6 @@ exports[`revokes token 3`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/account/security" > diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/commands/DotNetExecute.tsx b/server/sonar-web/src/main/js/components/tutorials/manual/commands/DotNetExecute.tsx index e3b2906cc0c..b2840c55b8e 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/commands/DotNetExecute.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/manual/commands/DotNetExecute.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 { translate } from '../../../../helpers/l10n'; import { Component } from '../../../../types/types'; import CodeSnippet from '../../../common/CodeSnippet'; diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/commands/ExecBuildWrapper.tsx b/server/sonar-web/src/main/js/components/tutorials/manual/commands/ExecBuildWrapper.tsx index 233cfcf0c19..a359e2bae1a 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/commands/ExecBuildWrapper.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/manual/commands/ExecBuildWrapper.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 { translate } from '../../../../helpers/l10n'; import CodeSnippet from '../../../common/CodeSnippet'; import { OSs } from '../../types'; diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/commands/ExecScanner.tsx b/server/sonar-web/src/main/js/components/tutorials/manual/commands/ExecScanner.tsx index 19751ebba7c..3bcb8b538bf 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/commands/ExecScanner.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/manual/commands/ExecScanner.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 { translate } from '../../../../helpers/l10n'; import { Component } from '../../../../types/types'; import CodeSnippet from '../../../common/CodeSnippet'; diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/commands/JavaGradle.tsx b/server/sonar-web/src/main/js/components/tutorials/manual/commands/JavaGradle.tsx index 854d76fdb1b..b640f69b70a 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/commands/JavaGradle.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/manual/commands/JavaGradle.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 { translate } from '../../../../helpers/l10n'; import { Component } from '../../../../types/types'; import CodeSnippet from '../../../common/CodeSnippet'; diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/commands/JavaMaven.tsx b/server/sonar-web/src/main/js/components/tutorials/manual/commands/JavaMaven.tsx index 8477b611393..f56b9be740e 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/commands/JavaMaven.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/manual/commands/JavaMaven.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 { translate } from '../../../../helpers/l10n'; import { Component } from '../../../../types/types'; import CodeSnippet from '../../../common/CodeSnippet'; diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/DotNetExecute-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/DotNetExecute-test.tsx.snap index 7ff51cbcb67..a921c78ff66 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/DotNetExecute-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/DotNetExecute-test.tsx.snap @@ -29,8 +29,6 @@ exports[`should render correctly 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/scan/sonarscanner-for-msbuild/" > diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/ExecBuildWrapper-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/ExecBuildWrapper-test.tsx.snap index 7e7f188ffe3..68e585ff320 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/ExecBuildWrapper-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/ExecBuildWrapper-test.tsx.snap @@ -24,8 +24,6 @@ exports[`Shoud renders for "linux" correctly 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/languages/cfamily/" > @@ -62,8 +60,6 @@ exports[`Shoud renders for "mac" correctly 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/languages/cfamily/" > @@ -100,8 +96,6 @@ exports[`Shoud renders for "win" correctly 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/languages/cfamily/" > diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/ExecScanner-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/ExecScanner-test.tsx.snap index b48bd7b66eb..c979aedbd8a 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/ExecScanner-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/ExecScanner-test.tsx.snap @@ -34,8 +34,6 @@ exports[`should render correctly for "linux" 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/scan/sonarscanner/" > @@ -106,8 +104,6 @@ exports[`should render correctly for "mac" 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/scan/sonarscanner/" > @@ -178,8 +174,6 @@ exports[`should render correctly for "win" 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/scan/sonarscanner/" > @@ -250,8 +244,6 @@ exports[`should render correctly for cfamily 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/scan/sonarscanner/" > diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap index ca9adca4c17..fc132dddb80 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap @@ -29,8 +29,6 @@ exports[`renders correctly 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/scan/sonarscanner-for-gradle/" > @@ -65,8 +63,6 @@ exports[`renders correctly 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/scan/sonarscanner-for-gradle/" > diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap index 741cf44262a..970c7f620cc 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/manual/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap @@ -33,8 +33,6 @@ exports[`renders correctly 1`] = ` values={ Object { "link": <Link - onlyActiveOnIndex={false} - style={Object {}} target="_blank" to="/documentation/analysis/scan/sonarscanner-for-maven/" > diff --git a/server/sonar-web/src/main/js/helpers/__tests__/handleRequiredAuthentication-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/handleRequiredAuthentication-test.ts index e27c6f2465a..48ce7ea3071 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/handleRequiredAuthentication-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/handleRequiredAuthentication-test.ts @@ -17,14 +17,34 @@ * 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 '../getHistory'; import handleRequiredAuthentication from '../handleRequiredAuthentication'; -jest.mock('../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 }); handleRequiredAuthentication(); - expect(replace).toBeCalledWith(expect.objectContaining({ pathname: '/sessions/new' })); + expect(replace).toBeCalledWith('/sessions/new?return_to=%2Fpath%3Fid%3D12%23tag'); }); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts index bdcb878c1b3..a881a16e4a1 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts @@ -22,14 +22,18 @@ import { ComponentQualifier } from '../../types/component'; import { IssueType } from '../../types/issues'; import { SecurityStandard } from '../../types/security'; import { mockBranch, mockMainBranch, mockPullRequest } from '../mocks/branch-like'; +import { mockLocation } from '../testMocks'; import { CodeScope, convertGithubApiUrlToLink, + convertToTo, + getComponentAdminUrl, getComponentDrilldownUrl, getComponentDrilldownUrlWithSelection, getComponentIssuesUrl, getComponentOverviewUrl, getComponentSecurityHotspotsUrl, + getDeprecatedActiveRulesUrl, getGlobalSettingsUrl, getIssuesUrl, getPathUrlAsString, @@ -38,6 +42,8 @@ import { getQualityGateUrl, getReturnUrl, isRelativeUrl, + queryToSearch, + searchParamsToQuery, stripTrailingSlash } from '../urls'; @@ -62,28 +68,55 @@ describe('#stripTrailingSlash', () => { }); }); +describe('getComponentAdminUrl', () => { + it.each([ + [ + 'Portfolio', + ComponentQualifier.Portfolio, + { pathname: '/project/admin/extension/governance/console', search: '?id=key&qualifier=VW' } + ], + [ + 'Application', + ComponentQualifier.Application, + { + pathname: '/project/admin/extension/developer-server/application-console', + search: '?id=key' + } + ], + ['Project', ComponentQualifier.Project, { pathname: '/dashboard', search: '?id=key' }] + ])('should work for %s', (_qualifierName, qualifier, result) => { + expect(getComponentAdminUrl('key', qualifier)).toEqual(result); + }); +}); + describe('#getComponentIssuesUrl', () => { it('should work without parameters', () => { - expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, {})).toEqual({ - pathname: '/project/issues', - query: { id: SIMPLE_COMPONENT_KEY } - }); + expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY)).toEqual( + expect.objectContaining({ + pathname: '/project/issues', + search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }) + }) + ); }); it('should work with parameters', () => { - expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, { resolved: 'false' })).toEqual({ - pathname: '/project/issues', - query: { id: SIMPLE_COMPONENT_KEY, resolved: 'false' } - }); + expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, { resolved: 'false' })).toEqual( + expect.objectContaining({ + pathname: '/project/issues', + search: queryToSearch({ resolved: 'false', id: SIMPLE_COMPONENT_KEY }) + }) + ); }); }); describe('#getComponentSecurityHotspotsUrl', () => { it('should work with no extra parameters', () => { - expect(getComponentSecurityHotspotsUrl(SIMPLE_COMPONENT_KEY, {})).toEqual({ - pathname: '/security_hotspots', - query: { id: SIMPLE_COMPONENT_KEY } - }); + expect(getComponentSecurityHotspotsUrl(SIMPLE_COMPONENT_KEY)).toEqual( + expect.objectContaining({ + pathname: '/security_hotspots', + search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }) + }) + ); }); it('should forward some query parameters', () => { @@ -97,39 +130,47 @@ describe('#getComponentSecurityHotspotsUrl', () => { [SecurityStandard.SONARSOURCE]: 'a1', ignoredParam: '1234' }) - ).toEqual({ - pathname: '/security_hotspots', - query: { - id: SIMPLE_COMPONENT_KEY, - [SecurityStandard.OWASP_TOP10_2021]: 'a1', - [SecurityStandard.CWE]: 'a1', - [SecurityStandard.OWASP_TOP10]: 'a1', - [SecurityStandard.SANS_TOP25]: 'a1', - [SecurityStandard.SONARSOURCE]: 'a1', - sinceLeakPeriod: 'true' - } - }); + ).toEqual( + expect.objectContaining({ + pathname: '/security_hotspots', + search: queryToSearch({ + id: SIMPLE_COMPONENT_KEY, + sinceLeakPeriod: 'true', + [SecurityStandard.OWASP_TOP10_2021]: 'a1', + [SecurityStandard.SONARSOURCE]: 'a1', + [SecurityStandard.OWASP_TOP10]: 'a1', + [SecurityStandard.SANS_TOP25]: 'a1', + [SecurityStandard.CWE]: 'a1' + }) + }) + ); }); }); describe('#getComponentOverviewUrl', () => { it('should return a portfolio url for a portfolio', () => { - expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Portfolio)).toEqual({ - pathname: '/portfolio', - query: { id: SIMPLE_COMPONENT_KEY } - }); + expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Portfolio)).toEqual( + expect.objectContaining({ + pathname: '/portfolio', + search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }) + }) + ); }); it('should return a portfolio url for a subportfolio', () => { - expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.SubPortfolio)).toEqual({ - pathname: '/portfolio', - query: { id: SIMPLE_COMPONENT_KEY } - }); + expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.SubPortfolio)).toEqual( + expect.objectContaining({ + pathname: '/portfolio', + search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }) + }) + ); }); it('should return a dashboard url for a project', () => { - expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Project)).toEqual({ - pathname: '/dashboard', - query: { id: SIMPLE_COMPONENT_KEY } - }); + expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Project)).toEqual( + expect.objectContaining({ + pathname: '/dashboard', + search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }) + }) + ); }); it('should return correct dashboard url for a project when navigating from new code', () => { expect( @@ -139,10 +180,12 @@ describe('#getComponentOverviewUrl', () => { undefined, CodeScope.New ) - ).toEqual({ - pathname: '/dashboard', - query: { id: SIMPLE_COMPONENT_KEY, code_scope: 'new' } - }); + ).toEqual( + expect.objectContaining({ + pathname: '/dashboard', + search: queryToSearch({ id: SIMPLE_COMPONENT_KEY, code_scope: 'new' }) + }) + ); }); it('should return correct dashboard url for a project when navigating from overall code', () => { expect( @@ -152,16 +195,20 @@ describe('#getComponentOverviewUrl', () => { undefined, CodeScope.Overall ) - ).toEqual({ - pathname: '/dashboard', - query: { id: SIMPLE_COMPONENT_KEY, code_scope: 'overall' } - }); + ).toEqual( + expect.objectContaining({ + pathname: '/dashboard', + search: queryToSearch({ id: SIMPLE_COMPONENT_KEY, code_scope: 'overall' }) + }) + ); }); it('should return a dashboard url for an app', () => { - expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Application)).toEqual({ - pathname: '/dashboard', - query: { id: SIMPLE_COMPONENT_KEY } - }); + expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Application)).toEqual( + expect.objectContaining({ + pathname: '/dashboard', + search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }) + }) + ); }); }); @@ -169,28 +216,34 @@ describe('#getComponentDrilldownUrl', () => { it('should return component drilldown url', () => { expect( getComponentDrilldownUrl({ componentKey: SIMPLE_COMPONENT_KEY, metric: METRIC }) - ).toEqual({ - pathname: '/component_measures', - query: { id: SIMPLE_COMPONENT_KEY, metric: METRIC } - }); + ).toEqual( + expect.objectContaining({ + pathname: '/component_measures', + search: queryToSearch({ id: SIMPLE_COMPONENT_KEY, metric: METRIC }) + }) + ); }); it('should not encode component key', () => { expect( getComponentDrilldownUrl({ componentKey: COMPLEX_COMPONENT_KEY, metric: METRIC }) - ).toEqual({ - pathname: '/component_measures', - query: { id: COMPLEX_COMPONENT_KEY, metric: METRIC } - }); + ).toEqual( + expect.objectContaining({ + pathname: '/component_measures', + search: queryToSearch({ id: COMPLEX_COMPONENT_KEY, metric: METRIC }) + }) + ); }); it('should add asc param only when its list view', () => { expect( getComponentDrilldownUrl({ componentKey: SIMPLE_COMPONENT_KEY, metric: METRIC, asc: false }) - ).toEqual({ - pathname: '/component_measures', - query: { id: SIMPLE_COMPONENT_KEY, metric: METRIC } - }); + ).toEqual( + expect.objectContaining({ + pathname: '/component_measures', + search: queryToSearch({ id: SIMPLE_COMPONENT_KEY, metric: METRIC }) + }) + ); expect( getComponentDrilldownUrl({ @@ -199,10 +252,17 @@ describe('#getComponentDrilldownUrl', () => { listView: true, asc: false }) - ).toEqual({ - pathname: '/component_measures', - query: { id: SIMPLE_COMPONENT_KEY, metric: METRIC, asc: 'false', view: 'list' } - }); + ).toEqual( + expect.objectContaining({ + pathname: '/component_measures', + search: queryToSearch({ + id: SIMPLE_COMPONENT_KEY, + metric: METRIC, + view: 'list', + asc: 'false' + }) + }) + ); }); }); @@ -210,10 +270,16 @@ describe('#getComponentDrilldownUrlWithSelection', () => { it('should return component drilldown url with selection', () => { expect( getComponentDrilldownUrlWithSelection(SIMPLE_COMPONENT_KEY, COMPLEX_COMPONENT_KEY, METRIC) - ).toEqual({ - pathname: '/component_measures', - query: { id: SIMPLE_COMPONENT_KEY, metric: METRIC, selected: COMPLEX_COMPONENT_KEY } - }); + ).toEqual( + expect.objectContaining({ + pathname: '/component_measures', + search: queryToSearch({ + id: SIMPLE_COMPONENT_KEY, + metric: METRIC, + selected: COMPLEX_COMPONENT_KEY + }) + }) + ); }); it('should return component drilldown url with branchLike', () => { @@ -224,15 +290,17 @@ describe('#getComponentDrilldownUrlWithSelection', () => { METRIC, mockBranch({ name: 'foo' }) ) - ).toEqual({ - pathname: '/component_measures', - query: { - id: SIMPLE_COMPONENT_KEY, - metric: METRIC, - selected: COMPLEX_COMPONENT_KEY, - branch: 'foo' - } - }); + ).toEqual( + expect.objectContaining({ + pathname: '/component_measures', + search: queryToSearch({ + id: SIMPLE_COMPONENT_KEY, + metric: METRIC, + branch: 'foo', + selected: COMPLEX_COMPONENT_KEY + }) + }) + ); }); it('should return component drilldown url with view parameter', () => { @@ -244,15 +312,17 @@ describe('#getComponentDrilldownUrlWithSelection', () => { undefined, 'list' ) - ).toEqual({ - pathname: '/component_measures', - query: { - id: SIMPLE_COMPONENT_KEY, - metric: METRIC, - selected: COMPLEX_COMPONENT_KEY, - view: 'list' - } - }); + ).toEqual( + expect.objectContaining({ + pathname: '/component_measures', + search: queryToSearch({ + id: SIMPLE_COMPONENT_KEY, + metric: METRIC, + view: 'list', + selected: COMPLEX_COMPONENT_KEY + }) + }) + ); expect( getComponentDrilldownUrlWithSelection( @@ -262,15 +332,17 @@ describe('#getComponentDrilldownUrlWithSelection', () => { mockMainBranch(), 'treemap' ) - ).toEqual({ - pathname: '/component_measures', - query: { - id: SIMPLE_COMPONENT_KEY, - metric: METRIC, - selected: COMPLEX_COMPONENT_KEY, - view: 'treemap' - } - }); + ).toEqual( + expect.objectContaining({ + pathname: '/component_measures', + search: queryToSearch({ + id: SIMPLE_COMPONENT_KEY, + metric: METRIC, + view: 'treemap', + selected: COMPLEX_COMPONENT_KEY + }) + }) + ); expect( getComponentDrilldownUrlWithSelection( @@ -280,14 +352,31 @@ describe('#getComponentDrilldownUrlWithSelection', () => { mockPullRequest({ key: '1' }), 'tree' ) - ).toEqual({ - pathname: '/component_measures', - query: { - id: SIMPLE_COMPONENT_KEY, - metric: METRIC, - selected: COMPLEX_COMPONENT_KEY, - pullRequest: '1' - } + ).toEqual( + expect.objectContaining({ + pathname: '/component_measures', + search: queryToSearch({ + id: SIMPLE_COMPONENT_KEY, + metric: METRIC, + pullRequest: '1', + selected: COMPLEX_COMPONENT_KEY + }) + }) + ); + }); +}); + +describe('getDeprecatedActiveRulesUrl', () => { + it('should include query params', () => { + expect(getDeprecatedActiveRulesUrl({ languages: 'js' })).toEqual({ + pathname: '/coding_rules', + search: '?languages=js&activation=true&statuses=DEPRECATED' + }); + }); + it('should handle empty query', () => { + expect(getDeprecatedActiveRulesUrl()).toEqual({ + pathname: '/coding_rules', + search: '?activation=true&statuses=DEPRECATED' }); }); }); @@ -304,7 +393,7 @@ describe('#getIssuesUrl', () => { const type = IssueType.Bug; expect(getIssuesUrl({ type })).toEqual({ pathname: '/issues', - query: { type } + search: queryToSearch({ type }) }); }); }); @@ -313,11 +402,11 @@ describe('#getGlobalSettingsUrl', () => { it('should work as expected', () => { expect(getGlobalSettingsUrl('foo')).toEqual({ pathname: '/admin/settings', - query: { category: 'foo' } + search: queryToSearch({ category: 'foo' }) }); expect(getGlobalSettingsUrl('foo', { alm: AlmKeys.GitHub })).toEqual({ pathname: '/admin/settings', - query: { category: 'foo', alm: AlmKeys.GitHub } + search: queryToSearch({ category: 'foo', alm: AlmKeys.GitHub }) }); }); }); @@ -326,11 +415,11 @@ describe('#getProjectSettingsUrl', () => { it('should work as expected', () => { expect(getProjectSettingsUrl('foo')).toEqual({ pathname: '/project/settings', - query: { id: 'foo' } + search: queryToSearch({ id: 'foo' }) }); expect(getProjectSettingsUrl('foo', 'bar')).toEqual({ pathname: '/project/settings', - query: { id: 'foo', category: 'bar' } + search: queryToSearch({ id: 'foo', category: 'bar' }) }); }); }); @@ -338,15 +427,25 @@ describe('#getProjectSettingsUrl', () => { describe('#getPathUrlAsString', () => { it('should return component url', () => { expect( - getPathUrlAsString({ pathname: '/dashboard', query: { id: SIMPLE_COMPONENT_KEY } }) + getPathUrlAsString({ + pathname: '/dashboard', + search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }) + }) ).toBe('/dashboard?id=' + SIMPLE_COMPONENT_KEY); }); it('should encode component key', () => { expect( - getPathUrlAsString({ pathname: '/dashboard', query: { id: COMPLEX_COMPONENT_KEY } }) + getPathUrlAsString({ + pathname: '/dashboard', + search: queryToSearch({ id: COMPLEX_COMPONENT_KEY }) + }) ).toBe('/dashboard?id=' + COMPLEX_COMPONENT_KEY_ENCODED); }); + + it('should handle partial arguments', () => { + expect(getPathUrlAsString({}, true)).toBe('/'); + }); }); describe('#getReturnUrl', () => { @@ -394,3 +493,52 @@ describe('#getHostUrl', () => { ); }); }); + +describe('searchParamsToQuery', () => { + it('should handle arrays and single params', () => { + const searchParams = new URLSearchParams([ + ['a', 'v1'], + ['a', 'v2'], + ['b', 'awesome'], + ['a', 'v3'] + ]); + + const result = searchParamsToQuery(searchParams); + + expect(result).toEqual({ a: ['v1', 'v2', 'v3'], b: 'awesome' }); + }); +}); + +describe('queryToSearch', () => { + it('should handle all types', () => { + const query = { + author: ['GRRM', 'JKR', 'Stross'], + b1: true, + b2: false, + emptyArray: [], + normalString: 'hello', + undef: undefined + }; + + expect(queryToSearch(query)).toBe( + '?b1=true&b2=false&normalString=hello&author=GRRM&author=JKR&author=Stross' + ); + }); + + it('should handle an missing query', () => { + expect(queryToSearch()).toBe('?'); + }); +}); + +describe('convertToTo', () => { + it('should handle locations with a query', () => { + expect(convertToTo(mockLocation({ pathname: '/account', query: { id: 1 } }))).toEqual({ + pathname: '/account', + search: '?id=1' + }); + }); + + it('should forward strings', () => { + expect(convertToTo('/whatever')).toBe('/whatever'); + }); +}); diff --git a/server/sonar-web/src/main/js/helpers/branch-like.ts b/server/sonar-web/src/main/js/helpers/branch-like.ts index 8df94c21614..261bea49cae 100644 --- a/server/sonar-web/src/main/js/helpers/branch-like.ts +++ b/server/sonar-web/src/main/js/helpers/branch-like.ts @@ -120,9 +120,8 @@ export function getBranchLikeQuery(branchLike?: BranchLike): BranchParameters { return { branch: branchLike.name }; } else if (isPullRequest(branchLike)) { return { pullRequest: branchLike.key }; - } else { - return {}; } + return {}; } // Create branch object from branch name or pull request key diff --git a/server/sonar-web/src/main/js/helpers/handleRequiredAuthentication.ts b/server/sonar-web/src/main/js/helpers/handleRequiredAuthentication.ts index 75a046a24f9..fbf9c568551 100644 --- a/server/sonar-web/src/main/js/helpers/handleRequiredAuthentication.ts +++ b/server/sonar-web/src/main/js/helpers/handleRequiredAuthentication.ts @@ -17,11 +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 './getHistory'; - export default function handleRequiredAuthentication() { - const history = getHistory(); const returnTo = window.location.pathname + window.location.search + window.location.hash; - // eslint-disable-next-line camelcase - history.replace({ pathname: '/sessions/new', query: { return_to: returnTo } }); + const searchParams = new URLSearchParams({ return_to: returnTo }); + window.location.replace(`/sessions/new?${searchParams.toString()}`); } diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 1875185fa23..a71f5cb3a08 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -17,10 +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 { Location, LocationDescriptor } from 'history'; -import { InjectedRouter } from 'react-router'; +import { To } from 'react-router-dom'; import { DocumentationEntry } from '../apps/documentation/utils'; import { Exporter, Profile } from '../apps/quality-profiles/types'; +import { Location, Router } from '../components/hoc/withRouter'; import { AppState } from '../types/appstate'; import { RuleRepository } from '../types/coding-rules'; import { EditionKey } from '../types/editions'; @@ -402,7 +402,6 @@ export function mockIssue(withLocations = false, overrides: Partial<Issue> = {}) export function mockLocation(overrides: Partial<Location> = {}): Location { return { - action: 'PUSH', hash: '', key: 'key', pathname: '/path', @@ -518,8 +517,8 @@ export function mockQualityProfileExporter(override?: Partial<Exporter>): Export export function mockRouter( overrides: { - push?: (loc: LocationDescriptor) => void; - replace?: (loc: LocationDescriptor) => void; + push?: (loc: To) => void; + replace?: (loc: To) => void; } = {} ) { return { @@ -533,7 +532,7 @@ export function mockRouter( replace: jest.fn(), setRouteLeaveHook: jest.fn(), ...overrides - } as InjectedRouter; + } as Router; } export function mockRule(overrides: Partial<Rule> = {}): Rule { diff --git a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx index 6cbbe7ee4ce..840ade86c20 100644 --- a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx +++ b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx @@ -18,26 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { render, RenderResult } from '@testing-library/react'; -import { History } from 'history'; import * as React from 'react'; import { HelmetProvider } from 'react-helmet-async'; import { IntlProvider } from 'react-intl'; -import { - createMemoryHistory, - Route, - RouteComponent, - RouteConfig, - Router, - withRouter, - WithRouterProps -} from 'react-router'; + +import { MemoryRouter, Outlet, parsePath, Route, Routes } from 'react-router-dom'; import AdminContext from '../app/components/AdminContext'; import AppStateContextProvider from '../app/components/app-state/AppStateContextProvider'; import CurrentUserContextProvider from '../app/components/current-user/CurrentUserContextProvider'; import GlobalMessagesContainer from '../app/components/GlobalMessagesContainer'; +import IndexationContextProvider from '../app/components/indexation/IndexationContextProvider'; import { LanguagesContext } from '../app/components/languages/LanguagesContext'; import { MetricsContext } from '../app/components/metrics/MetricsContext'; -import { RouteWithChildRoutes } from '../app/utils/startReactApp'; +import { useLocation } from '../components/hoc/withRouter'; import { AppState } from '../types/appstate'; import { Dict, Extension, Languages, Metric, SysStatus } from '../types/types'; import { CurrentUser } from '../types/users'; @@ -46,7 +39,6 @@ import { mockAppState, mockCurrentUser } from './testMocks'; interface RenderContext { metrics?: Dict<Metric>; - history?: History; appState?: AppState; languages?: Languages; currentUser?: CurrentUser; @@ -55,11 +47,11 @@ interface RenderContext { export function renderAdminApp( indexPath: string, - routes: RouteConfig, + routes: () => JSX.Element, context: RenderContext = {}, overrides: { systemStatus?: SysStatus; adminPages?: Extension[] } = {} ): RenderResult { - function MockAdminContainer(props: { children: React.ReactElement }) { + function MockAdminContainer() { return ( <AdminContext.Provider value={{ @@ -72,29 +64,33 @@ export function renderAdminApp( pendingPlugins: { installing: [], removing: [], updating: [] }, systemStatus: overrides.systemStatus ?? 'UP' }}> - {React.cloneElement(props.children, { - adminPages: overrides.adminPages ?? [] - })} + <Outlet + context={{ + adminPages: overrides.adminPages ?? [] + }} + /> </AdminContext.Provider> ); } - const innerPath = indexPath.split('admin/').pop(); - return renderRoutedApp( - <Route component={MockAdminContainer} path="admin"> - <RouteWithChildRoutes path={innerPath} childRoutes={routes} /> + <Route element={<MockAdminContainer />} path="admin"> + {routes()} </Route>, indexPath, context ); } -export function renderComponent(component: React.ReactElement) { +export function renderComponent(component: React.ReactElement, pathname = '/') { function Wrapper({ children }: { children: React.ReactElement }) { return ( <IntlProvider defaultLocale="en" locale="en"> - {children} + <MemoryRouter initialEntries={[pathname]}> + <Routes> + <Route path="*" element={children} /> + </Routes> + </MemoryRouter> </IntlProvider> ); } @@ -104,31 +100,25 @@ export function renderComponent(component: React.ReactElement) { export function renderComponentApp( indexPath: string, - component: RouteComponent, + component: JSX.Element, context: RenderContext = {} ): RenderResult { - return renderRoutedApp(<Route path={indexPath} component={component} />, indexPath, context); + return renderRoutedApp(<Route path={indexPath} element={component} />, indexPath, context); } export function renderApp( indexPath: string, - routes: RouteConfig, + routes: () => JSX.Element, context?: RenderContext ): RenderResult { - return renderRoutedApp( - <RouteWithChildRoutes path={indexPath} childRoutes={routes} />, - indexPath, - context - ); + return renderRoutedApp(routes(), indexPath, context); } -const CatchAll = withRouter((props: WithRouterProps) => { - return ( - <div>{`${props.location.pathname}?${new URLSearchParams( - props.location.query - ).toString()}`}</div> - ); -}); +export function CatchAll() { + const location = useLocation(); + + return <div>{`${location.pathname}${location.search}`}</div>; +} function renderRoutedApp( children: React.ReactElement, @@ -138,11 +128,12 @@ function renderRoutedApp( navigateTo = indexPath, metrics = DEFAULT_METRICS, appState = mockAppState(), - history = createMemoryHistory(), languages = {} }: RenderContext = {} ): RenderResult { - history.push(`/${navigateTo}`); + const path = parsePath(navigateTo); + path.pathname = `/${path.pathname}`; + return render( <HelmetProvider context={{}}> <IntlProvider defaultLocale="en" locale="en"> @@ -150,11 +141,15 @@ function renderRoutedApp( <LanguagesContext.Provider value={languages}> <CurrentUserContextProvider currentUser={currentUser}> <AppStateContextProvider appState={appState}> - <GlobalMessagesContainer /> - <Router history={history}> - {children} - <Route path="*" component={CatchAll} /> - </Router> + <IndexationContextProvider> + <GlobalMessagesContainer /> + <MemoryRouter initialEntries={[path]}> + <Routes> + {children} + <Route path="*" element={<CatchAll />} /> + </Routes> + </MemoryRouter> + </IndexationContextProvider> </AppStateContextProvider> </CurrentUserContextProvider> </LanguagesContext.Provider> diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index 38013879e05..6dbf21e8d9e 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -17,14 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { isNil, omitBy, pick } from 'lodash'; +import { isArray, mapValues, omitBy, pick } from 'lodash'; +import { Path, To } from 'react-router-dom'; import { getProfilePath } from '../apps/quality-profiles/utils'; import { BranchLike, BranchParameters } from '../types/branch-like'; import { ComponentQualifier, isApplication, isPortfolioLike } from '../types/component'; import { MeasurePageView } from '../types/measures'; import { GraphType } from '../types/project-activity'; import { SecurityStandard } from '../types/security'; -import { Dict } from '../types/types'; +import { Dict, RawQuery } from '../types/types'; import { HomePage } from '../types/users'; import { getBranchLikeQuery, isBranch, isMainBranch, isPullRequest } from './branch-like'; import { IS_SSR } from './browser'; @@ -47,6 +48,35 @@ type Query = Location['query']; const PROJECT_BASE_URL = '/dashboard'; +export function queryToSearch(query: RawQuery = {}) { + const arrayParams: Array<{ key: string; values: string[] }> = []; + + const stringParams = mapValues(query, (value, key) => { + // array values are added afterwards + if (isArray(value)) { + arrayParams.push({ key, values: value }); + return ''; + } + + return value != null ? `${value}` : ''; + }); + const filteredParams = omitBy(stringParams, (v: string) => v.length === 0); + const searchParams = new URLSearchParams(filteredParams); + + /* + * Add each value separately + * e.g. author: ['a', 'b'] should be serialized as + * author=a&author=b + */ + arrayParams.forEach(({ key, values }) => { + values.forEach(value => { + searchParams.append(key, value); + }); + }); + + return `?${searchParams.toString()}`; +} + export function getComponentOverviewUrl( componentKey: string, componentQualifier: ComponentQualifier | string, @@ -66,19 +96,18 @@ export function getComponentAdminUrl( return getPortfolioAdminUrl(componentKey); } else if (isApplication(componentQualifier)) { return getApplicationAdminUrl(componentKey); - } else { - return getProjectUrl(componentKey); } + return getProjectUrl(componentKey); } export function getProjectUrl( project: string, branch?: string, codeScope?: CodeScopeType -): Location { +): Partial<Path> { return { pathname: PROJECT_BASE_URL, - query: { id: project, branch, ...(codeScope && { code_scope: codeScope }) } + search: queryToSearch({ id: project, branch, ...(codeScope && { code_scope: codeScope }) }) }; } @@ -86,32 +115,32 @@ export function getProjectQueryUrl( project: string, branchParameters?: BranchParameters, codeScope?: CodeScopeType -): Location { +): To { return { pathname: PROJECT_BASE_URL, - query: { + search: queryToSearch({ id: project, ...branchParameters, ...(codeScope && { code_scope: codeScope }) - } + }) }; } -export function getPortfolioUrl(key: string): Location { - return { pathname: '/portfolio', query: { id: key } }; +export function getPortfolioUrl(key: string): To { + return { pathname: '/portfolio', search: queryToSearch({ id: key }) }; } -export function getPortfolioAdminUrl(key: string) { +export function getPortfolioAdminUrl(key: string): To { return { pathname: '/project/admin/extension/governance/console', - query: { id: key, qualifier: ComponentQualifier.Portfolio } + search: queryToSearch({ id: key, qualifier: ComponentQualifier.Portfolio }) }; } -export function getApplicationAdminUrl(key: string) { +export function getApplicationAdminUrl(key: string): To { return { pathname: '/project/admin/extension/developer-server/application-console', - query: { id: key } + search: queryToSearch({ id: key }) }; } @@ -119,51 +148,58 @@ export function getComponentBackgroundTaskUrl( componentKey: string, status?: string, taskType?: string -): Location { - return { pathname: '/project/background_tasks', query: { id: componentKey, status, taskType } }; +): Path { + return { + pathname: '/project/background_tasks', + search: queryToSearch({ id: componentKey, status, taskType }), + hash: '' + }; } -export function getBranchLikeUrl(project: string, branchLike?: BranchLike): Location { +export function getBranchLikeUrl(project: string, branchLike?: BranchLike): Partial<Path> { if (isPullRequest(branchLike)) { return getPullRequestUrl(project, branchLike.key); } else if (isBranch(branchLike) && !isMainBranch(branchLike)) { return getBranchUrl(project, branchLike.name); - } else { - return getProjectUrl(project); } + return getProjectUrl(project); } -export function getBranchUrl(project: string, branch: string): Location { - return { pathname: PROJECT_BASE_URL, query: { branch, id: project } }; +export function getBranchUrl(project: string, branch: string): Partial<Path> { + return { pathname: PROJECT_BASE_URL, search: queryToSearch({ branch, id: project }) }; } -export function getPullRequestUrl(project: string, pullRequest: string): Location { - return { pathname: PROJECT_BASE_URL, query: { id: project, pullRequest } }; +export function getPullRequestUrl(project: string, pullRequest: string): Partial<Path> { + return { pathname: PROJECT_BASE_URL, search: queryToSearch({ id: project, pullRequest }) }; } /** * Generate URL for a global issues page */ -export function getIssuesUrl(query: Query): Location { +export function getIssuesUrl(query: Query): To { const pathname = '/issues'; - return { pathname, query }; + return { pathname, search: queryToSearch(query) }; } /** * Generate URL for a component's issues page */ -export function getComponentIssuesUrl(componentKey: string, query?: Query): Location { - return { pathname: '/project/issues', query: { ...(query || {}), id: componentKey } }; +export function getComponentIssuesUrl(componentKey: string, query?: Query): Path { + return { + pathname: '/project/issues', + search: queryToSearch({ ...(query || {}), id: componentKey }), + hash: '' + }; } /** * Generate URL for a component's security hotspot page */ -export function getComponentSecurityHotspotsUrl(componentKey: string, query: Query = {}): Location { +export function getComponentSecurityHotspotsUrl(componentKey: string, query: Query = {}): Path { const { branch, pullRequest, sinceLeakPeriod, hotspots, assignedToMe, file } = query; return { pathname: '/security_hotspots', - query: { + search: queryToSearch({ id: componentKey, branch, pullRequest, @@ -178,7 +214,8 @@ export function getComponentSecurityHotspotsUrl(componentKey: string, query: Que SecurityStandard.SANS_TOP25, SecurityStandard.CWE ]) - } + }), + hash: '' }; } @@ -193,7 +230,7 @@ export function getComponentDrilldownUrl(options: { treemapView?: boolean; listView?: boolean; asc?: boolean; -}): Location { +}): To { const { componentKey, metric, branchLike, selectionKey, treemapView, listView, asc } = options; const query: Query = { id: componentKey, metric, ...getBranchLikeQuery(branchLike) }; if (treemapView) { @@ -206,7 +243,7 @@ export function getComponentDrilldownUrl(options: { if (selectionKey) { query.selected = selectionKey; } - return { pathname: '/component_measures', query }; + return { pathname: '/component_measures', search: queryToSearch(query) }; } export function getComponentDrilldownUrlWithSelection( @@ -215,7 +252,7 @@ export function getComponentDrilldownUrlWithSelection( metric: string, branchLike?: BranchLike, view?: MeasurePageView -): Location { +): To { return getComponentDrilldownUrl({ componentKey, selectionKey, @@ -233,7 +270,7 @@ export function getMeasureTreemapUrl(componentKey: string, metric: string) { export function getActivityUrl(component: string, branchLike?: BranchLike, graph?: GraphType) { return { pathname: '/project/activity', - query: { id: component, graph, ...getBranchLikeQuery(branchLike) } + search: queryToSearch({ id: component, graph, ...getBranchLikeQuery(branchLike) }) }; } @@ -243,36 +280,36 @@ export function getActivityUrl(component: string, branchLike?: BranchLike, graph export function getMeasureHistoryUrl(component: string, metric: string, branchLike?: BranchLike) { return { pathname: '/project/activity', - query: { + search: queryToSearch({ id: component, graph: 'custom', custom_metrics: metric, ...getBranchLikeQuery(branchLike) - } + }) }; } /** * Generate URL for a component's permissions page */ -export function getComponentPermissionsUrl(componentKey: string): Location { - return { pathname: '/project_roles', query: { id: componentKey } }; +export function getComponentPermissionsUrl(componentKey: string): To { + return { pathname: '/project_roles', search: queryToSearch({ id: componentKey }) }; } /** * Generate URL for a quality profile */ -export function getQualityProfileUrl(name: string, language: string): Location { +export function getQualityProfileUrl(name: string, language: string): To { return getProfilePath(name, language); } -export function getQualityGateUrl(key: string): Location { +export function getQualityGateUrl(key: string): To { return { pathname: '/quality_gates/show/' + encodeURIComponent(key) }; } -export function getQualityGatesUrl(): Location { +export function getQualityGatesUrl(): To { return { pathname: '/quality_gates' }; @@ -281,31 +318,31 @@ export function getQualityGatesUrl(): Location { export function getGlobalSettingsUrl( category?: string, query?: Dict<string | undefined | number> -): Location { +): Partial<Path> { return { pathname: '/admin/settings', - query: { category, ...query } + search: queryToSearch({ category, ...query }) }; } -export function getProjectSettingsUrl(id: string, category?: string): Location { +export function getProjectSettingsUrl(id: string, category?: string): Partial<Path> { return { pathname: '/project/settings', - query: { id, category } + search: queryToSearch({ id, category }) }; } /** * Generate URL for the rules page */ -export function getRulesUrl(query: Query): Location { - return { pathname: '/coding_rules', query }; +export function getRulesUrl(query: Query): To { + return { pathname: '/coding_rules', search: queryToSearch(query) }; } /** * Generate URL for the rules page filtering only active deprecated rules */ -export function getDeprecatedActiveRulesUrl(query: Query = {}): Location { +export function getDeprecatedActiveRulesUrl(query: Query = {}): To { const baseQuery = { activation: 'true', statuses: 'DEPRECATED' }; return getRulesUrl({ ...query, ...baseQuery }); } @@ -323,10 +360,15 @@ export function getCodeUrl( branchLike?: BranchLike, selected?: string, line?: number -): Location { +): Partial<Path> { return { pathname: '/code', - query: { id: project, ...getBranchLikeQuery(branchLike), selected, line: line?.toFixed() } + search: queryToSearch({ + id: project, + ...getBranchLikeQuery(branchLike), + selected, + line: line?.toFixed() + }) }; } @@ -372,14 +414,13 @@ export function getHostUrl(): string { return window.location.origin + getBaseUrl(); } -export function getPathUrlAsString(path: Location, internal = true): string { - return `${internal ? getBaseUrl() : getHostUrl()}${path.pathname}?${new URLSearchParams( - omitBy(path.query, isNil) - ).toString()}`; +export function getPathUrlAsString(path: Partial<Path>, internal = true): string { + return `${internal ? getBaseUrl() : getHostUrl()}${path.pathname ?? '/'}${path.search ?? ''}`; } export function getReturnUrl(location: { hash?: string; query?: { return_to?: string } }) { const returnTo = location.query && location.query['return_to']; + if (isRelativeUrl(returnTo)) { return returnTo + (location.hash ? location.hash : ''); } @@ -390,3 +431,28 @@ export function isRelativeUrl(url?: string): boolean { const regex = new RegExp(/^\/[^/\\]/); return Boolean(url && regex.test(url)); } + +export function searchParamsToQuery(searchParams: URLSearchParams) { + const result: RawQuery = {}; + + searchParams.forEach((value, key) => { + if (result[key]) { + result[key] = ([] as string[]).concat(result[key], value); + } else { + result[key] = value; + } + }); + + return result; +} + +export function convertToTo(link: string | Location) { + if (linkIsLocation(link)) { + return { pathname: link.pathname, search: queryToSearch(link.query) } as Partial<Path>; + } + return link; +} + +function linkIsLocation(link: string | Location): link is Location { + return (link as Location).query !== undefined; +} diff --git a/server/sonar-web/src/main/js/types/admin.ts b/server/sonar-web/src/main/js/types/admin.ts new file mode 100644 index 00000000000..53e4d768cd3 --- /dev/null +++ b/server/sonar-web/src/main/js/types/admin.ts @@ -0,0 +1,24 @@ +/* + * 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 { Extension } from './types'; + +export interface AdminPagesContext { + adminPages: Extension[]; +} diff --git a/server/sonar-web/src/main/js/types/component.ts b/server/sonar-web/src/main/js/types/component.ts index 020a455fb94..b1faecc307c 100644 --- a/server/sonar-web/src/main/js/types/component.ts +++ b/server/sonar-web/src/main/js/types/component.ts @@ -17,7 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { LightComponent } from './types'; +import { ProjectAlmBindingResponse } from './alm-settings'; +import { BranchLike } from './branch-like'; +import { Component, LightComponent } from './types'; export enum Visibility { Public = 'public', @@ -94,3 +96,14 @@ export function isView(componentQualifier?: string | ComponentQualifier): boolea ComponentQualifier.Application ].includes(componentQualifier as ComponentQualifier); } + +export interface ComponentContextShape { + branchLike?: BranchLike; + branchLikes: BranchLike[]; + component?: Component; + isInProgress?: boolean; + isPending?: boolean; + onBranchesChange: (updateBranches?: boolean, updatePRs?: boolean) => void; + onComponentChange: (changes: Partial<Component>) => void; + projectBinding?: ProjectAlmBindingResponse; +} diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 974673267ff..8292592e03d 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -609,6 +609,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.7.6": + version: 7.18.3 + resolution: "@babel/runtime@npm:7.18.3" + dependencies: + regenerator-runtime: ^0.13.4 + checksum: db8526226aa02cfa35a5a7ac1a34b5f303c62a1f000c7db48cb06c6290e616483e5036ab3c4e7a84d0f3be6d4e2148d5fe5cec9564bf955f505c3e764b83d7f1 + languageName: node + linkType: hard + "@babel/template@npm:^7.1.0, @babel/template@npm:^7.4.4": version: 7.4.4 resolution: "@babel/template@npm:7.4.4" @@ -1823,13 +1832,6 @@ __metadata: languageName: node linkType: hard -"@types/history@npm:^3": - version: 3.2.3 - resolution: "@types/history@npm:3.2.3" - checksum: da2e2ca56921ef86703f1a19317bf4af409e425b90d174892fe2fb4a6b8c5856cc5113096ccd94e9436e11720be38920452a612a0e1b2fce1670956a68a3a0a1 - languageName: node - linkType: hard - "@types/hoist-non-react-statics@npm:^3.3.1": version: 3.3.1 resolution: "@types/hoist-non-react-statics@npm:3.3.1" @@ -2007,16 +2009,6 @@ __metadata: languageName: node linkType: hard -"@types/react-router@npm:3.0.20": - version: 3.0.20 - resolution: "@types/react-router@npm:3.0.20" - dependencies: - "@types/history": ^3 - "@types/react": "*" - checksum: fe9b2b00feb6fb6911b47e89109f434c0673878734ad906ceeafa8bc1a99202d6e654bef90cf8216d3f1bb9f00e3b2994982c4307ce87af841db3b67a5d0e793 - languageName: node - linkType: hard - "@types/react-select@npm:4.0.16": version: 4.0.16 resolution: "@types/react-select@npm:4.0.16" @@ -2301,7 +2293,6 @@ __metadata: "@types/react-dom": 16.8.4 "@types/react-helmet": 5.0.15 "@types/react-modal": 3.13.1 - "@types/react-router": 3.0.20 "@types/react-select": 4.0.16 "@types/react-virtualized": 9.21.20 "@types/valid-url": 1.0.3 @@ -2338,7 +2329,6 @@ __metadata: fs-extra: 10.0.1 glob: 7.2.0 glob-promise: 4.2.2 - history: 3.3.0 http-proxy: 1.18.1 jest: 27.5.1 jest-emotion: 10.0.32 @@ -2359,7 +2349,7 @@ __metadata: react-helmet-async: 1.2.3 react-intl: 3.12.1 react-modal: 3.14.4 - react-router: 3.2.6 + react-router-dom: 6.3.0 react-select: 4.3.1 react-select-event: 5.4.0 react-virtualized: 9.22.3 @@ -3478,17 +3468,6 @@ __metadata: languageName: node linkType: hard -"create-react-class@npm:^15.5.1": - version: 15.6.3 - resolution: "create-react-class@npm:15.6.3" - dependencies: - fbjs: ^0.8.9 - loose-envify: ^1.3.1 - object-assign: ^4.1.1 - checksum: 8ad00603815efafe44d511dc39beb0e2d03177c99c60c85978c2d791db880e83be64042e0ee718ccdeb596cd850f3649333adbbd08783980ba3882488bb2bf7d - languageName: node - linkType: hard - "create-react-context@npm:^0.2.2": version: 0.2.3 resolution: "create-react-context@npm:0.2.3" @@ -5143,7 +5122,7 @@ __metadata: languageName: node linkType: hard -"fbjs@npm:^0.8.0, fbjs@npm:^0.8.9": +"fbjs@npm:^0.8.0": version: 0.8.17 resolution: "fbjs@npm:0.8.17" dependencies: @@ -5842,15 +5821,12 @@ __metadata: languageName: node linkType: hard -"history@npm:3.3.0, history@npm:^3.0.0": - version: 3.3.0 - resolution: "history@npm:3.3.0" +"history@npm:^5.2.0": + version: 5.3.0 + resolution: "history@npm:5.3.0" dependencies: - invariant: ^2.2.1 - loose-envify: ^1.2.0 - query-string: ^4.2.2 - warning: ^3.0.0 - checksum: 1570a278a9821d81fba37db7460eb06416c13805df0a9802432ce3161b0fa6d3d4e50fb7c538d1bf84de0f1c697771e5f252d684a4931b25ccb0128cffbc08aa + "@babel/runtime": ^7.7.6 + checksum: d73c35df49d19ac172f9547d30a21a26793e83f16a78386d99583b5bf1429cc980799fcf1827eb215d31816a6600684fba9686ce78104e23bd89ec239e7c726f languageName: node linkType: hard @@ -6185,7 +6161,7 @@ __metadata: languageName: node linkType: hard -"invariant@npm:^2.2.1, invariant@npm:^2.2.4": +"invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" dependencies: @@ -7629,7 +7605,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.2.0, loose-envify@npm:^1.3.1, loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -8952,16 +8928,6 @@ __metadata: languageName: node linkType: hard -"query-string@npm:^4.2.2": - version: 4.3.4 - resolution: "query-string@npm:4.3.4" - dependencies: - object-assign: ^4.1.0 - strict-uri-encode: ^1.0.0 - checksum: 3b2bae6a8454cf0edf11cf1aa4d1f920398bbdabc1c39222b9bb92147e746fcd97faf00e56f494728fb66b2961b495ba0fde699d5d3bd06b11472d664b36c6cf - languageName: node - linkType: hard - "raf@npm:^3.4.1": version: 3.4.1 resolution: "raf@npm:3.4.1" @@ -9089,7 +9055,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.12.0, react-is@npm:^16.13.0": +"react-is@npm:^16.12.0": version: 16.13.0 resolution: "react-is@npm:16.13.0" checksum: 9da7d02ebeb5f2bedb781db5427097dbff9a23d7800b06f0a788bd557a47cd863ebf80de21348207edb66d7667c1adbd65a434e81a3b84c3fdae2597bb697ac5 @@ -9139,21 +9105,27 @@ __metadata: languageName: node linkType: hard -"react-router@npm:3.2.6": - version: 3.2.6 - resolution: "react-router@npm:3.2.6" +"react-router-dom@npm:6.3.0": + version: 6.3.0 + resolution: "react-router-dom@npm:6.3.0" dependencies: - create-react-class: ^15.5.1 - history: ^3.0.0 - hoist-non-react-statics: ^3.3.2 - invariant: ^2.2.1 - loose-envify: ^1.2.0 - prop-types: ^15.7.2 - react-is: ^16.13.0 - warning: ^3.0.0 + history: ^5.2.0 + react-router: 6.3.0 peerDependencies: - react: ^0.14.0 || ^15.0.0 || ^16.0.0 - checksum: 1604dab702ce2dbf2b9754fc62e0ce78ad9ec359e8414a282e633d162bc7294b46fd85ef54fd2edf179a5ea299775d06c35d334edad0798174fd88e9f871f4ed + react: ">=16.8" + react-dom: ">=16.8" + checksum: 77603a654f8a8dc7f65535a2e5917a65f8d9ffcb06546d28dd297e52adcc4b8a84377e0115f48dca330b080af2da3e78f29d590c89307094d36927d2b1751ec3 + languageName: node + linkType: hard + +"react-router@npm:6.3.0": + version: 6.3.0 + resolution: "react-router@npm:6.3.0" + dependencies: + history: ^5.2.0 + peerDependencies: + react: ">=16.8" + checksum: 7be673f5e72104be01e6ab274516bdb932efd93305243170690f6560e3bd1035dd1df3d3c9ce1e0f452638a2529f43a1e77dcf0934fc8031c4783da657be13ca languageName: node linkType: hard @@ -10066,13 +10038,6 @@ resolve@^1.3.2: languageName: node linkType: hard -"strict-uri-encode@npm:^1.0.0": - version: 1.1.0 - resolution: "strict-uri-encode@npm:1.1.0" - checksum: 9466d371f7b36768d43f7803f26137657559e4c8b0161fb9e320efb8edba3ae22f8e99d4b0d91da023b05a13f62ec5412c3f4f764b5788fac11d1fea93720bb3 - languageName: node - linkType: hard - "string-hash@npm:^1.1.1": version: 1.1.3 resolution: "string-hash@npm:1.1.3" |