diff options
author | Guillaume Peoc'h <guillaume.peoch@sonarsource.com> | 2022-02-02 15:36:37 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-02-09 20:02:55 +0000 |
commit | 2b13f016dee62cc813d0374e9f835dee5f4cda28 (patch) | |
tree | a14a39e129bb993ab041c8b29af4f9963bdf0002 | |
parent | 2d48cb3c1eb4f6cd68bd091c4eb9d62fa71deff8 (diff) | |
download | sonarqube-2b13f016dee62cc813d0374e9f835dee5f4cda28.tar.gz sonarqube-2b13f016dee62cc813d0374e9f835dee5f4cda28.zip |
SONAR-15909 Extract AppState Redux
125 files changed, 791 insertions, 758 deletions
diff --git a/server/sonar-web/src/main/js/api/nav.ts b/server/sonar-web/src/main/js/api/nav.ts index 61fa7d6e3cb..974fb5fc1b3 100644 --- a/server/sonar-web/src/main/js/api/nav.ts +++ b/server/sonar-web/src/main/js/api/nav.ts @@ -20,7 +20,7 @@ import throwGlobalError from '../app/utils/throwGlobalError'; import { getJSON } from '../helpers/request'; import { BranchParameters } from '../types/branch-like'; -import { Component } from '../types/types'; +import { Component, Extension } from '../types/types'; type NavComponent = Omit<Component, 'alm' | 'qualifier' | 'leakPeriodDate' | 'path' | 'tags'>; @@ -34,6 +34,9 @@ export function getMarketplaceNavigation(): Promise<{ serverId: string; ncloc: n return getJSON('/api/navigation/marketplace').catch(throwGlobalError); } -export function getSettingsNavigation(): Promise<any> { +export function getSettingsNavigation(): Promise<{ + extensions: Extension[]; + showUpdateCenter: boolean; +}> { return getJSON('/api/navigation/settings').catch(throwGlobalError); } 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 c66a0678b35..8a690681ac7 100644 --- a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx @@ -19,35 +19,35 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { connect } from 'react-redux'; 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 { setAdminPages } from '../../store/appState'; -import { getAppState, Store } from '../../store/rootReducer'; import { PendingPluginResult } from '../../types/plugins'; import { AppState, Extension, SysStatus } from '../../types/types'; import AdminContext, { defaultPendingPlugins, defaultSystemStatus } from './AdminContext'; +import withAppStateContext from './app-state/withAppStateContext'; import SettingsNav from './nav/settings/SettingsNav'; -interface Props { - appState: Pick<AppState, 'adminPages' | 'canAdmin'>; +export interface AdminContainerProps { + appState: AppState; location: {}; - setAdminPages: (adminPages: Extension[]) => void; + children: React.ReactElement; } interface State { pendingPlugins: PendingPluginResult; systemStatus: SysStatus; + adminPages: Extension[]; } -export class AdminContainer extends React.PureComponent<Props, State> { +export class AdminContainer extends React.PureComponent<AdminContainerProps, State> { mounted = false; state: State = { pendingPlugins: defaultPendingPlugins, - systemStatus: defaultSystemStatus + systemStatus: defaultSystemStatus, + adminPages: [] }; componentDidMount() { @@ -67,7 +67,7 @@ export class AdminContainer extends React.PureComponent<Props, State> { fetchNavigationSettings = () => { getSettingsNavigation().then( - r => this.props.setAdminPages(r.extensions), + r => this.setState({ adminPages: r.extensions }), () => {} ); }; @@ -110,7 +110,7 @@ export class AdminContainer extends React.PureComponent<Props, State> { }; render() { - const { adminPages } = this.props.appState; + const { adminPages } = this.state; // Check that the adminPages are loaded if (!adminPages) { @@ -138,15 +138,13 @@ export class AdminContainer extends React.PureComponent<Props, State> { pendingPlugins, systemStatus }}> - {this.props.children} + {React.cloneElement(this.props.children, { + adminPages + })} </AdminContext.Provider> </div> ); } } -const mapStateToProps = (state: Store) => ({ appState: getAppState(state) }); - -const mapDispatchToProps = { setAdminPages }; - -export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer); +export default withAppStateContext(AdminContainer); 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 e1ba399c605..b7e22a74638 100644 --- a/server/sonar-web/src/main/js/app/components/App.tsx +++ b/server/sonar-web/src/main/js/app/components/App.tsx @@ -72,9 +72,8 @@ export class App extends React.PureComponent<Props> { parser.href = this.props.gravatarServerUrl; if (parser.hostname !== window.location.hostname) { return <link href={parser.origin} rel="preconnect" />; - } else { - return null; } + return null; }; render() { 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 e979ec1979d..7a362ba5da7 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -25,7 +25,6 @@ import { getBranches, getPullRequests } from '../../api/branches'; import { getAnalysisStatus, getTasksForComponent } from '../../api/ce'; import { getComponentData } from '../../api/components'; import { getComponentNavigation } from '../../api/nav'; -import { withAppState } from '../../components/hoc/withAppState'; import { Location, Router, withRouter } from '../../components/hoc/withRouter'; import { getBranchLikeQuery, @@ -44,13 +43,14 @@ import { BranchLike } from '../../types/branch-like'; import { ComponentQualifier, isPortfolioLike } from '../../types/component'; import { Task, TaskStatuses, TaskTypes, TaskWarning } from '../../types/tasks'; import { AppState, Component, Status } from '../../types/types'; +import withAppStateContext from './app-state/withAppStateContext'; import ComponentContainerNotFound from './ComponentContainerNotFound'; import { ComponentContext } from './ComponentContext'; import PageUnavailableDueToIndexation from './indexation/PageUnavailableDueToIndexation'; import ComponentNav from './nav/component/ComponentNav'; interface Props { - appState: Pick<AppState, 'branchesEnabled'>; + appState: AppState; children: React.ReactElement; location: Pick<Location, 'query' | 'pathname'>; registerBranchStatus: (branchLike: BranchLike, component: string, status: Status) => void; @@ -417,7 +417,8 @@ export class ComponentContainer extends React.PureComponent<Props, State> { isPending, projectBinding, projectBindingErrors, - tasksInProgress + tasksInProgress, + warnings } = this.state; const isInProgress = tasksInProgress && tasksInProgress.length > 0; @@ -439,7 +440,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> { onWarningDismiss={this.handleWarningDismiss} projectBinding={projectBinding} projectBindingErrors={projectBindingErrors} - warnings={this.state.warnings} + warnings={warnings} /> )} {loading ? ( @@ -467,4 +468,6 @@ export class ComponentContainer extends React.PureComponent<Props, State> { const mapDispatchToProps = { registerBranchStatus, requireAuthorization }; -export default withAppState(withRouter(connect(null, mapDispatchToProps)(ComponentContainer))); +export default withRouter( + connect(null, mapDispatchToProps)(withAppStateContext(ComponentContainer)) +); 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 c9423bff331..d00fbc0cf73 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -22,7 +22,7 @@ import Workspace from '../../components/workspace/Workspace'; import A11yProvider from './a11y/A11yProvider'; import A11ySkipLinks from './a11y/A11ySkipLinks'; import SuggestionsProvider from './embed-docs-modal/SuggestionsProvider'; -import GlobalFooterContainer from './GlobalFooterContainer'; +import GlobalFooter from './GlobalFooter'; import GlobalMessagesContainer from './GlobalMessagesContainer'; import IndexationContextProvider from './indexation/IndexationContextProvider'; import IndexationNotification from './indexation/IndexationNotification'; @@ -41,13 +41,12 @@ export interface Props { export default function GlobalContainer(props: Props) { // it is important to pass `location` down to `GlobalNav` to trigger render on url change - const { footer = <GlobalFooterContainer /> } = props; + const { footer = <GlobalFooter /> } = props; return ( <SuggestionsProvider> <A11yProvider> <StartupModal> <A11ySkipLinks /> - <div className="global-container"> <div className="page-wrapper" id="container"> <div className="page-container"> 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 e52b89d107a..da00469d33c 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx @@ -24,26 +24,21 @@ import { Alert } from '../../components/ui/Alert'; import { getEdition } from '../../helpers/editions'; import { translate, translateWithParameters } from '../../helpers/l10n'; import { EditionKey } from '../../types/editions'; +import { AppState } from '../../types/types'; +import withAppStateContext from './app-state/withAppStateContext'; import GlobalFooterBranding from './GlobalFooterBranding'; -interface Props { +export interface GlobalFooterProps { hideLoggedInInfo?: boolean; - productionDatabase: boolean; - sonarqubeEdition?: EditionKey; - sonarqubeVersion?: string; + appState?: AppState; } -export default function GlobalFooter({ - hideLoggedInInfo, - productionDatabase, - sonarqubeEdition, - sonarqubeVersion -}: Props) { - const currentEdition = sonarqubeEdition && getEdition(sonarqubeEdition); +export function GlobalFooter({ hideLoggedInInfo, appState }: GlobalFooterProps) { + const currentEdition = appState?.edition && getEdition(appState.edition as EditionKey); return ( <div className="page-footer page-container" id="footer"> - {productionDatabase === false && ( + {appState?.productionDatabase === false && ( <Alert display="inline" id="evaluation_warning" variant="warning"> <p className="big">{translate('footer.production_database_warning')}</p> <p> @@ -58,9 +53,9 @@ export default function GlobalFooter({ {!hideLoggedInInfo && currentEdition && ( <li className="page-footer-menu-item">{currentEdition.name}</li> )} - {!hideLoggedInInfo && sonarqubeVersion && ( + {!hideLoggedInInfo && appState?.version && ( <li className="page-footer-menu-item"> - {translateWithParameters('footer.version_x', sonarqubeVersion)} + {translateWithParameters('footer.version_x', appState.version)} </li> )} <li className="page-footer-menu-item"> @@ -99,3 +94,5 @@ export default function GlobalFooter({ </div> ); } + +export default withAppStateContext(GlobalFooter); diff --git a/server/sonar-web/src/main/js/app/components/PageTracker.tsx b/server/sonar-web/src/main/js/app/components/PageTracker.tsx index 8a3e610f4d2..02c47e467fb 100644 --- a/server/sonar-web/src/main/js/app/components/PageTracker.tsx +++ b/server/sonar-web/src/main/js/app/components/PageTracker.tsx @@ -25,12 +25,14 @@ import { gtm } from '../../helpers/analytics'; import { installScript } from '../../helpers/extensions'; import { getWebAnalyticsPageHandlerFromCache } from '../../helpers/extensionsHandler'; import { getInstance } from '../../helpers/system'; -import { getAppState, getGlobalSettingValue, Store } from '../../store/rootReducer'; +import { getGlobalSettingValue, Store } from '../../store/rootReducer'; +import { AppState } from '../../types/types'; +import withAppStateContext from './app-state/withAppStateContext'; interface Props { location: Location; trackingIdGTM?: string; - webAnalytics?: string; + appState: AppState; } interface State { @@ -41,10 +43,10 @@ export class PageTracker extends React.Component<Props, State> { state: State = {}; componentDidMount() { - const { trackingIdGTM, webAnalytics } = this.props; + const { trackingIdGTM, appState } = this.props; - if (webAnalytics && !getWebAnalyticsPageHandlerFromCache()) { - installScript(webAnalytics, 'head'); + if (appState.webAnalyticsJsPath && !getWebAnalyticsPageHandlerFromCache()) { + installScript(appState.webAnalyticsJsPath, 'head'); } if (trackingIdGTM) { @@ -69,13 +71,15 @@ export class PageTracker extends React.Component<Props, State> { }; render() { - const { trackingIdGTM, webAnalytics } = this.props; + const { trackingIdGTM, appState } = this.props; return ( <Helmet defaultTitle={getInstance()} defer={false} - onChangeClientState={trackingIdGTM || webAnalytics ? this.trackPage : undefined}> + onChangeClientState={ + trackingIdGTM || appState.webAnalyticsJsPath ? this.trackPage : undefined + }> {this.props.children} </Helmet> ); @@ -85,9 +89,8 @@ export class PageTracker extends React.Component<Props, State> { const mapStateToProps = (state: Store) => { const trackingIdGTM = getGlobalSettingValue(state, 'sonar.analytics.gtm.trackingId'); return { - trackingIdGTM: trackingIdGTM && trackingIdGTM.value, - webAnalytics: getAppState(state).webAnalyticsJsPath + trackingIdGTM: trackingIdGTM && trackingIdGTM.value }; }; -export default withRouter(connect(mapStateToProps)(PageTracker)); +export default withRouter(connect(mapStateToProps)(withAppStateContext(PageTracker))); 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 bcad1eb490f..0a9e52d54dd 100644 --- a/server/sonar-web/src/main/js/app/components/SimpleContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/SimpleContainer.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import NavBar from '../../components/ui/NavBar'; import { rawSizes } from '../theme'; -import GlobalFooterContainer from './GlobalFooterContainer'; +import GlobalFooter from './GlobalFooter'; interface Props { children?: React.ReactNode; @@ -33,7 +33,7 @@ export default function SimpleContainer({ children }: Props) { <NavBar className="navbar-global" height={rawSizes.globalNavHeightRaw} /> {children} </div> - <GlobalFooterContainer /> + <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 126dd543676..c9564386eaa 100644 --- a/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { lazyLoadComponent } from '../../components/lazyLoadComponent'; -import GlobalFooterContainer from './GlobalFooterContainer'; +import GlobalFooter from './GlobalFooter'; const PageTracker = lazyLoadComponent(() => import('./PageTracker')); @@ -36,7 +36,7 @@ export default function SimpleSessionsContainer({ children }: Props) { <div className="page-wrapper" id="container"> {children} </div> - <GlobalFooterContainer hideLoggedInInfo={true} /> + <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 4b6606246a2..ff8216a49d1 100644 --- a/server/sonar-web/src/main/js/app/components/StartupModal.tsx +++ b/server/sonar-web/src/main/js/app/components/StartupModal.tsx @@ -27,9 +27,10 @@ import { parseDate, toShortNotSoISOString } from '../../helpers/dates'; import { hasMessage } from '../../helpers/l10n'; import { get, save } from '../../helpers/storage'; import { isLoggedIn } from '../../helpers/users'; -import { getAppState, getCurrentUser, Store } from '../../store/rootReducer'; +import { getCurrentUser, Store } from '../../store/rootReducer'; import { EditionKey } from '../../types/editions'; -import { CurrentUser } from '../../types/types'; +import { AppState, CurrentUser } from '../../types/types'; +import withAppStateContext from './app-state/withAppStateContext'; const LicensePromptModal = lazyLoadComponent( () => import('../../apps/marketplace/components/LicensePromptModal'), @@ -37,21 +38,15 @@ const LicensePromptModal = lazyLoadComponent( ); interface StateProps { - canAdmin?: boolean; - currentEdition?: EditionKey; currentUser: CurrentUser; } -interface OwnProps { +type Props = { children?: React.ReactNode; -} - -interface WithRouterProps { location: Pick<Location, 'pathname'>; router: Pick<Router, 'push'>; -} - -type Props = StateProps & OwnProps & WithRouterProps; + appState: AppState; +}; interface State { open?: boolean; @@ -59,7 +54,7 @@ interface State { const LICENSE_PROMPT = 'sonarqube.license.prompt'; -export class StartupModal extends React.PureComponent<Props, State> { +export class StartupModal extends React.PureComponent<Props & StateProps, State> { state: State = {}; componentDidMount() { @@ -71,11 +66,11 @@ export class StartupModal extends React.PureComponent<Props, State> { }; tryAutoOpenLicense = () => { - const { canAdmin, currentEdition, currentUser } = this.props; + const { appState, currentUser } = this.props; const hasLicenseManager = hasMessage('license.prompt.title'); - const hasLicensedEdition = currentEdition && currentEdition !== EditionKey.community; + const hasLicensedEdition = appState.edition && appState.edition !== EditionKey.community; - if (canAdmin && hasLicensedEdition && isLoggedIn(currentUser) && hasLicenseManager) { + if (appState.canAdmin && hasLicensedEdition && isLoggedIn(currentUser) && hasLicenseManager) { const lastPrompt = get(LICENSE_PROMPT, currentUser.login); if (!lastPrompt || differenceInDays(new Date(), parseDate(lastPrompt)) >= 1) { @@ -103,9 +98,7 @@ export class StartupModal extends React.PureComponent<Props, State> { } const mapStateToProps = (state: Store): StateProps => ({ - canAdmin: getAppState(state).canAdmin, - currentEdition: getAppState(state).edition as EditionKey, // TODO: Fix once AppState is no longer ambiant. currentUser: getCurrentUser(state) }); -export default connect(mapStateToProps)(withRouter(StartupModal)); +export default connect(mapStateToProps)(withRouter(withAppStateContext(StartupModal))); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx index e39878b0246..c0ca09c8ee1 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx @@ -19,24 +19,23 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { mockLocation } from '../../../helpers/testMocks'; -import { AdminContainer } from '../AdminContainer'; +import { mockAppState, mockLocation } from '../../../helpers/testMocks'; +import { AdminContainer, AdminContainerProps } from '../AdminContainer'; it('should render correctly', () => { const wrapper = shallowRender(); expect(wrapper).toMatchSnapshot(); }); -function shallowRender(props: Partial<AdminContainer['props']> = {}) { +function shallowRender(props: Partial<AdminContainerProps> = {}) { return shallow( <AdminContainer - appState={{ - adminPages: [{ key: 'foo', name: 'Foo' }], + appState={mockAppState({ canAdmin: true - }} + })} location={mockLocation()} - setAdminPages={jest.fn()} - {...props} - /> + {...props}> + <div /> + </AdminContainer> ); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx index 6f8972a7ec3..7758e327c69 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx @@ -19,8 +19,9 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockAppState } from '../../../helpers/testMocks'; import { EditionKey } from '../../../types/editions'; -import GlobalFooter from '../GlobalFooter'; +import { GlobalFooter, GlobalFooterProps } from '../GlobalFooter'; jest.mock('../../../helpers/system', () => ({ isSonarCloud: jest.fn() })); @@ -30,22 +31,34 @@ it('should render the only logged in information', () => { it('should not render the only logged in information', () => { expect( - getWrapper({ hideLoggedInInfo: true, sonarqubeVersion: '6.4-SNAPSHOT' }) + getWrapper({ + hideLoggedInInfo: true, + appState: mockAppState({ version: '6.4-SNAPSHOT' }) + }) ).toMatchSnapshot(); }); it('should show the db warning message', () => { - expect(getWrapper({ productionDatabase: false }).find('Alert')).toMatchSnapshot(); + expect( + getWrapper({ + appState: mockAppState({ productionDatabase: false, edition: EditionKey.community }) + }).find('Alert') + ).toMatchSnapshot(); }); it('should display the sq version', () => { expect( - getWrapper({ sonarqubeEdition: EditionKey.enterprise, sonarqubeVersion: '6.4-SNAPSHOT' }) + getWrapper({ + appState: mockAppState({ edition: EditionKey.enterprise, version: '6.4-SNAPSHOT' }) + }) ).toMatchSnapshot(); }); -function getWrapper(props = {}) { +function getWrapper(props?: GlobalFooterProps) { return shallow( - <GlobalFooter productionDatabase={true} sonarqubeEdition={EditionKey.community} {...props} /> + <GlobalFooter + appState={mockAppState({ productionDatabase: true, edition: EditionKey.community })} + {...props} + /> ); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx index d8e1c916629..5ea91b924db 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import { gtm } from '../../../helpers/analytics'; import { installScript } from '../../../helpers/extensions'; import { getWebAnalyticsPageHandlerFromCache } from '../../../helpers/extensionsHandler'; -import { mockLocation } from '../../../helpers/testMocks'; +import { mockAppState, mockLocation } from '../../../helpers/testMocks'; import { PageTracker } from '../PageTracker'; jest.mock('../../../helpers/extensions', () => ({ @@ -51,12 +51,12 @@ it('should not trigger if no analytics system is given', () => { it('should work for WebAnalytics plugin', () => { const pageChange = jest.fn(); - const webAnalytics = '/static/pluginKey/web_analytics.js'; - const wrapper = shallowRender({ webAnalytics }); + const webAnalyticsJsPath = '/static/pluginKey/web_analytics.js'; + const wrapper = shallowRender({ appState: mockAppState({ webAnalyticsJsPath }) }); expect(wrapper).toMatchSnapshot(); expect(wrapper.find('Helmet').prop('onChangeClientState')).toBe(wrapper.instance().trackPage); - expect(installScript).toBeCalledWith(webAnalytics, 'head'); + expect(installScript).toBeCalledWith(webAnalyticsJsPath, 'head'); (getWebAnalyticsPageHandlerFromCache as jest.Mock).mockReturnValueOnce(pageChange); wrapper.instance().trackPage(); @@ -81,5 +81,7 @@ it('should work for Google Tag Manager', () => { }); function shallowRender(props: Partial<PageTracker['props']> = {}) { - return shallow<PageTracker>(<PageTracker location={mockLocation()} {...props} />); + return shallow<PageTracker>( + <PageTracker appState={mockAppState()} location={mockLocation()} {...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 1d1035ebfc4..4a07e47012b 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,6 +24,7 @@ import { showLicense } from '../../../api/marketplace'; import { toShortNotSoISOString } from '../../../helpers/dates'; import { hasMessage } from '../../../helpers/l10n'; import { get, save } from '../../../helpers/storage'; +import { mockAppState } from '../../../helpers/testMocks'; import { waitAndUpdate } from '../../../helpers/testUtils'; import { EditionKey } from '../../../types/editions'; import { LoggedInUser } from '../../../types/types'; @@ -67,12 +68,12 @@ beforeEach(() => { }); it('should render only the children', async () => { - const wrapper = getWrapper({ currentEdition: EditionKey.community }); + const wrapper = getWrapper({ appState: mockAppState({ edition: EditionKey.community }) }); await shouldNotHaveModals(wrapper); expect(showLicense).toHaveBeenCalledTimes(0); expect(wrapper.find('div').exists()).toBe(true); - await shouldNotHaveModals(getWrapper({ canAdmin: false })); + await shouldNotHaveModals(getWrapper({ appState: mockAppState({ canAdmin: false }) })); (hasMessage as jest.Mock<any>).mockReturnValueOnce(false); await shouldNotHaveModals(getWrapper()); @@ -86,7 +87,7 @@ it('should render only the children', async () => { await shouldNotHaveModals( getWrapper({ - canAdmin: false, + appState: mockAppState({ canAdmin: false }), currentUser: { ...LOGGED_IN_USER }, location: { pathname: '/documentation/' } }) @@ -94,7 +95,7 @@ it('should render only the children', async () => { await shouldNotHaveModals( getWrapper({ - canAdmin: false, + appState: mockAppState({ canAdmin: false }), currentUser: { ...LOGGED_IN_USER }, location: { pathname: '/create-organization' } }) @@ -126,8 +127,7 @@ async function shouldDisplayLicense(wrapper: ShallowWrapper) { function getWrapper(props: Partial<StartupModal['props']> = {}) { return shallow<StartupModal>( <StartupModal - canAdmin={true} - currentEdition={EditionKey.enterprise} + appState={mockAppState({ edition: EditionKey.enterprise, canAdmin: true })} currentUser={LOGGED_IN_USER} location={{ pathname: 'foo/bar' }} router={{ push: jest.fn() }} 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 db431ca16f0..a12b91340b1 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 @@ -10,14 +10,7 @@ exports[`should render correctly 1`] = ` titleTemplate="%s - layout.settings" /> <SettingsNav - extensions={ - Array [ - Object { - "key": "foo", - "name": "Foo", - }, - ] - } + extensions={Array []} fetchPendingPlugins={[Function]} fetchSystemStatus={[Function]} location={ @@ -53,6 +46,10 @@ exports[`should render correctly 1`] = ` "systemStatus": "UP", } } - /> + > + <div + adminPages={Array []} + /> + </ContextProvider> </div> `; 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 index 1e1d574f0a0..58f58f0bfd2 100644 --- 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 @@ -3,7 +3,7 @@ exports[`should render correctly 1`] = ` <SuggestionsProvider> <A11yProvider> - <Connect(withRouter(StartupModal))> + <Connect(withRouter(withAppStateContext(StartupModal)))> <A11ySkipLinks /> <div className="global-container" @@ -16,7 +16,7 @@ exports[`should render correctly 1`] = ` className="page-container" > <Workspace> - <Connect(withAppState(IndexationContextProvider))> + <withAppStateContext(IndexationContextProvider)> <LanguageContextProvider> <MetricContextProvider> <Connect(GlobalNav) @@ -34,20 +34,20 @@ exports[`should render correctly 1`] = ` /> <Connect(GlobalMessages) /> <Connect(withCurrentUser(withIndexationContext(IndexationNotification))) /> - <Connect(withCurrentUser(Connect(withAppState(UpdateNotification)))) + <Connect(withCurrentUser(withAppStateContext(UpdateNotification))) dismissable={true} /> <ChildComponent /> </MetricContextProvider> </LanguageContextProvider> - </Connect(withAppState(IndexationContextProvider))> + </withAppStateContext(IndexationContextProvider)> </Workspace> </div> <Connect(Connect(withCurrentUser(PromotionNotification))) /> </div> - <Connect(GlobalFooter) /> + <withAppStateContext(GlobalFooter) /> </div> - </Connect(withRouter(StartupModal))> + </Connect(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 61e974fab75..d5450c04210 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 @@ -152,6 +152,11 @@ exports[`should render the only logged in information 1`] = ` <li className="page-footer-menu-item" > + footer.version_x.1.0 + </li> + <li + className="page-footer-menu-item" + > <a href="https://www.gnu.org/licenses/lgpl-3.0.txt" rel="noopener noreferrer" diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/AppContainer.ts b/server/sonar-web/src/main/js/app/components/app-state/AppStateContext.tsx index 17120732063..1e2dcf47d4f 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/AppContainer.ts +++ b/server/sonar-web/src/main/js/app/components/app-state/AppStateContext.tsx @@ -17,13 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { connect } from 'react-redux'; -import { getAppState, Store } from '../../../store/rootReducer'; -import App from './App'; -const mapStateToProps = (state: Store) => ({ - branchesEnabled: getAppState(state).branchesEnabled, - canAdmin: getAppState(state).canAdmin -}); +import * as React from 'react'; +import { AppState } from '../../../types/types'; -export default connect(mapStateToProps)(App); +const defaultAppState = { + authenticationError: false, + authorizationError: false, + edition: undefined, + productionDatabase: true, + qualifiers: [], + settings: {}, + version: '' +}; +export const AppStateContext = React.createContext<AppState>(defaultAppState); diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx b/server/sonar-web/src/main/js/app/components/app-state/AppStateContextProvider.tsx index e0bf748c7c4..0b7336d10f1 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/app-state/AppStateContextProvider.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ /* * SonarQube * Copyright (C) 2009-2022 SonarSource SA @@ -17,21 +18,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 { connect } from 'react-redux'; -import { getAppState, Store } from '../../store/rootReducer'; -import { EditionKey } from '../../types/editions'; -import GlobalFooter from './GlobalFooter'; -interface StateProps { - productionDatabase: boolean; - sonarqubeEdition?: EditionKey; - sonarqubeVersion?: string; -} +import * as React from 'react'; +import { AppState } from '../../../types/types'; +import { AppStateContext } from './AppStateContext'; -const mapStateToProps = (state: Store): StateProps => ({ - productionDatabase: getAppState(state).productionDatabase, - sonarqubeEdition: getAppState(state).edition as EditionKey, // TODO: Fix once AppState is no longer ambiant. - sonarqubeVersion: getAppState(state).version -}); +export interface AppStateContextProviderProps { + appState: AppState; +} -export default connect(mapStateToProps)(GlobalFooter); +export default function AppStateContextProvider({ + appState, + children +}: React.PropsWithChildren<AppStateContextProviderProps>) { + return <AppStateContext.Provider value={appState}>{children}</AppStateContext.Provider>; +} diff --git a/server/sonar-web/src/main/js/app/components/app-state/__tests__/withAppStateContext-test.tsx b/server/sonar-web/src/main/js/app/components/app-state/__tests__/withAppStateContext-test.tsx new file mode 100644 index 00000000000..d6323fb0f81 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/app-state/__tests__/withAppStateContext-test.tsx @@ -0,0 +1,50 @@ +/* + * 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 { shallow } from 'enzyme'; +import * as React from 'react'; +import { mockAppState } from '../../../../helpers/testMocks'; +import { AppState } from '../../../../types/types'; +import withAppStateContext from '../withAppStateContext'; + +const appState = mockAppState(); + +jest.mock('../AppStateContext', () => { + return { + AppStateContext: { + Consumer: ({ children }: { children: (props: {}) => React.ReactNode }) => { + return children(appState); + } + } + }; +}); + +class Wrapped extends React.Component<{ appState: AppState }> { + render() { + return <div />; + } +} + +const UnderTest = withAppStateContext(Wrapped); + +it('should inject appState', () => { + const wrapper = shallow(<UnderTest />); + expect(wrapper.dive().type()).toBe(Wrapped); + expect(wrapper.dive<Wrapped>().props().appState).toEqual(appState); +}); diff --git a/server/sonar-web/src/main/js/components/hoc/withAppState.tsx b/server/sonar-web/src/main/js/app/components/app-state/withAppStateContext.tsx index 6e343d26989..64107510cf1 100644 --- a/server/sonar-web/src/main/js/components/hoc/withAppState.tsx +++ b/server/sonar-web/src/main/js/app/components/app-state/withAppStateContext.tsx @@ -17,26 +17,30 @@ * 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 { connect } from 'react-redux'; -import { getAppState, Store } from '../../store/rootReducer'; -import { AppState } from '../../types/types'; -import { getWrappedDisplayName } from './utils'; +import { getWrappedDisplayName } from '../../../components/hoc/utils'; +import { AppState } from '../../../types/types'; +import { AppStateContext } from './AppStateContext'; + +export interface WithAppStateContextProps { + appState: AppState; +} -export function withAppState<P>( - WrappedComponent: React.ComponentType<P & { appState: Partial<AppState> }> +export default function withAppStateContext<P>( + WrappedComponent: React.ComponentType<P & WithAppStateContextProps> ) { - class Wrapper extends React.Component<P & { appState: AppState }> { - static displayName = getWrappedDisplayName(WrappedComponent, 'withAppState'); + return class WithAppStateContext extends React.PureComponent< + Omit<P, keyof WithAppStateContextProps> + > { + static displayName = getWrappedDisplayName(WrappedComponent, 'withAppStateContext'); render() { - return <WrappedComponent {...this.props} />; + return ( + <AppStateContext.Consumer> + {appState => <WrappedComponent appState={appState} {...(this.props as P)} />} + </AppStateContext.Consumer> + ); } - } - - function mapStateToProps(state: Store) { - return { appState: getAppState(state) }; - } - - return connect(mapStateToProps)(Wrapper); + }; } 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 986a581768e..28974085d08 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,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; -import { getAppState, Store } from '../../../store/rootReducer'; import { Extension as TypeExtension } from '../../../types/types'; import NotFound from '../NotFound'; import Extension from './Extension'; @@ -29,14 +27,11 @@ interface Props { params: { extensionKey: string; pluginKey: string }; } -function GlobalAdminPageExtension(props: Props) { - const { extensionKey, pluginKey } = props.params; - const extension = (props.adminPages || []).find(p => p.key === `${pluginKey}/${extensionKey}`); +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} />; } - -const mapStateToProps = (state: Store) => ({ - adminPages: getAppState(state).adminPages -}); - -export default connect(mapStateToProps)(GlobalAdminPageExtension); 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 1f1fd89fd30..d3b0559d35c 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,25 +18,23 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; -import { getAppState, Store } from '../../../store/rootReducer'; -import { Extension as TypeExtension } from '../../../types/types'; +import { AppState } from '../../../types/types'; +import withAppStateContext from '../app-state/withAppStateContext'; import NotFound from '../NotFound'; import Extension from './Extension'; interface Props { - globalPages: TypeExtension[] | undefined; + appState: AppState; params: { extensionKey: string; pluginKey: string }; } function GlobalPageExtension(props: Props) { - const { extensionKey, pluginKey } = props.params; - const extension = (props.globalPages || []).find(p => p.key === `${pluginKey}/${extensionKey}`); + const { + params: { extensionKey, pluginKey }, + appState: { globalPages } + } = props; + const extension = (globalPages || []).find(p => p.key === `${pluginKey}/${extensionKey}`); return extension ? <Extension extension={extension} /> : <NotFound withContainer={false} />; } -const mapStateToProps = (state: Store) => ({ - globalPages: getAppState(state).globalPages -}); - -export default connect(mapStateToProps)(GlobalPageExtension); +export default withAppStateContext(GlobalPageExtension); diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationContextProvider.tsx b/server/sonar-web/src/main/js/app/components/indexation/IndexationContextProvider.tsx index 84c997f6b54..091fc03024d 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/IndexationContextProvider.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/IndexationContextProvider.tsx @@ -19,18 +19,18 @@ */ /* eslint-disable react/no-unused-state */ import * as React from 'react'; -import { withAppState } from '../../../components/hoc/withAppState'; import { IndexationContextInterface, IndexationStatus } from '../../../types/indexation'; import { AppState } from '../../../types/types'; +import withAppStateContext from '../app-state/withAppStateContext'; import { IndexationContext } from './IndexationContext'; import IndexationNotificationHelper from './IndexationNotificationHelper'; -interface Props { - appState: Pick<AppState, 'needIssueSync'>; +export interface IndexationContextProviderProps { + appState: AppState; } export class IndexationContextProvider extends React.PureComponent< - React.PropsWithChildren<Props>, + React.PropsWithChildren<IndexationContextProviderProps>, IndexationContextInterface > { mounted = false; @@ -66,4 +66,4 @@ export class IndexationContextProvider extends React.PureComponent< } } -export default withAppState(IndexationContextProvider); +export default withAppStateContext(IndexationContextProvider); diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationContextProvider-test.tsx b/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationContextProvider-test.tsx index eed129e5a53..c6c383cf158 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationContextProvider-test.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationContextProvider-test.tsx @@ -19,9 +19,13 @@ */ import { mount } from 'enzyme'; import * as React from 'react'; +import { mockAppState } from '../../../../helpers/testMocks'; import { IndexationStatus } from '../../../../types/indexation'; import { IndexationContext } from '../IndexationContext'; -import { IndexationContextProvider } from '../IndexationContextProvider'; +import { + IndexationContextProvider, + IndexationContextProviderProps +} from '../IndexationContextProvider'; import IndexationNotificationHelper from '../IndexationNotificationHelper'; beforeEach(() => jest.clearAllMocks()); @@ -36,7 +40,8 @@ it('should render correctly and start polling if issue sync is needed', () => { }); it('should not start polling if no issue sync is needed', () => { - const wrapper = mountRender({ appState: { needIssueSync: false } }); + const appState = mockAppState({ needIssueSync: false }); + const wrapper = mountRender({ appState }); expect(IndexationNotificationHelper.startPolling).not.toHaveBeenCalled(); @@ -72,9 +77,9 @@ it('should stop polling when component is destroyed', () => { expect(IndexationNotificationHelper.stopPolling).toHaveBeenCalled(); }); -function mountRender(props?: IndexationContextProvider['props']) { +function mountRender(props?: IndexationContextProviderProps) { return mount<IndexationContextProvider>( - <IndexationContextProvider appState={{ needIssueSync: true }} {...props}> + <IndexationContextProvider appState={mockAppState({ needIssueSync: true, ...props?.appState })}> <TestComponent /> </IndexationContextProvider> ); diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationContextProvider-test.tsx.snap b/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationContextProvider-test.tsx.snap index a7c918f76fe..a966efb2160 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationContextProvider-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationContextProvider-test.tsx.snap @@ -4,7 +4,14 @@ exports[`should render correctly and start polling if issue sync is needed 1`] = <IndexationContextProvider appState={ Object { + "edition": "community", "needIssueSync": true, + "productionDatabase": true, + "qualifiers": Array [ + "TRK", + ], + "settings": Object {}, + "version": "1.0", } } > 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 5466afe3f32..37770276d4f 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 @@ -20,15 +20,15 @@ import * as React from 'react'; import { Link } from 'react-router'; import { isValidLicense } from '../../../../api/marketplace'; -import { withAppState } from '../../../../components/hoc/withAppState'; import { Alert } from '../../../../components/ui/Alert'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; import { ComponentQualifier } from '../../../../types/component'; import { Task } from '../../../../types/tasks'; import { AppState } from '../../../../types/types'; +import withAppStateContext from '../../app-state/withAppStateContext'; interface Props { - appState: Pick<AppState, 'canAdmin'>; + appState: AppState; currentTask?: Task; } @@ -67,7 +67,7 @@ export class ComponentNavLicenseNotif extends React.PureComponent<Props, State> }; render() { - const { currentTask } = this.props; + const { currentTask, appState } = this.props; const { isValidLicense, loading } = this.state; if (loading || !currentTask || !currentTask.errorType) { @@ -88,7 +88,7 @@ export class ComponentNavLicenseNotif extends React.PureComponent<Props, State> return ( <Alert display="banner" variant="error"> <span className="little-spacer-right">{currentTask.errorMessage}</span> - {this.props.appState.canAdmin ? ( + {appState.canAdmin ? ( <Link to="/admin/extension/license/app"> {translate('license.component_navigation.button', currentTask.errorType)}. </Link> @@ -100,4 +100,4 @@ export class ComponentNavLicenseNotif extends React.PureComponent<Props, State> } } -export default withAppState(ComponentNavLicenseNotif); +export default withAppStateContext(ComponentNavLicenseNotif); 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 7d08885238a..7f7d3c32843 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 @@ -24,7 +24,6 @@ import * as React from 'react'; import { Link, LinkProps } from 'react-router'; import Dropdown from '../../../../components/controls/Dropdown'; import Tooltip from '../../../../components/controls/Tooltip'; -import { withAppState } from '../../../../components/hoc/withAppState'; import BulletListIcon from '../../../../components/icons/BulletListIcon'; import DropdownIcon from '../../../../components/icons/DropdownIcon'; import NavBarTabs from '../../../../components/ui/NavBarTabs'; @@ -34,6 +33,7 @@ import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls'; import { BranchLike, BranchParameters } from '../../../../types/branch-like'; import { ComponentQualifier, isPortfolioLike } from '../../../../types/component'; import { AppState, Component, Extension } from '../../../../types/types'; +import withAppStateContext from '../../app-state/withAppStateContext'; import './Menu.css'; const SETTINGS_URLS = [ @@ -53,7 +53,7 @@ const SETTINGS_URLS = [ ]; interface Props { - appState: Pick<AppState, 'branchesEnabled'>; + appState: AppState; branchLike: BranchLike | undefined; branchLikes: BranchLike[] | undefined; component: Component; @@ -620,4 +620,4 @@ export class Menu extends React.PureComponent<Props> { } } -export default withAppState(Menu); +export default withAppStateContext(Menu); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx index aed4d489ece..354a592c179 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx @@ -21,6 +21,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { isValidLicense } from '../../../../../api/marketplace'; import { mockTask } from '../../../../../helpers/mocks/tasks'; +import { mockAppState } from '../../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../../helpers/testUtils'; import { TaskStatuses } from '../../../../../types/tasks'; import { ComponentNavLicenseNotif } from '../ComponentNavLicenseNotif'; @@ -50,7 +51,7 @@ it('renders background task license info correctly', async () => { expect(wrapper).toMatchSnapshot(); wrapper = getWrapper({ - appState: { canAdmin: false }, + appState: mockAppState({ canAdmin: false }), currentTask: mockTask({ status: TaskStatuses.Failed, errorType: 'LICENSING', @@ -90,7 +91,7 @@ it('renders correctly for LICENSING_LOC error', async () => { function getWrapper(props: Partial<ComponentNavLicenseNotif['props']> = {}) { return shallow( <ComponentNavLicenseNotif - appState={{ canAdmin: true }} + appState={mockAppState({ canAdmin: true })} currentTask={mockTask({ errorMessage: 'Foo', errorType: 'LICENSING' })} {...props} /> diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx index 17a7293eb30..2d65f7eefa0 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx @@ -25,6 +25,7 @@ import { mockPullRequest } from '../../../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../../../helpers/mocks/component'; +import { mockAppState } from '../../../../../helpers/testMocks'; import { ComponentQualifier } from '../../../../../types/component'; import { Menu } from '../Menu'; @@ -165,7 +166,7 @@ it('should disable links if application has inaccessible projects', () => { function shallowRender(props: Partial<Menu['props']>) { return shallow<Menu>( <Menu - appState={{ branchesEnabled: true }} + appState={mockAppState({ branchesEnabled: true })} branchLike={mainBranch} branchLikes={[mainBranch]} component={baseComponent} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap index 4c9f5933de5..0a372d2d7b8 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap @@ -73,7 +73,7 @@ exports[`renders correctly: default 1`] = ` warnings={Array []} /> </div> - <Connect(withAppState(Menu)) + <withAppStateContext(Menu) branchLikes={Array []} component={ Object { @@ -267,7 +267,7 @@ exports[`renders correctly: has failed notification 1`] = ` warnings={Array []} /> </div> - <Connect(withAppState(Menu)) + <withAppStateContext(Menu) branchLikes={Array []} component={ Object { @@ -447,7 +447,7 @@ exports[`renders correctly: has failed project binding 1`] = ` warnings={Array []} /> </div> - <Connect(withAppState(Menu)) + <withAppStateContext(Menu) branchLikes={Array []} component={ Object { @@ -629,7 +629,7 @@ exports[`renders correctly: has in progress notification 1`] = ` warnings={Array []} /> </div> - <Connect(withAppState(Menu)) + <withAppStateContext(Menu) branchLikes={Array []} component={ Object { @@ -811,7 +811,7 @@ exports[`renders correctly: has pending notification 1`] = ` warnings={Array []} /> </div> - <Connect(withAppState(Menu)) + <withAppStateContext(Menu) branchLikes={Array []} component={ Object { @@ -966,7 +966,7 @@ exports[`renders correctly: has warnings 1`] = ` } /> </div> - <Connect(withAppState(Menu)) + <withAppStateContext(Menu) branchLikes={Array []} component={ Object { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap index cfed20bb431..03f114eee5b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap @@ -22,7 +22,7 @@ exports[`renders correctly: default 1`] = ` `; exports[`renders correctly: license issue 1`] = ` -<Connect(withAppState(ComponentNavLicenseNotif)) +<withAppStateContext(ComponentNavLicenseNotif) currentTask={ Object { "analysisId": "x123", diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Header-test.tsx.snap index dbb95fd354c..c3f9e27795d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Header-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Header-test.tsx.snap @@ -49,7 +49,7 @@ exports[`should render correctly 1`] = ` favorite={false} qualifier="TRK" /> - <Connect(withAppState(Component)) + <withAppStateContext(Component) branchLikes={ Array [ Object { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx index 0a85e8a9e14..2e619a4e049 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx @@ -20,16 +20,16 @@ import classNames from 'classnames'; import * as React from 'react'; import Toggler from '../../../../../components/controls/Toggler'; -import { withAppState } from '../../../../../components/hoc/withAppState'; import { ProjectAlmBindingResponse } from '../../../../../types/alm-settings'; import { BranchLike } from '../../../../../types/branch-like'; import { AppState, Component } from '../../../../../types/types'; +import withAppStateContext from '../../../app-state/withAppStateContext'; import './BranchLikeNavigation.css'; import CurrentBranchLike from './CurrentBranchLike'; import Menu from './Menu'; export interface BranchLikeNavigationProps { - appState: Pick<AppState, 'branchesEnabled'>; + appState: AppState; branchLikes: BranchLike[]; component: Component; currentBranchLike: BranchLike; @@ -94,4 +94,4 @@ export function BranchLikeNavigation(props: BranchLikeNavigationProps) { ); } -export default withAppState(React.memo(BranchLikeNavigation)); +export default withAppStateContext(React.memo(BranchLikeNavigation)); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx index b9f73a60aef..488135ebd8b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx @@ -20,8 +20,8 @@ import * as React from 'react'; import { connect } from 'react-redux'; import NavBar from '../../../../components/ui/NavBar'; -import { getAppState, getCurrentUser, Store } from '../../../../store/rootReducer'; -import { AppState, CurrentUser } from '../../../../types/types'; +import { getCurrentUser, Store } from '../../../../store/rootReducer'; +import { CurrentUser } from '../../../../types/types'; import { rawSizes } from '../../../theme'; import EmbedDocsPopupHelper from '../../embed-docs-modal/EmbedDocsPopupHelper'; import Search from '../../search/Search'; @@ -31,18 +31,17 @@ import GlobalNavMenu from './GlobalNavMenu'; import GlobalNavUser from './GlobalNavUser'; export interface GlobalNavProps { - appState: Pick<AppState, 'canAdmin' | 'globalPages' | 'qualifiers'>; currentUser: CurrentUser; location: { pathname: string }; } export function GlobalNav(props: GlobalNavProps) { - const { appState, currentUser, location } = props; + const { currentUser, location } = props; return ( <NavBar className="navbar-global" height={rawSizes.globalNavHeightRaw} id="global-navigation"> <GlobalNavBranding /> - <GlobalNavMenu appState={appState} currentUser={currentUser} location={location} /> + <GlobalNavMenu currentUser={currentUser} location={location} /> <ul className="global-navbar-menu global-navbar-menu-right"> <EmbedDocsPopupHelper /> @@ -55,8 +54,7 @@ export function GlobalNav(props: GlobalNavProps) { const mapStateToProps = (state: Store) => { return { - currentUser: getCurrentUser(state), - appState: getAppState(state) + currentUser: getCurrentUser(state) }; }; 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 8da1d583c0f..daf8af77711 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 @@ -27,14 +27,15 @@ import { translate } from '../../../../helpers/l10n'; import { getQualityGatesUrl } from '../../../../helpers/urls'; import { ComponentQualifier } from '../../../../types/component'; import { AppState, CurrentUser, Extension } from '../../../../types/types'; +import withAppStateContext from '../../app-state/withAppStateContext'; interface Props { - appState: Pick<AppState, 'canAdmin' | 'globalPages' | 'qualifiers'>; + appState: AppState; currentUser: CurrentUser; location: { pathname: string }; } -export default class GlobalNavMenu extends React.PureComponent<Props> { +export class GlobalNavMenu extends React.PureComponent<Props> { renderProjects() { const active = this.props.location.pathname.startsWith('/projects') && @@ -173,3 +174,5 @@ export default class GlobalNavMenu extends React.PureComponent<Props> { ); } } + +export default withAppStateContext(GlobalNavMenu); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx index 0a8bd89a46a..49b64ed497c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx @@ -26,11 +26,6 @@ import { GlobalNav, GlobalNavProps } from '../GlobalNav'; // https://stackoverflow.com/questions/43375079/redux-warning-only-appearing-in-tests jest.mock('../../../../../store/rootReducer'); -const appState: GlobalNavProps['appState'] = { - globalPages: [], - canAdmin: false, - qualifiers: [] -}; const location = { pathname: '' }; it('should render correctly', async () => { @@ -44,12 +39,5 @@ it('should render correctly', async () => { }); function shallowRender(props: Partial<GlobalNavProps> = {}) { - return shallow( - <GlobalNav - appState={appState} - currentUser={{ isLoggedIn: false }} - location={location} - {...props} - /> - ); + return shallow(<GlobalNav currentUser={{ isLoggedIn: false }} location={location} {...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 f18528c24cd..1a56df67d0f 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 @@ -19,13 +19,14 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import GlobalNavMenu from '../GlobalNavMenu'; +import { mockAppState } from '../../../../../helpers/testMocks'; +import { GlobalNavMenu } from '../GlobalNavMenu'; it('should work with extensions', () => { - const appState = { + const appState = mockAppState({ globalPages: [{ key: 'foo', name: 'Foo' }], qualifiers: ['TRK'] - }; + }); const currentUser = { isLoggedIn: false }; @@ -36,11 +37,11 @@ it('should work with extensions', () => { }); it('should show administration menu if the user has the rights', () => { - const appState = { + const appState = mockAppState({ canAdmin: true, globalPages: [], qualifiers: ['TRK'] - }; + }); const currentUser = { isLoggedIn: false }; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap index d3f0165206c..cc530594fb3 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap @@ -7,14 +7,7 @@ exports[`should render correctly: anonymous users 1`] = ` id="global-navigation" > <Connect(GlobalNavBranding) /> - <GlobalNavMenu - appState={ - Object { - "canAdmin": false, - "globalPages": Array [], - "qualifiers": Array [], - } - } + <withAppStateContext(GlobalNavMenu) currentUser={ Object { "isLoggedIn": false, @@ -55,14 +48,7 @@ exports[`should render correctly: logged in users 1`] = ` id="global-navigation" > <Connect(GlobalNavBranding) /> - <GlobalNavMenu - appState={ - Object { - "canAdmin": false, - "globalPages": Array [], - "qualifiers": Array [], - } - } + <withAppStateContext(GlobalNavMenu) currentUser={ Object { "isLoggedIn": true, diff --git a/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx b/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx index 2195b772fa3..8958a1c02d6 100644 --- a/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx +++ b/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx @@ -20,7 +20,6 @@ import { groupBy, isEmpty, mapValues } from 'lodash'; import * as React from 'react'; import { getSystemUpgrades } from '../../../api/system'; -import { withAppState } from '../../../components/hoc/withAppState'; import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; import { Alert, AlertVariant } from '../../../components/ui/Alert'; import DismissableAlert from '../../../components/ui/DismissableAlert'; @@ -31,6 +30,7 @@ import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users'; import { Permissions } from '../../../types/permissions'; import { SystemUpgrade } from '../../../types/system'; import { AppState, CurrentUser, Dict } from '../../../types/types'; +import withAppStateContext from '../app-state/withAppStateContext'; import './UpdateNotification.css'; const MONTH_BEFOR_PREVIOUS_LTS_NOTIFICATION = 6; @@ -48,7 +48,7 @@ const MAP_VARIANT: Dict<AlertVariant> = { interface Props { dismissable: boolean; - appState: Pick<AppState, 'version'>; + appState: AppState; currentUser: CurrentUser; } @@ -249,4 +249,4 @@ export class UpdateNotification extends React.PureComponent<Props, State> { } } -export default withCurrentUser(withAppState(UpdateNotification)); +export default withCurrentUser(withAppStateContext(UpdateNotification)); diff --git a/server/sonar-web/src/main/js/app/index.ts b/server/sonar-web/src/main/js/app/index.ts index 02e3c39a063..d94e52dbd7e 100644 --- a/server/sonar-web/src/main/js/app/index.ts +++ b/server/sonar-web/src/main/js/app/index.ts @@ -31,7 +31,7 @@ if (isMainApp()) { Promise.all([loadL10nBundle(), loadUser(), loadAppState(), loadApp()]).then( ([l10nBundle, user, appState, startReactApp]) => { - startReactApp(l10nBundle.locale, user, appState); + startReactApp(l10nBundle.locale, appState, user); }, error => { if (isResponse(error) && error.status === 401) { @@ -44,19 +44,27 @@ if (isMainApp()) { } else { // login, maintenance or setup pages - const appStatePromise: Promise<AppState | undefined> = new Promise(resolve => { + const appStatePromise: Promise<AppState> = new Promise(resolve => { loadAppState() .then(data => { resolve(data); }) .catch(() => { - resolve(undefined); + resolve({ + authenticationError: false, + authorizationError: false, + edition: undefined, + productionDatabase: true, + qualifiers: [], + settings: {}, + version: '' + }); }); }); Promise.all([loadL10nBundle(), appStatePromise, loadApp()]).then( ([l10nBundle, appState, startReactApp]) => { - startReactApp(l10nBundle.locale, undefined, appState); + startReactApp(l10nBundle.locale, appState); }, error => { logError(error); 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 aa7fda54aa1..89456a949bb 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -62,6 +62,7 @@ import { lazyLoadComponent } from '../../components/lazyLoadComponent'; import getHistory from '../../helpers/getHistory'; import { AppState, CurrentUser } from '../../types/types'; import App from '../components/App'; +import AppStateContextProvider from '../components/app-state/AppStateContextProvider'; import GlobalContainer from '../components/GlobalContainer'; import { PageContext } from '../components/indexation/PageUnavailableDueToIndexation'; import MigrationContainer from '../components/MigrationContainer'; @@ -278,11 +279,7 @@ function renderAdminRoutes() { ); } -export default function startReactApp( - lang: string, - currentUser?: CurrentUser, - appState?: AppState -) { +export default function startReactApp(lang: string, appState: AppState, currentUser?: CurrentUser) { attachToGlobal(); const el = document.getElementById('content'); @@ -293,92 +290,96 @@ export default function startReactApp( render( <HelmetProvider> <Provider store={store}> - <IntlProvider defaultLocale={lang} locale={lang}> - <Router history={history} onUpdate={handleUpdate}> - {renderRedirects()} + <AppStateContextProvider appState={appState}> + <IntlProvider defaultLocale={lang} locale={lang}> + <Router history={history} onUpdate={handleUpdate}> + {renderRedirects()} - <Route - path="formatting/help" - component={lazyLoadComponent(() => import('../components/FormattingHelp'))} - /> - - <Route component={lazyLoadComponent(() => import('../components/SimpleContainer'))}> - <Route path="maintenance">{maintenanceRoutes}</Route> - <Route path="setup">{setupRoutes}</Route> - </Route> - - <Route component={MigrationContainer}> <Route - component={lazyLoadComponent(() => - import('../components/SimpleSessionsContainer') - )}> - <RouteWithChildRoutes path="/sessions" childRoutes={sessionsRoutes} /> + path="formatting/help" + component={lazyLoadComponent(() => import('../components/FormattingHelp'))} + /> + + <Route component={lazyLoadComponent(() => import('../components/SimpleContainer'))}> + <Route path="maintenance">{maintenanceRoutes}</Route> + <Route path="setup">{setupRoutes}</Route> </Route> - <Route path="/" component={App}> - <IndexRoute component={lazyLoadComponent(() => import('../components/Landing'))} /> + <Route component={MigrationContainer}> + <Route + component={lazyLoadComponent(() => + import('../components/SimpleSessionsContainer') + )}> + <RouteWithChildRoutes path="/sessions" childRoutes={sessionsRoutes} /> + </Route> + + <Route path="/" component={App}> + <IndexRoute + component={lazyLoadComponent(() => import('../components/Landing'))} + /> + + <Route component={GlobalContainer}> + <RouteWithChildRoutes path="account" childRoutes={accountRoutes} /> + <RouteWithChildRoutes path="coding_rules" childRoutes={codingRulesRoutes} /> + <RouteWithChildRoutes path="documentation" childRoutes={documentationRoutes} /> + <Route + path="extension/:pluginKey/:extensionKey" + component={lazyLoadComponent(() => + import('../components/extensions/GlobalPageExtension') + )} + /> + <Route + path="issues" + component={withIndexationGuard(Issues, PageContext.Issues)} + /> + <RouteWithChildRoutes path="projects" childRoutes={projectsRoutes} /> + <RouteWithChildRoutes path="quality_gates" childRoutes={qualityGatesRoutes} /> + <Route + path="portfolios" + component={lazyLoadComponent(() => + import('../components/extensions/PortfoliosPage') + )} + /> + <RouteWithChildRoutes path="profiles" childRoutes={qualityProfilesRoutes} /> + <RouteWithChildRoutes path="web_api" childRoutes={webAPIRoutes} /> - <Route component={GlobalContainer}> - <RouteWithChildRoutes path="account" childRoutes={accountRoutes} /> - <RouteWithChildRoutes path="coding_rules" childRoutes={codingRulesRoutes} /> - <RouteWithChildRoutes path="documentation" childRoutes={documentationRoutes} /> + {renderComponentRoutes()} + + {renderAdminRoutes()} + </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 - path="extension/:pluginKey/:extensionKey" + // 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('../components/extensions/GlobalPageExtension') + import('../../apps/change-admin-password/ChangeAdminPasswordApp') )} /> <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/plugin_risk_consent" + component={lazyLoadComponent(() => import('../components/PluginRiskConsent'))} /> - <RouteWithChildRoutes path="projects" childRoutes={projectsRoutes} /> - <RouteWithChildRoutes path="quality_gates" childRoutes={qualityGatesRoutes} /> <Route - path="portfolios" - component={lazyLoadComponent(() => - import('../components/extensions/PortfoliosPage') - )} + path="not_found" + component={lazyLoadComponent(() => import('../components/NotFound'))} + /> + <Route + path="*" + component={lazyLoadComponent(() => import('../components/NotFound'))} /> - <RouteWithChildRoutes path="profiles" childRoutes={qualityProfilesRoutes} /> - <RouteWithChildRoutes path="web_api" childRoutes={webAPIRoutes} /> - - {renderComponentRoutes()} - - {renderAdminRoutes()} </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> - </IntlProvider> + </Router> + </IntlProvider> + </AppStateContextProvider> </Provider> </HelmetProvider>, el 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 4854062f011..84c09c6ec85 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,8 +19,9 @@ */ import * as React from 'react'; import { connect } from 'react-redux'; -import { getAppState, getGlobalSettingValue, Store } from '../../../store/rootReducer'; +import { getGlobalSettingValue, Store } from '../../../store/rootReducer'; import { AdminPageExtension } from '../../../types/extension'; +import { Extension } from '../../../types/types'; import { fetchValues } from '../../settings/store/actions'; import '../style.css'; import { HousekeepingPolicy, RangeOption } from '../utils'; @@ -29,23 +30,32 @@ import AuditAppRenderer from './AuditAppRenderer'; interface Props { auditHousekeepingPolicy: HousekeepingPolicy; fetchValues: typeof fetchValues; - hasGovernanceExtension?: boolean; + adminPages: Extension[]; } interface State { dateRange?: { from?: Date; to?: Date }; + hasGovernanceExtension?: boolean; downloadStarted: boolean; selection: RangeOption; } export class AuditApp extends React.PureComponent<Props, State> { - state: State = { - downloadStarted: false, - selection: RangeOption.Today - }; + constructor(props: Props) { + super(props); + const hasGovernanceExtension = Boolean( + props.adminPages?.find(e => e.key === AdminPageExtension.GovernanceConsole) + ); + this.state = { + downloadStarted: false, + selection: RangeOption.Today, + hasGovernanceExtension + }; + } componentDidMount() { - const { hasGovernanceExtension } = this.props; + const { hasGovernanceExtension } = this.state; + if (hasGovernanceExtension) { this.props.fetchValues(['sonar.dbcleaner.auditHousekeeping']); } @@ -64,17 +74,22 @@ export class AuditApp extends React.PureComponent<Props, State> { }; render() { - const { hasGovernanceExtension, auditHousekeepingPolicy } = this.props; + const { hasGovernanceExtension, ...auditAppRendererProps } = this.state; + const { auditHousekeepingPolicy } = this.props; + + if (!hasGovernanceExtension) { + return null; + } - return hasGovernanceExtension ? ( + return ( <AuditAppRenderer handleDateSelection={this.handleDateSelection} handleOptionSelection={this.handleOptionSelection} handleStartDownload={this.handleStartDownload} housekeepingPolicy={auditHousekeepingPolicy || HousekeepingPolicy.Monthly} - {...this.state} + {...auditAppRendererProps} /> - ) : null; + ); } } @@ -82,13 +97,8 @@ const mapDispatchToProps = { fetchValues }; const mapStateToProps = (state: Store) => { const settingValue = getGlobalSettingValue(state, 'sonar.dbcleaner.auditHousekeeping'); - const { adminPages } = getAppState(state); - const hasGovernanceExtension = Boolean( - adminPages?.find(e => e.key === AdminPageExtension.GovernanceConsole) - ); return { - auditHousekeepingPolicy: settingValue?.value as HousekeepingPolicy, - hasGovernanceExtension + auditHousekeepingPolicy: settingValue?.value as HousekeepingPolicy }; }; 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 b4d6a18eca0..1b9740af760 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 @@ -21,6 +21,7 @@ import { subDays } from 'date-fns'; import { shallow } from 'enzyme'; import * as React from 'react'; import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { AdminPageExtension } from '../../../../types/extension'; import { HousekeepingPolicy, RangeOption } from '../../utils'; import { AuditApp } from '../AuditApp'; import AuditAppRenderer from '../AuditAppRenderer'; @@ -31,7 +32,7 @@ it('should render correctly', () => { it('should do nothing if governance is not available', async () => { const fetchValues = jest.fn(); - const wrapper = shallowRender({ fetchValues, hasGovernanceExtension: false }); + const wrapper = shallowRender({ fetchValues, adminPages: [] }); await waitAndUpdate(wrapper); expect(wrapper.type()).toBeNull(); @@ -80,7 +81,7 @@ function shallowRender(props: Partial<AuditApp['props']> = {}) { <AuditApp auditHousekeepingPolicy={HousekeepingPolicy.Monthly} fetchValues={jest.fn()} - hasGovernanceExtension={true} + adminPages={[{ key: AdminPageExtension.GovernanceConsole, name: 'name' }]} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingCount.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingCount.tsx index 6c2ef497549..d1cc1e097c7 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingCount.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingCount.tsx @@ -18,21 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import { colors } from '../../../app/theme'; import { ClearButton } from '../../../components/controls/buttons'; import ConfirmButton from '../../../components/controls/ConfirmButton'; import Tooltip from '../../../components/controls/Tooltip'; import { translate } from '../../../helpers/l10n'; -import { getAppState, Store } from '../../../store/rootReducer'; +import { AppState } from '../../../types/types'; export interface Props { - isSystemAdmin?: boolean; + appState: AppState; onCancelAllPending: () => void; pendingCount?: number; } -export function StatPendingCount({ isSystemAdmin, onCancelAllPending, pendingCount }: Props) { +export function StatPendingCount({ appState, onCancelAllPending, pendingCount }: Props) { if (pendingCount === undefined) { return null; } @@ -42,7 +42,7 @@ export function StatPendingCount({ isSystemAdmin, onCancelAllPending, pendingCou <span className="emphasised-measure">{pendingCount}</span> <span className="little-spacer-left display-inline-flex-center"> {translate('background_tasks.pending')} - {isSystemAdmin && pendingCount > 0 && ( + {appState.canAdmin && pendingCount > 0 && ( <ConfirmButton cancelButtonText={translate('close')} confirmButtonText={translate('background_tasks.cancel_all_tasks.submit')} @@ -62,8 +62,4 @@ export function StatPendingCount({ isSystemAdmin, onCancelAllPending, pendingCou ); } -const mapStateToProps = (state: Store) => ({ - isSystemAdmin: getAppState(state).canAdmin -}); - -export default connect(mapStateToProps)(StatPendingCount); +export default withAppStateContext(StatPendingCount); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/StatPendingCount-test.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/StatPendingCount-test.tsx index f0714f5a1be..4a4086955a6 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/StatPendingCount-test.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/StatPendingCount-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockAppState } from '../../../../helpers/testMocks'; import { Props, StatPendingCount } from '../StatPendingCount'; it('should render correctly', () => { @@ -36,7 +37,7 @@ it('should not show cancel pending button', () => { .exists() ).toBe(false); expect( - shallowRender({ isSystemAdmin: false }) + shallowRender({ appState: mockAppState({ canAdmin: false }) }) .find('ConfirmButton') .exists() ).toBe(false); @@ -53,7 +54,7 @@ it('should trigger cancelling pending', () => { function shallowRender(props: Partial<Props> = {}) { return shallow( <StatPendingCount - isSystemAdmin={true} + appState={mockAppState({ canAdmin: true })} onCancelAllPending={jest.fn()} pendingCount={5} {...props} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Stats-test.tsx.snap b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Stats-test.tsx.snap index c352f81491e..1253b96bb1f 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Stats-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Stats-test.tsx.snap @@ -4,7 +4,7 @@ exports[`should render correctly 1`] = ` <section className="big-spacer-top big-spacer-bottom" > - <Connect(StatPendingCount) + <withAppStateContext(StatPendingCount) onCancelAllPending={[MockFunction]} pendingCount={2} /> @@ -25,7 +25,7 @@ exports[`should render correctly for a component 1`] = ` <section className="big-spacer-top big-spacer-bottom" > - <Connect(StatPendingCount) + <withAppStateContext(StatPendingCount) onCancelAllPending={[MockFunction]} pendingCount={2} /> diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordApp.tsx b/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordApp.tsx index 80c821e4c79..5bb5d8b601b 100644 --- a/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordApp.tsx +++ b/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordApp.tsx @@ -18,16 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; import { changePassword } from '../../api/users'; +import withAppStateContext from '../../app/components/app-state/withAppStateContext'; import { Location, withRouter } from '../../components/hoc/withRouter'; -import { getAppState, Store } from '../../store/rootReducer'; +import { AppState } from '../../types/types'; import ChangeAdminPasswordAppRenderer from './ChangeAdminPasswordAppRenderer'; import { DEFAULT_ADMIN_LOGIN, DEFAULT_ADMIN_PASSWORD } from './constants'; interface Props { - canAdmin?: boolean; - instanceUsesDefaultAdminCredentials?: boolean; + appState: AppState; location: Location; } @@ -49,7 +48,7 @@ export class ChangeAdminPasswordApp extends React.PureComponent<Props, State> { passwordValue: '', confirmPasswordValue: '', submitting: false, - success: !props.instanceUsesDefaultAdminCredentials + success: !props.appState.instanceUsesDefaultAdminCredentials }; } @@ -93,7 +92,10 @@ export class ChangeAdminPasswordApp extends React.PureComponent<Props, State> { }; render() { - const { canAdmin, location } = this.props; + const { + appState: { canAdmin }, + location + } = this.props; const { canSubmit, confirmPasswordValue, passwordValue, submitting, success } = this.state; return ( <ChangeAdminPasswordAppRenderer @@ -112,9 +114,4 @@ export class ChangeAdminPasswordApp extends React.PureComponent<Props, State> { } } -export const mapStateToProps = (state: Store) => { - const { canAdmin, instanceUsesDefaultAdminCredentials } = getAppState(state); - return { canAdmin, instanceUsesDefaultAdminCredentials }; -}; - -export default connect(mapStateToProps)(withRouter(ChangeAdminPasswordApp)); +export default withRouter(withAppStateContext(ChangeAdminPasswordApp)); diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx index cf173422826..fe8e3e54412 100644 --- a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx @@ -20,21 +20,15 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { changePassword } from '../../../api/users'; -import { mockLocation } from '../../../helpers/testMocks'; +import { mockAppState, mockLocation } from '../../../helpers/testMocks'; import { waitAndUpdate } from '../../../helpers/testUtils'; -import { getAppState, Store } from '../../../store/rootReducer'; -import { ChangeAdminPasswordApp, mapStateToProps } from '../ChangeAdminPasswordApp'; +import { ChangeAdminPasswordApp } from '../ChangeAdminPasswordApp'; import { DEFAULT_ADMIN_LOGIN, DEFAULT_ADMIN_PASSWORD } from '../constants'; jest.mock('react-redux', () => ({ connect: jest.fn(() => (a: any) => a) })); -jest.mock('../../../store/rootReducer', () => ({ - ...jest.requireActual('../../../store/rootReducer'), - getAppState: jest.fn() -})); - jest.mock('../../../api/users', () => ({ changePassword: jest.fn().mockResolvedValue(null) })); @@ -43,9 +37,9 @@ beforeEach(jest.clearAllMocks); it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot('default'); - expect(shallowRender({ instanceUsesDefaultAdminCredentials: undefined })).toMatchSnapshot( - 'admin is not using the default password' - ); + expect( + shallowRender({ appState: mockAppState({ instanceUsesDefaultAdminCredentials: undefined }) }) + ).toMatchSnapshot('admin is not using the default password'); }); it('should correctly handle input changes', () => { @@ -99,29 +93,10 @@ it('should correctly update the password', async () => { expect(wrapper.state().success).toBe(false); }); -describe('redux', () => { - it('should correctly map state props', () => { - (getAppState as jest.Mock) - .mockReturnValueOnce({}) - .mockReturnValueOnce({ canAdmin: false, instanceUsesDefaultAdminCredentials: true }); - - expect(mapStateToProps({} as Store)).toEqual({ - canAdmin: undefined, - instanceUsesDefaultAdminCredentials: undefined - }); - - expect(mapStateToProps({} as Store)).toEqual({ - canAdmin: false, - instanceUsesDefaultAdminCredentials: true - }); - }); -}); - function shallowRender(props: Partial<ChangeAdminPasswordApp['props']> = {}) { return shallow<ChangeAdminPasswordApp>( <ChangeAdminPasswordApp - canAdmin={true} - instanceUsesDefaultAdminCredentials={true} + appState={mockAppState({ canAdmin: true, instanceUsesDefaultAdminCredentials: true })} location={mockLocation()} {...props} /> 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 5371a8f4386..e7e4585e6ae 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 @@ -2,7 +2,6 @@ exports[`should render correctly: admin is not using the default password 1`] = ` <ChangeAdminPasswordAppRenderer - canAdmin={true} confirmPasswordValue="" location={ Object { 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 ef4601199f1..fa64d3cee56 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 @@ -20,8 +20,8 @@ import * as React from 'react'; import { Link } from 'react-router'; import { getFacet } from '../../../api/issues'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import Tooltip from '../../../components/controls/Tooltip'; -import { withAppState } from '../../../components/hoc/withAppState'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; @@ -29,7 +29,7 @@ import { getIssuesUrl } from '../../../helpers/urls'; import { AppState, RuleDetails } from '../../../types/types'; interface Props { - appState: Pick<AppState, 'branchesEnabled'>; + appState: AppState; ruleDetails: Pick<RuleDetails, 'key' | 'type'>; } @@ -173,4 +173,4 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> { } } -export default withAppState(RuleDetailsIssues); +export default withAppStateContext(RuleDetailsIssues); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsIssues-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsIssues-test.tsx index 207b08648f7..7aeb0542100 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsIssues-test.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsIssues-test.tsx @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { getFacet } from '../../../../api/issues'; +import { mockAppState } from '../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../helpers/testUtils'; import { RuleDetailsIssues } from '../RuleDetailsIssues'; @@ -59,7 +60,7 @@ it('should fetch issues and render', async () => { function shallowRender(props: Partial<RuleDetailsIssues['props']> = {}) { return shallow( <RuleDetailsIssues - appState={{ branchesEnabled: false }} + appState={mockAppState({ branchesEnabled: false })} ruleDetails={{ key: 'foo', type: 'BUG' }} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap index e7230ad913a..5de95675322 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap @@ -156,7 +156,7 @@ exports[`should render correctly: loaded 1`] = ` } } /> - <Connect(withAppState(RuleDetailsIssues)) + <withAppStateContext(RuleDetailsIssues) ruleDetails={ Object { "createdAt": "2014-12-16T17:26:54+0100", diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx index 7e4819ac484..3cd2701916e 100644 --- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx @@ -19,7 +19,7 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { withAppState } from '../../../components/hoc/withAppState'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import ChevronsIcon from '../../../components/icons/ChevronsIcon'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/system'; @@ -31,7 +31,7 @@ export interface CreateProjectModeSelectionProps { almCounts: { [k in AlmKeys]: number; }; - appState: Pick<AppState, 'canAdmin'>; + appState: AppState; loadingBindings: boolean; onSelectMode: (mode: CreateProjectModes) => void; onConfigMode: (mode: AlmKeys) => void; @@ -166,4 +166,4 @@ export function CreateProjectModeSelection(props: CreateProjectModeSelectionProp ); } -export default withAppState(CreateProjectModeSelection); +export default withAppStateContext(CreateProjectModeSelection); 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 a97188e9969..6f250980373 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 @@ -22,8 +22,8 @@ import { Helmet } from 'react-helmet-async'; import { WithRouterProps } from 'react-router'; import { getAlmSettings } from '../../../api/alm-settings'; import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn'; -import { withAppState } from '../../../components/hoc/withAppState'; import { translate } from '../../../helpers/l10n'; import { getProjectUrl } from '../../../helpers/urls'; import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; @@ -40,7 +40,7 @@ import './style.css'; import { CreateProjectModes } from './types'; interface Props extends Pick<WithRouterProps, 'router' | 'location'> { - appState: Pick<AppState, 'canAdmin' | 'branchesEnabled'>; + appState: AppState; currentUser: LoggedInUser; } @@ -272,4 +272,4 @@ export class CreateProjectPage extends React.PureComponent<Props, State> { } } -export default whenLoggedIn(withAppState(CreateProjectPage)); +export default whenLoggedIn(withAppStateContext(CreateProjectPage)); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx index f9cee16a180..4be1b325d6a 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockAppState } from '../../../../helpers/testMocks'; import { click } from '../../../../helpers/testUtils'; import { AlmKeys } from '../../../../types/alm-settings'; import { @@ -35,19 +36,19 @@ it('should render correctly', () => { ); expect( shallowRender( - { appState: { canAdmin: true } }, + { appState: mockAppState({ canAdmin: true }) }, { [AlmKeys.BitbucketServer]: 0, [AlmKeys.GitHub]: 2 } ) ).toMatchSnapshot('invalid configs, admin'); expect( shallowRender( - { appState: { canAdmin: true } }, + { appState: mockAppState({ canAdmin: true }) }, { [AlmKeys.BitbucketServer]: 0, [AlmKeys.BitbucketCloud]: 0, [AlmKeys.GitHub]: 2 } ) ).toMatchSnapshot('invalid configs, admin'); expect( shallowRender( - { appState: { canAdmin: true } }, + { appState: mockAppState({ canAdmin: true }) }, { [AlmKeys.Azure]: 0, [AlmKeys.BitbucketCloud]: 0, @@ -118,7 +119,7 @@ it('should call the proper click handler', () => { onSelectMode.mockClear(); wrapper = shallowRender( - { onSelectMode, onConfigMode, appState: { canAdmin: true } }, + { onSelectMode, onConfigMode, appState: mockAppState({ canAdmin: true }) }, { [AlmKeys.Azure]: 0 } ); @@ -144,7 +145,7 @@ function shallowRender( return shallow<CreateProjectModeSelectionProps>( <CreateProjectModeSelection almCounts={almCounts} - appState={{ canAdmin: false }} + appState={mockAppState({ canAdmin: false })} loadingBindings={false} onSelectMode={jest.fn()} onConfigMode={jest.fn()} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx index 59c21d71170..541f478f55c 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx @@ -20,7 +20,12 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { getAlmSettings } from '../../../../api/alm-settings'; -import { mockLocation, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks'; +import { + mockAppState, + mockLocation, + mockLoggedInUser, + mockRouter +} from '../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../helpers/testUtils'; import { AlmKeys } from '../../../../types/alm-settings'; import AlmBindingDefinitionForm from '../../../settings/components/almIntegration/AlmBindingDefinitionForm'; @@ -124,7 +129,7 @@ it('should submit alm configuration creation properly for BBC', async () => { function shallowRender(props: Partial<CreateProjectPage['props']> = {}) { return shallow<CreateProjectPage>( <CreateProjectPage - appState={{}} + appState={mockAppState()} currentUser={mockLoggedInUser()} location={mockLocation()} router={mockRouter()} 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 4b824a163f9..70474361ec7 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 @@ -16,7 +16,7 @@ exports[`should render alm configuration creation correctly 1`] = ` className="page page-limited huge-spacer-bottom position-relative" id="create-project" > - <Connect(withAppState(CreateProjectModeSelection)) + <withAppStateContext(CreateProjectModeSelection) almCounts={ Object { "azure": 0, @@ -57,7 +57,7 @@ exports[`should render correctly 1`] = ` className="page page-limited huge-spacer-bottom position-relative" id="create-project" > - <Connect(withAppState(CreateProjectModeSelection)) + <withAppStateContext(CreateProjectModeSelection) almCounts={ Object { "azure": 0, diff --git a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx index 7ba8b943083..379c082bfe8 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx @@ -20,38 +20,47 @@ import * as React from 'react'; import { connect } from 'react-redux'; import AdminContext from '../../app/components/AdminContext'; -import { getAppState, getGlobalSettingValue, Store } from '../../store/rootReducer'; +import withAppStateContext from '../../app/components/app-state/withAppStateContext'; +import { getGlobalSettingValue, Store } from '../../store/rootReducer'; import { EditionKey } from '../../types/editions'; -import { RawQuery } from '../../types/types'; +import { AppState, RawQuery } from '../../types/types'; import App from './App'; interface OwnProps { location: { pathname: string; query: RawQuery }; + appState: AppState; } interface StateToProps { - currentEdition?: EditionKey; - standaloneMode?: boolean; updateCenterActive: boolean; } const mapStateToProps = (state: Store) => { const updateCenterActive = getGlobalSettingValue(state, 'sonar.updatecenter.activate'); return { - currentEdition: getAppState(state).edition as EditionKey, // TODO: Fix once AppState is no longer ambiant. - standaloneMode: getAppState(state).standalone, updateCenterActive: Boolean(updateCenterActive && updateCenterActive.value === 'true') }; }; function WithAdminContext(props: StateToProps & OwnProps) { + const propsToPass = { + location: props.location, + updateCenterActive: props.updateCenterActive, + currentEdition: props.appState.edition as EditionKey, + standaloneMode: props.appState.standalone + }; + return ( <AdminContext.Consumer> {({ fetchPendingPlugins, pendingPlugins }) => ( - <App fetchPendingPlugins={fetchPendingPlugins} pendingPlugins={pendingPlugins} {...props} /> + <App + fetchPendingPlugins={fetchPendingPlugins} + pendingPlugins={pendingPlugins} + {...propsToPass} + /> )} </AdminContext.Consumer> ); } -export default connect(mapStateToProps)(WithAdminContext); +export default connect(mapStateToProps)(withAppStateContext(WithAdminContext)); diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx index fbafb1047d8..d306400526f 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx @@ -19,8 +19,7 @@ */ import { connect } from 'react-redux'; import { mockStore } from '../../../helpers/testMocks'; -import { getAppState, getGlobalSettingValue } from '../../../store/rootReducer'; -import { EditionKey } from '../../../types/editions'; +import { getGlobalSettingValue } from '../../../store/rootReducer'; import '../AppContainer'; jest.mock('react-redux', () => ({ @@ -29,7 +28,6 @@ jest.mock('react-redux', () => ({ jest.mock('../../../store/rootReducer', () => { return { - getAppState: jest.fn(), getGlobalSettingValue: jest.fn() }; }); @@ -37,10 +35,7 @@ jest.mock('../../../store/rootReducer', () => { describe('redux', () => { it('should correctly map state and dispatch props', () => { const store = mockStore(); - const edition = EditionKey.developer; - const standalone = true; const updateCenterActive = true; - (getAppState as jest.Mock).mockReturnValue({ edition, standalone }); (getGlobalSettingValue as jest.Mock).mockReturnValueOnce({ value: `${updateCenterActive}` }); @@ -49,8 +44,6 @@ describe('redux', () => { const props = mapStateToProps(store); expect(props).toEqual({ - currentEdition: edition, - standaloneMode: standalone, updateCenterActive }); 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 bfa2e17b359..665b63e6ddc 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 @@ -18,8 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; -import { withAppState } from '../../../components/hoc/withAppState'; import { Router, withRouter } from '../../../components/hoc/withRouter'; import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; import { isPullRequest } from '../../../helpers/branch-like'; @@ -33,7 +33,7 @@ const EmptyOverview = lazyLoadComponent(() => import('./EmptyOverview')); const PullRequestOverview = lazyLoadComponent(() => import('../pullRequests/PullRequestOverview')); interface Props { - appState: Pick<AppState, 'branchesEnabled'>; + appState: AppState; branchLike?: BranchLike; branchLikes: BranchLike[]; component: Component; @@ -93,4 +93,4 @@ export class App extends React.PureComponent<Props> { } } -export default withRouter(withAppState(App)); +export default withRouter(withAppStateContext(App)); 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 8f1f508aca0..f5fea8b899a 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 @@ -20,12 +20,11 @@ import { Location } from 'history'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { connect } from 'react-redux'; import { getPermissionTemplates } from '../../../api/permissions'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; import { translate } from '../../../helpers/l10n'; -import { getAppState, Store } from '../../../store/rootReducer'; -import { Permission, PermissionTemplate } from '../../../types/types'; +import { AppState, Permission, PermissionTemplate } from '../../../types/types'; import '../../permissions/styles.css'; import { mergeDefaultsToTemplates, mergePermissionsToTemplates, sortPermissions } from '../utils'; import Home from './Home'; @@ -33,7 +32,7 @@ import Template from './Template'; interface Props { location: Location; - topQualifiers: string[]; + appState: AppState; } interface State { @@ -90,7 +89,7 @@ export class App extends React.PureComponent<Props, State> { <Template refresh={this.requestPermissions} template={template} - topQualifiers={this.props.topQualifiers} + topQualifiers={this.props.appState.qualifiers} /> ); } @@ -102,7 +101,7 @@ export class App extends React.PureComponent<Props, State> { permissions={this.state.permissions} ready={this.state.ready} refresh={this.requestPermissions} - topQualifiers={this.props.topQualifiers} + topQualifiers={this.props.appState.qualifiers} /> ); } @@ -121,6 +120,4 @@ export class App extends React.PureComponent<Props, State> { } } -const mapStateToProps = (state: Store) => ({ topQualifiers: getAppState(state).qualifiers }); - -export default connect(mapStateToProps)(App); +export default withAppStateContext(App); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/App-test.tsx index fa5469f91b5..7dbbdfa0f9f 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/App-test.tsx @@ -19,7 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { mockLocation } from '../../../../helpers/testMocks'; +import { mockAppState, mockLocation } from '../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../helpers/testUtils'; import { App } from '../App'; @@ -54,5 +54,7 @@ it('should render correctly', async () => { }); function shallowRender(props: Partial<App['props']> = {}) { - return shallow(<App location={mockLocation()} topQualifiers={['TRK']} {...props} />); + return shallow( + <App location={mockLocation()} appState={mockAppState({ qualifiers: ['TRK'] })} {...props} /> + ); } diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx index def9d1e4ae7..0f2d5f98748 100644 --- a/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx @@ -18,9 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; +import withAppStateContext from '../../../../app/components/app-state/withAppStateContext'; import ListFooter from '../../../../components/controls/ListFooter'; -import { getAppState, Store } from '../../../../store/rootReducer'; import { ComponentQualifier } from '../../../../types/component'; import { AppState, Paging, PermissionGroup, PermissionUser } from '../../../../types/types'; import HoldersList from '../../shared/components/HoldersList'; @@ -32,7 +31,7 @@ import { } from '../../utils'; interface StateProps { - appState: Pick<AppState, 'qualifiers'>; + appState: AppState; } interface OwnProps { @@ -60,9 +59,8 @@ export class AllHoldersList extends React.PureComponent<Props> { const hasPermission = user.permissions.includes(permission); if (hasPermission) { return this.props.revokePermissionFromUser(user.login, permission); - } else { - return this.props.grantPermissionToUser(user.login, permission); } + return this.props.grantPermissionToUser(user.login, permission); }; handleToggleGroup = (group: PermissionGroup, permission: string) => { @@ -70,13 +68,21 @@ export class AllHoldersList extends React.PureComponent<Props> { if (hasPermission) { return this.props.revokePermissionFromGroup(group.name, permission); - } else { - return this.props.grantPermissionToGroup(group.name, permission); } + return this.props.grantPermissionToGroup(group.name, permission); }; render() { - const { appState, filter, groups, groupsPaging, users, usersPaging } = this.props; + const { + appState, + filter, + groups, + groupsPaging, + users, + usersPaging, + loading, + query + } = this.props; const l10nPrefix = 'global_permissions'; const hasPortfoliosEnabled = appState.qualifiers.includes(ComponentQualifier.Portfolio); @@ -100,19 +106,19 @@ export class AllHoldersList extends React.PureComponent<Props> { return ( <> <HoldersList - filter={this.props.filter} - groups={this.props.groups} - loading={this.props.loading} + filter={filter} + groups={groups} + loading={loading} onToggleGroup={this.handleToggleGroup} onToggleUser={this.handleToggleUser} permissions={permissions} - query={this.props.query} - users={this.props.users}> + query={query} + users={users}> <SearchForm - filter={this.props.filter} + filter={filter} onFilter={this.props.onFilter} onSearch={this.props.onSearch} - query={this.props.query} + query={query} /> </HoldersList> <ListFooter count={count} loadMore={this.props.onLoadMore} total={total} /> @@ -121,8 +127,4 @@ export class AllHoldersList extends React.PureComponent<Props> { } } -const mapStateToProps = (state: Store): StateProps => ({ - appState: getAppState(state) -}); - -export default connect(mapStateToProps)(AllHoldersList); +export default withAppStateContext(AllHoldersList); diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/AllHoldersList-test.tsx b/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/AllHoldersList-test.tsx index 354bbf20c36..7ca580ec0db 100644 --- a/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/AllHoldersList-test.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/AllHoldersList-test.tsx @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockPermissionGroup, mockPermissionUser } from '../../../../../helpers/mocks/permissions'; +import { mockAppState } from '../../../../../helpers/testMocks'; import { ComponentQualifier } from '../../../../../types/component'; import { AllHoldersList } from '../AllHoldersList'; @@ -29,12 +30,16 @@ it('should render correctly', () => { expect(shallowRender({ filter: 'groups' })).toMatchSnapshot('filter groups'); expect( shallowRender({ - appState: { qualifiers: [ComponentQualifier.Project, ComponentQualifier.Application] } + appState: mockAppState({ + qualifiers: [ComponentQualifier.Project, ComponentQualifier.Application] + }) }) ).toMatchSnapshot('applications available'); expect( shallowRender({ - appState: { qualifiers: [ComponentQualifier.Project, ComponentQualifier.Portfolio] } + appState: mockAppState({ + qualifiers: [ComponentQualifier.Project, ComponentQualifier.Portfolio] + }) }) ).toMatchSnapshot('portfolios available'); }); @@ -74,7 +79,7 @@ it('should correctly toggle group permissions', () => { function shallowRender(props: Partial<AllHoldersList['props']> = {}) { return shallow<AllHoldersList>( <AllHoldersList - appState={{ qualifiers: [ComponentQualifier.Project] }} + appState={mockAppState({ qualifiers: [ComponentQualifier.Project] })} filter="" grantPermissionToGroup={jest.fn()} grantPermissionToUser={jest.fn()} diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/__snapshots__/App-test.tsx.snap index 9ccdf81dfce..bf85f340bc3 100644 --- a/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/__snapshots__/App-test.tsx.snap @@ -16,7 +16,7 @@ exports[`should render correctly 1`] = ` <PageHeader loading={true} /> - <Connect(AllHoldersList) + <withAppStateContext(AllHoldersList) filter="all" grantPermissionToGroup={[Function]} grantPermissionToUser={[Function]} @@ -50,7 +50,7 @@ exports[`should render correctly 2`] = ` <PageHeader loading={false} /> - <Connect(AllHoldersList) + <withAppStateContext(AllHoldersList) filter="all" grantPermissionToGroup={[Function]} grantPermissionToUser={[Function]} 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 eb380cbc7b1..d515f2294e7 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 @@ -21,6 +21,7 @@ import classNames from 'classnames'; 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 Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; @@ -28,6 +29,7 @@ import { isBranch, sortBranches } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { Branch, BranchLike } from '../../../types/branch-like'; import { + AppState, Component, NewCodePeriod, NewCodePeriodSettingType, @@ -42,9 +44,8 @@ import ProjectBaselineSelector from './ProjectBaselineSelector'; interface Props { branchLike: Branch; branchLikes: BranchLike[]; - branchesEnabled?: boolean; - canAdmin?: boolean; component: Component; + appState: AppState; } interface State { @@ -68,7 +69,7 @@ const DEFAULT_GENERAL_SETTING: { type: NewCodePeriodSettingType } = { type: 'PREVIOUS_VERSION' }; -export default class App extends React.PureComponent<Props, State> { +export class App extends React.PureComponent<Props, State> { mounted = false; state: State = { branchList: [], @@ -127,14 +128,14 @@ export default class App extends React.PureComponent<Props, State> { } fetchLeakPeriodSetting() { - const { branchLike, branchesEnabled, component } = this.props; + const { branchLike, appState, component } = this.props; this.setState({ loading: true }); Promise.all([ getNewCodePeriod(), getNewCodePeriod({ - branch: branchesEnabled ? undefined : branchLike.name, + branch: appState.branchesEnabled ? undefined : branchLike.name, project: component.key }) ]).then( @@ -235,7 +236,7 @@ export default class App extends React.PureComponent<Props, State> { }; render() { - const { branchesEnabled, canAdmin, component, branchLike } = this.props; + const { appState, component, branchLike } = this.props; const { analysis, branchList, @@ -255,19 +256,19 @@ export default class App extends React.PureComponent<Props, State> { <> <Suggestions suggestions="project_baseline" /> <div className="page page-limited"> - <AppHeader canAdmin={!!canAdmin} /> + <AppHeader canAdmin={!!appState.canAdmin} /> {loading ? ( <DeferredSpinner /> ) : ( <div className="panel-white project-baseline"> - {branchesEnabled && <h2>{translate('project_baseline.default_setting')}</h2>} + {appState.branchesEnabled && <h2>{translate('project_baseline.default_setting')}</h2>} {generalSetting && overrideGeneralSetting !== undefined && ( <ProjectBaselineSelector analysis={analysis} branch={branchLike} branchList={branchList} - branchesEnabled={branchesEnabled} + branchesEnabled={appState.branchesEnabled} component={component.key} currentSetting={currentSetting} currentSettingValue={currentSettingValue} @@ -293,7 +294,7 @@ export default class App extends React.PureComponent<Props, State> { {translate('settings.state.saved')} </span> </div> - {generalSetting && branchesEnabled && ( + {generalSetting && appState.branchesEnabled && ( <div className="huge-spacer-top branch-baseline-selector"> <hr /> <h2>{translate('project_baseline.configure_branches')}</h2> @@ -318,3 +319,5 @@ export default class App extends React.PureComponent<Props, State> { ); } } + +export default withAppStateContext(App); diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx index f05c9404271..e99eb125f1d 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx @@ -26,9 +26,9 @@ import { } from '../../../../api/newCodePeriod'; import { mockBranch, mockMainBranch, mockPullRequest } from '../../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../../helpers/mocks/component'; -import { mockEvent } from '../../../../helpers/testMocks'; +import { mockAppState, mockEvent } from '../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../helpers/testUtils'; -import App from '../App'; +import { App } from '../App'; jest.mock('../../../../api/newCodePeriod', () => ({ getNewCodePeriod: jest.fn().mockResolvedValue({}), @@ -41,7 +41,7 @@ it('should render correctly', async () => { await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); - wrapper = shallowRender({ branchesEnabled: false }); + wrapper = shallowRender({ appState: mockAppState({ branchesEnabled: false, canAdmin: true }) }); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot('without branch support'); }); @@ -109,8 +109,7 @@ function shallowRender(props: Partial<App['props']> = {}) { <App branchLike={mockBranch()} branchLikes={[mockMainBranch()]} - branchesEnabled={true} - canAdmin={true} + appState={mockAppState({ branchesEnabled: true, canAdmin: true })} component={mockComponent()} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/routes.ts b/server/sonar-web/src/main/js/apps/projectBaseline/routes.ts index 16e3be4e88a..9d8f2bd42e4 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/routes.ts +++ b/server/sonar-web/src/main/js/apps/projectBaseline/routes.ts @@ -21,7 +21,7 @@ import { lazyLoadComponent } from '../../components/lazyLoadComponent'; const routes = [ { - indexRoute: { component: lazyLoadComponent(() => import('./components/AppContainer')) } + indexRoute: { component: lazyLoadComponent(() => import('./components/App')) } } ]; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/LifetimeInformation.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/LifetimeInformation.tsx index 05c1e3b2f71..3e855acdfa6 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/LifetimeInformation.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/LifetimeInformation.tsx @@ -18,14 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; import { getValues } from '../../../api/settings'; -import { getAppState, Store } from '../../../store/rootReducer'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import { SettingsKey } from '../../../types/settings'; +import { AppState } from '../../../types/types'; import LifetimeInformationRenderer from './LifetimeInformationRenderer'; interface Props { - canAdmin?: boolean; + appState: AppState; } interface State { @@ -65,7 +65,9 @@ export class LifetimeInformation extends React.PureComponent<Props, State> { } render() { - const { canAdmin } = this.props; + const { + appState: { canAdmin } + } = this.props; const { branchAndPullRequestLifeTimeInDays, loading } = this.state; return ( @@ -78,8 +80,4 @@ export class LifetimeInformation extends React.PureComponent<Props, State> { } } -const mapStoreToProps = (state: Store) => ({ - canAdmin: getAppState(state).canAdmin -}); - -export default connect(mapStoreToProps)(LifetimeInformation); +export default withAppStateContext(LifetimeInformation); diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LifetimeInformation-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LifetimeInformation-test.tsx index 65326bee132..f34d9e24983 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LifetimeInformation-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LifetimeInformation-test.tsx @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { getValues } from '../../../../api/settings'; +import { mockAppState } from '../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../helpers/testUtils'; import { SettingsKey } from '../../../../types/settings'; import { LifetimeInformation } from '../LifetimeInformation'; @@ -41,5 +42,7 @@ it('should render correctly', async () => { }); function shallowRender(props: Partial<LifetimeInformation['props']> = {}) { - return shallow<LifetimeInformation>(<LifetimeInformation canAdmin={true} {...props} />); + return shallow<LifetimeInformation>( + <LifetimeInformation appState={mockAppState({ canAdmin: true })} {...props} /> + ); } diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap index 0bd2000c616..e673ebd9ff5 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap @@ -11,7 +11,7 @@ exports[`should render correctly 1`] = ` <h1> project_branch_pull_request.page </h1> - <Connect(LifetimeInformation) /> + <withAppStateContext(LifetimeInformation) /> </header> <BranchLikeTabs branchLikes={ 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 0ad4fcc38de..cd067423adf 100644 --- a/server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx @@ -20,8 +20,8 @@ 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 throwGlobalError from '../../app/utils/throwGlobalError'; -import { withAppState } from '../../components/hoc/withAppState'; import { translate } from '../../helpers/l10n'; import { DumpStatus, DumpTask } from '../../types/project-dump'; import { TaskStatuses, TaskTypes } from '../../types/tasks'; @@ -33,7 +33,7 @@ import './styles.css'; const POLL_INTERNAL = 5000; interface Props { - appState: Pick<AppState, 'projectImportFeatureEnabled'>; + appState: AppState; component: Component; } @@ -198,4 +198,4 @@ export class ProjectDumpApp extends React.Component<Props, State> { } } -export default withAppState(ProjectDumpApp); +export default withAppStateContext(ProjectDumpApp); 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 be94169551e..38fa2a56eaf 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 @@ -21,6 +21,7 @@ import { omitBy } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; import ListFooter from '../../../components/controls/ListFooter'; @@ -33,7 +34,7 @@ import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages'; import { get, save } from '../../../helpers/storage'; import { isLoggedIn } from '../../../helpers/users'; import { ComponentQualifier } from '../../../types/component'; -import { CurrentUser, RawQuery } from '../../../types/types'; +import { AppState, CurrentUser, RawQuery } from '../../../types/types'; import { hasFilterParams, hasViewParams, parseUrlQuery, Query } from '../query'; import '../styles.css'; import { Facets, Project } from '../types'; @@ -46,7 +47,7 @@ interface Props { currentUser: CurrentUser; isFavorite: boolean; location: Pick<Location, 'pathname' | 'query'>; - qualifiers: ComponentQualifier[]; + appState: AppState; router: Pick<Router, 'push' | 'replace'>; } @@ -226,7 +227,9 @@ export class AllProjects extends React.PureComponent<Props, State> { /> <PageSidebar - applicationsEnabled={this.props.qualifiers.includes(ComponentQualifier.Application)} + applicationsEnabled={this.props.appState.qualifiers.includes( + ComponentQualifier.Application + )} facets={this.state.facets} onClearAll={this.handleClearAll} onQueryChange={this.updateLocationQuery} @@ -313,4 +316,4 @@ export class AllProjects extends React.PureComponent<Props, State> { } } -export default withRouter(AllProjects); +export default withRouter(withAppStateContext(AllProjects)); diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx index 01165132238..c1d5e5491ca 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx @@ -19,12 +19,10 @@ */ import { connect } from 'react-redux'; import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; -import { getAppState, getCurrentUser, Store } from '../../../store/rootReducer'; -import { ComponentQualifier } from '../../../types/component'; +import { getCurrentUser, Store } from '../../../store/rootReducer'; const stateToProps = (state: Store) => ({ - currentUser: getCurrentUser(state), - qualifiers: getAppState(state).qualifiers as ComponentQualifier[] + currentUser: getCurrentUser(state) }); export default connect(stateToProps)(lazyLoadComponent(() => import('./AllProjects'))); diff --git a/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx b/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx index 268fbd19754..4fe00dc2785 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx @@ -19,9 +19,9 @@ */ import * as React from 'react'; import { getComponentNavigation } from '../../../api/nav'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import CreateApplicationForm from '../../../app/components/extensions/CreateApplicationForm'; import { Button } from '../../../components/controls/buttons'; -import { withAppState } from '../../../components/hoc/withAppState'; import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; import { Router, withRouter } from '../../../components/hoc/withRouter'; import { translate } from '../../../helpers/l10n'; @@ -32,7 +32,7 @@ import { Permissions } from '../../../types/permissions'; import { AppState, LoggedInUser } from '../../../types/types'; export interface ApplicationCreationProps { - appState: Pick<AppState, 'qualifiers'>; + appState: AppState; className?: string; currentUser: LoggedInUser; router: Router; @@ -84,4 +84,4 @@ export function ApplicationCreation(props: ApplicationCreationProps) { ); } -export default withAppState(withCurrentUser(withRouter(ApplicationCreation))); +export default withCurrentUser(withRouter(withAppStateContext(ApplicationCreation))); 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 819e956a14a..63b97a12434 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,6 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { get, save } from '../../../../helpers/storage'; +import { mockAppState } from '../../../../helpers/testMocks'; import { ComponentQualifier } from '../../../../types/component'; import { Dict } from '../../../../types/types'; import { AllProjects, LS_PROJECTS_SORT, LS_PROJECTS_VIEW } from '../AllProjects'; @@ -174,7 +175,9 @@ function shallowRender( currentUser={{ isLoggedIn: true }} isFavorite={false} location={{ pathname: '/projects', query: {} }} - qualifiers={[ComponentQualifier.Project, ComponentQualifier.Application]} + appState={mockAppState({ + qualifiers: [ComponentQualifier.Project, ComponentQualifier.Application] + })} router={{ push, replace }} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap index 6f89a384375..8dff4f4dd70 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap @@ -21,7 +21,7 @@ exports[`should render correctly 1`] = ` <Connect(withCurrentUser(ProjectCreationMenu)) className="little-spacer-right" /> - <Connect(withAppState(Connect(withCurrentUser(withRouter(ApplicationCreation))))) + <Connect(withCurrentUser(withRouter(withAppStateContext(ApplicationCreation)))) className="little-spacer-right" /> <Connect(HomePageSelect) @@ -97,7 +97,7 @@ exports[`should render correctly while loading 1`] = ` <Connect(withCurrentUser(ProjectCreationMenu)) className="little-spacer-right" /> - <Connect(withAppState(Connect(withCurrentUser(withRouter(ApplicationCreation))))) + <Connect(withCurrentUser(withRouter(withAppStateContext(ApplicationCreation)))) className="little-spacer-right" /> <Connect(HomePageSelect) diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx index 7b090bbc3ee..be9c7f03620 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx @@ -29,10 +29,10 @@ import ListFooter from '../../components/controls/ListFooter'; import { toShortNotSoISOString } from '../../helpers/dates'; import { translate } from '../../helpers/l10n'; import { hasGlobalPermission } from '../../helpers/users'; -import { getAppState, getCurrentUser, Store } from '../../store/rootReducer'; +import { getCurrentUser, Store } from '../../store/rootReducer'; import { Permissions } from '../../types/permissions'; import { SettingsKey } from '../../types/settings'; -import { AppState, LoggedInUser, Visibility } from '../../types/types'; +import { LoggedInUser, Visibility } from '../../types/types'; import CreateProjectForm from './CreateProjectForm'; import Header from './Header'; import Projects from './Projects'; @@ -40,7 +40,6 @@ import Search from './Search'; export interface Props { currentUser: LoggedInUser; - appState: Pick<AppState, 'qualifiers'>; } interface State { @@ -198,7 +197,7 @@ export class App extends React.PureComponent<Props, State> { }; render() { - const { appState, currentUser } = this.props; + const { currentUser } = this.props; const { defaultProjectVisibility } = this.state; return ( <div className="page page-limited" id="projects-management-page"> @@ -228,7 +227,6 @@ export class App extends React.PureComponent<Props, State> { query={this.state.query} ready={this.state.ready} selection={this.state.selection} - topLevelQualifiers={appState.qualifiers} total={this.state.total} visibility={this.state.visibility} /> @@ -262,7 +260,6 @@ export class App extends React.PureComponent<Props, State> { } const mapStateToProps = (state: Store) => ({ - appState: getAppState(state), currentUser: getCurrentUser(state) as LoggedInUser }); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx index 8f47af0fdf4..4d19a37e27a 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx @@ -20,6 +20,7 @@ import { sortBy } from 'lodash'; import * as React from 'react'; import { Project } from '../../api/components'; +import withAppStateContext from '../../app/components/app-state/withAppStateContext'; import { Button } from '../../components/controls/buttons'; import Checkbox from '../../components/controls/Checkbox'; import DateInput from '../../components/controls/DateInput'; @@ -28,7 +29,7 @@ import SearchBox from '../../components/controls/SearchBox'; import SelectLegacy from '../../components/controls/SelectLegacy'; import QualifierIcon from '../../components/icons/QualifierIcon'; import { translate } from '../../helpers/l10n'; -import { Visibility } from '../../types/types'; +import { AppState, Visibility } from '../../types/types'; import BulkApplyTemplateModal from './BulkApplyTemplateModal'; import DeleteModal from './DeleteModal'; @@ -48,7 +49,7 @@ export interface Props { query: string; ready: boolean; selection: any[]; - topLevelQualifiers: string[]; + appState: AppState; total: number; visibility?: Visibility; } @@ -60,12 +61,12 @@ interface State { const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP']; -export default class Search extends React.PureComponent<Props, State> { +export class Search extends React.PureComponent<Props, State> { mounted = false; state: State = { bulkApplyTemplateModal: false, deleteModal: false }; getQualifierOptions = () => { - const options = this.props.topLevelQualifiers.map(q => ({ + const options = this.props.appState.qualifiers.map(q => ({ label: translate('qualifiers', q), value: q })); @@ -281,3 +282,5 @@ export default class Search extends React.PureComponent<Props, State> { ); } } + +export default withAppStateContext(Search); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx index cc02fe3436f..653e059a155 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx @@ -22,10 +22,9 @@ import * as React from 'react'; import { getComponents } from '../../../api/components'; import { changeProjectDefaultVisibility } from '../../../api/permissions'; import { getValues } from '../../../api/settings'; -import { mockAppState, mockLoggedInUser } from '../../../helpers/testMocks'; +import { mockLoggedInUser } from '../../../helpers/testMocks'; import { waitAndUpdate } from '../../../helpers/testUtils'; import { App, Props } from '../App'; -import Search from '../Search'; jest.mock('lodash', () => { const lodash = jest.requireActual('lodash'); @@ -65,7 +64,7 @@ it('fetches all projects on mount', async () => { it('selects provisioned', () => { const wrapper = shallowRender(); - wrapper.find('Search').prop<Function>('onProvisionedChanged')(true); + wrapper.find('withAppStateContext(Search)').prop<Function>('onProvisionedChanged')(true); expect(getComponents).lastCalledWith({ ...defaultSearchParameters, onProvisionedOnly: true, @@ -76,22 +75,21 @@ it('selects provisioned', () => { it('changes qualifier and resets provisioned', () => { const wrapper = shallowRender(); wrapper.setState({ provisioned: true }); - wrapper.find('Search').prop<Function>('onQualifierChanged')('VW'); + wrapper.find('withAppStateContext(Search)').prop<Function>('onQualifierChanged')('VW'); expect(getComponents).lastCalledWith({ ...defaultSearchParameters, qualifiers: 'VW' }); }); it('searches', () => { const wrapper = shallowRender(); - wrapper.find('Search').prop<Function>('onSearch')('foo'); + wrapper.find('withAppStateContext(Search)').prop<Function>('onSearch')('foo'); expect(getComponents).lastCalledWith({ ...defaultSearchParameters, q: 'foo', qualifiers: 'TRK' }); }); it('should handle date filtering', () => { const wrapper = shallowRender(); - wrapper - .find(Search) - .props() - .onDateChanged(new Date('2019-11-14T06:55:02.663Z')); + wrapper.find('withAppStateContext(Search)').prop<Function>('onDateChanged')( + '2019-11-14T06:55:02.663Z' + ); expect(getComponents).toHaveBeenCalledWith({ ...defaultSearchParameters, qualifiers: 'TRK', @@ -138,10 +136,10 @@ it('selects and deselects projects', async () => { wrapper.find('Projects').prop<Function>('onProjectDeselected')('foo'); expect(wrapper.state('selection')).toEqual(['bar']); - wrapper.find('Search').prop<Function>('onAllDeselected')(); + wrapper.find('withAppStateContext(Search)').prop<Function>('onAllDeselected')(); expect(wrapper.state('selection')).toEqual([]); - wrapper.find('Search').prop<Function>('onAllSelected')(); + wrapper.find('withAppStateContext(Search)').prop<Function>('onAllSelected')(); expect(wrapper.state('selection')).toEqual(['foo', 'bar']); }); @@ -165,7 +163,6 @@ it('creates project', () => { function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { return shallow<App>( <App - appState={mockAppState({ qualifiers: ['TRK', 'VW', 'APP'] })} currentUser={mockLoggedInUser({ login: 'foo', permissions: { global: ['provisioning'] } })} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx index 11dc6ea01f0..b4e3f13b637 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx @@ -19,8 +19,9 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockAppState } from '../../../helpers/testMocks'; import { click } from '../../../helpers/testUtils'; -import Search, { Props } from '../Search'; +import { Props, Search } from '../Search'; it('renders', () => { expect(shallowRender()).toMatchSnapshot(); @@ -37,12 +38,17 @@ it('disables the delete and bulk apply buttons unless a project is selected', () }); it('render qualifiers filter', () => { - expect(shallowRender({ topLevelQualifiers: ['TRK', 'VW', 'APP'] })).toMatchSnapshot(); + expect( + shallowRender({ appState: mockAppState({ qualifiers: ['TRK', 'VW', 'APP'] }) }) + ).toMatchSnapshot(); }); it('updates qualifier', () => { const onQualifierChanged = jest.fn(); - const wrapper = shallowRender({ onQualifierChanged, topLevelQualifiers: ['TRK', 'VW', 'APP'] }); + const wrapper = shallowRender({ + onQualifierChanged, + appState: mockAppState({ qualifiers: ['TRK', 'VW', 'APP'] }) + }); wrapper.find('SelectLegacy[name="projects-qualifier"]').prop<Function>('onChange')({ value: 'VW' }); @@ -129,7 +135,7 @@ function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { query="" ready={true} selection={[]} - topLevelQualifiers={['TRK']} + appState={mockAppState({ qualifiers: ['TRK'] })} total={17} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx index d7ae7b87b7d..81dbb739289 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx @@ -19,11 +19,11 @@ */ import { differenceWith, map, sortBy, uniqBy } from 'lodash'; import * as React from 'react'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; import DocumentationTooltip from '../../../components/common/DocumentationTooltip'; import { Button } from '../../../components/controls/buttons'; import ModalButton from '../../../components/controls/ModalButton'; -import { withAppState } from '../../../components/hoc/withAppState'; import { Alert } from '../../../components/ui/Alert'; import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; import { isDiffMetric } from '../../../helpers/measures'; @@ -39,7 +39,7 @@ import Condition from './Condition'; import ConditionModal from './ConditionModal'; interface Props { - appState: Pick<AppState, 'branchesEnabled'>; + appState: AppState; canEdit: boolean; conditions: ConditionType[]; metrics: Dict<Metric>; @@ -228,4 +228,4 @@ export class Conditions extends React.PureComponent<Props> { } } -export default withAppState(withMetricsContext(Conditions)); +export default withMetricsContext(withAppStateContext(Conditions)); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Conditions-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Conditions-test.tsx index c63143a38da..2963e8c21f2 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Conditions-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Conditions-test.tsx @@ -20,7 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockQualityGate } from '../../../../helpers/mocks/quality-gates'; -import { mockCondition, mockMetric } from '../../../../helpers/testMocks'; +import { mockAppState, mockCondition, mockMetric } from '../../../../helpers/testMocks'; import { MetricKey } from '../../../../types/metrics'; import { Conditions } from '../Conditions'; @@ -57,7 +57,7 @@ it('should render the add conditions button and modal', () => { function shallowRender(props: Partial<Conditions['props']> = {}) { return shallow<Conditions>( <Conditions - appState={{ branchesEnabled: true }} + appState={mockAppState({ branchesEnabled: true })} canEdit={false} conditions={[mockCondition(), mockCondition({ id: 2, metric: MetricKey.duplicated_lines })]} metrics={{ diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsContent-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsContent-test.tsx.snap index 456f401e581..7354a115ce8 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsContent-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsContent-test.tsx.snap @@ -4,7 +4,7 @@ exports[`should render correctly: Admin 1`] = ` <div className="layout-page-main-inner" > - <Connect(withAppState(withMetricsContext(Conditions))) + <withMetricsContext(withAppStateContext(Conditions)) canEdit={false} conditions={Array []} onAddCondition={[MockFunction]} @@ -80,7 +80,7 @@ exports[`should render correctly: is default 1`] = ` <div className="layout-page-main-inner" > - <Connect(withAppState(withMetricsContext(Conditions))) + <withMetricsContext(withAppStateContext(Conditions)) canEdit={false} conditions={ Array [ @@ -150,7 +150,7 @@ exports[`should render correctly: is default, no conditions 1`] = ` > quality_gates.is_default_no_conditions </Alert> - <Connect(withAppState(withMetricsContext(Conditions))) + <withMetricsContext(withAppStateContext(Conditions)) canEdit={false} conditions={Array []} onAddCondition={[MockFunction]} @@ -198,7 +198,7 @@ exports[`should render correctly: is not default 1`] = ` <div className="layout-page-main-inner" > - <Connect(withAppState(withMetricsContext(Conditions))) + <withMetricsContext(withAppStateContext(Conditions)) canEdit={false} conditions={ Array [ 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 c05f1fb30fc..69722c98a05 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,17 +20,16 @@ import classNames from 'classnames'; import { sortBy } from 'lodash'; import * as React from 'react'; -import { connect } from 'react-redux'; import { IndexLink } from 'react-router'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import { getGlobalSettingsUrl, getProjectSettingsUrl } from '../../../helpers/urls'; -import { getAppState, Store } from '../../../store/rootReducer'; -import { Component } from '../../../types/types'; +import { AppState, Component } from '../../../types/types'; import { getCategoryName } from '../utils'; import { ADDITIONAL_CATEGORIES } from './AdditionalCategories'; import CATEGORY_OVERRIDES from './CategoryOverrides'; export interface CategoriesListProps { - branchesEnabled?: boolean; + appState: AppState; categories: string[]; component?: Component; defaultCategory: string; @@ -38,7 +37,7 @@ export interface CategoriesListProps { } export function CategoriesList(props: CategoriesListProps) { - const { branchesEnabled, categories, component, defaultCategory, selectedCategory } = props; + const { appState, categories, component, defaultCategory, selectedCategory } = props; const categoriesWithName = categories .filter(key => !CATEGORY_OVERRIDES[key.toLowerCase()]) @@ -55,7 +54,7 @@ export function CategoriesList(props: CategoriesListProps) { : // Global settings c.availableGlobally ) - .filter(c => branchesEnabled || !c.requiresBranchesEnabled) + .filter(c => appState.branchesEnabled || !c.requiresBranchesEnabled) ); const sortedCategories = sortBy(categoriesWithName, category => category.name.toLowerCase()); @@ -84,8 +83,4 @@ export function CategoriesList(props: CategoriesListProps) { ); } -const mapStateToProps = (state: Store) => ({ - branchesEnabled: getAppState(state).branchesEnabled -}); - -export default connect(mapStateToProps)(CategoriesList); +export default withAppStateContext(CategoriesList); 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 35e919e9f31..3d7a1401872 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 @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockComponent } from '../../../../helpers/mocks/component'; +import { mockAppState } from '../../../../helpers/testMocks'; import { AdditionalCategory } from '../AdditionalCategories'; import { CategoriesList, CategoriesListProps } from '../AllCategoriesList'; @@ -65,13 +66,15 @@ 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({ branchesEnabled: false })).toMatchSnapshot('branches disabled'); + expect(shallowRender({ appState: mockAppState({ branchesEnabled: false }) })).toMatchSnapshot( + 'branches disabled' + ); }); function shallowRender(props?: Partial<CategoriesListProps>) { return shallow<CategoriesListProps>( <CategoriesList - branchesEnabled={true} + appState={mockAppState({ branchesEnabled: true })} categories={['general']} defaultCategory="general" selectedCategory="" diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AdditionalCategories-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AdditionalCategories-test.tsx.snap index 7d3b5667d2b..8787399d313 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AdditionalCategories-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AdditionalCategories-test.tsx.snap @@ -63,7 +63,7 @@ exports[`should render additional categories component correctly 3`] = ` `; exports[`should render additional categories component correctly 4`] = ` -<withRouter(Connect(withAppState(AlmIntegration))) +<withRouter(withAppStateContext(AlmIntegration)) categories={Array []} component={ Object { @@ -93,7 +93,7 @@ exports[`should render additional categories component correctly 4`] = ` `; exports[`should render additional categories component correctly 5`] = ` -<Connect(Connect(withCurrentUser(PRDecorationBinding))) +<Connect(withCurrentUser(PRDecorationBinding)) component={ Object { "breadcrumbs": Array [], diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsAppRenderer-test.tsx.snap index 1b1ea74df0c..a40cc74b2c1 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsAppRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsAppRenderer-test.tsx.snap @@ -50,7 +50,7 @@ exports[`should render almintegration correctly 1`] = ` <div className="big-padded" > - <withRouter(Connect(withAppState(AlmIntegration))) + <withRouter(withAppStateContext(AlmIntegration)) categories={ Array [ "foo category", @@ -177,7 +177,7 @@ exports[`should render default view correctly: All Categories List 1`] = ` <div className="layout-page-side-inner" > - <Connect(CategoriesList) + <withAppStateContext(CategoriesList) categories={ Array [ "foo category", 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 f26b1f7ffc2..7a54c0bc12b 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 @@ -25,7 +25,7 @@ import { getAlmDefinitions, validateAlmSettings } from '../../../../api/alm-settings'; -import { withAppState } from '../../../../components/hoc/withAppState'; +import withAppStateContext from '../../../../app/components/app-state/withAppStateContext'; import { withRouter } from '../../../../components/hoc/withRouter'; import { AlmBindingDefinitionBase, @@ -39,7 +39,7 @@ import { AppState, Dict } from '../../../../types/types'; import AlmIntegrationRenderer from './AlmIntegrationRenderer'; interface Props extends Pick<WithRouterProps, 'location' | 'router'> { - appState: Pick<AppState, 'branchesEnabled' | 'multipleAlmEnabled'>; + appState: AppState; definitions: ExtendedSettingDefinition[]; } @@ -246,4 +246,4 @@ export class AlmIntegration extends React.PureComponent<Props, State> { } } -export default withRouter(withAppState(AlmIntegration)); +export default withRouter(withAppStateContext(AlmIntegration)); diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/CreationTooltip.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/CreationTooltip.tsx index 147f90d8c46..9f53a41396f 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/CreationTooltip.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/CreationTooltip.tsx @@ -19,8 +19,8 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import withAppStateContext from '../../../../app/components/app-state/withAppStateContext'; import Tooltip from '../../../../components/controls/Tooltip'; -import { withAppState } from '../../../../components/hoc/withAppState'; import { getEdition, getEditionUrl } from '../../../../helpers/editions'; import { translate } from '../../../../helpers/l10n'; import { AlmKeys } from '../../../../types/alm-settings'; @@ -73,4 +73,4 @@ export function CreationTooltip(props: CreationTooltipProps) { ); } -export default withAppState(CreationTooltip); +export default withAppStateContext(CreationTooltip); diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-test.tsx index c6f66f233bb..aa701feea92 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-test.tsx @@ -25,7 +25,7 @@ import { getAlmDefinitions, validateAlmSettings } from '../../../../../api/alm-settings'; -import { mockLocation, mockRouter } from '../../../../../helpers/testMocks'; +import { mockAppState, mockLocation, mockRouter } from '../../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../../helpers/testUtils'; import { AlmKeys, AlmSettingsBindingStatusType } from '../../../../../types/alm-settings'; import { AlmIntegration } from '../AlmIntegration'; @@ -189,7 +189,7 @@ it('should detect the current ALM from the query', () => { function shallowRender(props: Partial<AlmIntegration['props']> = {}) { return shallow<AlmIntegration>( <AlmIntegration - appState={{ branchesEnabled: true }} + appState={mockAppState({ branchesEnabled: true })} definitions={[]} location={mockLocation()} router={mockRouter()} diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTabRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTabRenderer-test.tsx.snap index ba36a1e7f3f..b987913a570 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTabRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTabRenderer-test.tsx.snap @@ -13,7 +13,7 @@ exports[`should render correctly for multi-ALM binding: editing a definition 1`] <div className="spacer-bottom text-right" > - <Connect(withAppState(CreationTooltip)) + <withAppStateContext(CreationTooltip) alm="azure" preventCreation={false} > @@ -24,7 +24,7 @@ exports[`should render correctly for multi-ALM binding: editing a definition 1`] > settings.almintegration.create </Button> - </Connect(withAppState(CreationTooltip))> + </withAppStateContext(CreationTooltip)> </div> <AlmBindingDefinitionBox alm="azure" @@ -71,7 +71,7 @@ exports[`should render correctly for multi-ALM binding: loaded 1`] = ` <div className="spacer-bottom text-right" > - <Connect(withAppState(CreationTooltip)) + <withAppStateContext(CreationTooltip) alm="azure" preventCreation={false} > @@ -82,7 +82,7 @@ exports[`should render correctly for multi-ALM binding: loaded 1`] = ` > settings.almintegration.create </Button> - </Connect(withAppState(CreationTooltip))> + </withAppStateContext(CreationTooltip)> </div> <AlmBindingDefinitionBox alm="azure" @@ -129,7 +129,7 @@ exports[`should render correctly for multi-ALM binding: loading ALM definitions <div className="spacer-bottom text-right" > - <Connect(withAppState(CreationTooltip)) + <withAppStateContext(CreationTooltip) alm="azure" preventCreation={false} > @@ -140,7 +140,7 @@ exports[`should render correctly for multi-ALM binding: loading ALM definitions > settings.almintegration.create </Button> - </Connect(withAppState(CreationTooltip))> + </withAppStateContext(CreationTooltip)> </div> <AlmBindingDefinitionBox alm="azure" @@ -187,7 +187,7 @@ exports[`should render correctly for multi-ALM binding: loading project count 1` <div className="spacer-bottom text-right" > - <Connect(withAppState(CreationTooltip)) + <withAppStateContext(CreationTooltip) alm="azure" preventCreation={true} > @@ -198,7 +198,7 @@ exports[`should render correctly for multi-ALM binding: loading project count 1` > settings.almintegration.create </Button> - </Connect(withAppState(CreationTooltip))> + </withAppStateContext(CreationTooltip)> </div> <AlmBindingDefinitionBox alm="azure" @@ -245,7 +245,7 @@ exports[`should render correctly for single-ALM binding 1`] = ` <div className="spacer-bottom text-right" > - <Connect(withAppState(CreationTooltip)) + <withAppStateContext(CreationTooltip) alm="azure" preventCreation={true} > @@ -256,7 +256,7 @@ exports[`should render correctly for single-ALM binding 1`] = ` > settings.almintegration.create </Button> - </Connect(withAppState(CreationTooltip))> + </withAppStateContext(CreationTooltip)> </div> <AlmBindingDefinitionBox alm="azure" @@ -303,7 +303,7 @@ exports[`should render correctly for single-ALM binding 2`] = ` <div className="spacer-bottom text-right" > - <Connect(withAppState(CreationTooltip)) + <withAppStateContext(CreationTooltip) alm="azure" preventCreation={true} > @@ -314,7 +314,7 @@ exports[`should render correctly for single-ALM binding 2`] = ` > settings.almintegration.create </Button> - </Connect(withAppState(CreationTooltip))> + </withAppStateContext(CreationTooltip)> </div> <AlmBindingDefinitionBox alm="azure" @@ -361,7 +361,7 @@ exports[`should render correctly for single-ALM binding 3`] = ` <div className="spacer-bottom text-right" > - <Connect(withAppState(CreationTooltip)) + <withAppStateContext(CreationTooltip) alm="azure" preventCreation={true} > @@ -372,7 +372,7 @@ exports[`should render correctly for single-ALM binding 3`] = ` > settings.almintegration.create </Button> - </Connect(withAppState(CreationTooltip))> + </withAppStateContext(CreationTooltip)> </div> <AlmBindingDefinitionBox alm="azure" @@ -424,7 +424,7 @@ exports[`should render correctly with validation: create a first 1`] = ` <div className="big-spacer-top" > - <Connect(withAppState(CreationTooltip)) + <withAppStateContext(CreationTooltip) alm="azure" preventCreation={false} > @@ -435,7 +435,7 @@ exports[`should render correctly with validation: create a first 1`] = ` > settings.almintegration.create </Button> - </Connect(withAppState(CreationTooltip))> + </withAppStateContext(CreationTooltip)> </div> </DeferredSpinner> </div> @@ -467,7 +467,7 @@ exports[`should render correctly with validation: create a second 1`] = ` <div className="spacer-bottom text-right" > - <Connect(withAppState(CreationTooltip)) + <withAppStateContext(CreationTooltip) alm="azure" preventCreation={false} > @@ -478,7 +478,7 @@ exports[`should render correctly with validation: create a second 1`] = ` > settings.almintegration.create </Button> - </Connect(withAppState(CreationTooltip))> + </withAppStateContext(CreationTooltip)> </div> <AlmBindingDefinitionBox alm="azure" @@ -529,7 +529,7 @@ exports[`should render correctly with validation: default 1`] = ` <div className="spacer-bottom text-right" > - <Connect(withAppState(CreationTooltip)) + <withAppStateContext(CreationTooltip) alm="azure" preventCreation={false} > @@ -540,7 +540,7 @@ exports[`should render correctly with validation: default 1`] = ` > settings.almintegration.create </Button> - </Connect(withAppState(CreationTooltip))> + </withAppStateContext(CreationTooltip)> </div> <AlmBindingDefinitionBox alm="azure" @@ -596,7 +596,7 @@ exports[`should render correctly with validation: empty 1`] = ` <div className="big-spacer-top" > - <Connect(withAppState(CreationTooltip)) + <withAppStateContext(CreationTooltip) alm="azure" preventCreation={false} > @@ -607,7 +607,7 @@ exports[`should render correctly with validation: empty 1`] = ` > settings.almintegration.create </Button> - </Connect(withAppState(CreationTooltip))> + </withAppStateContext(CreationTooltip)> </div> </DeferredSpinner> </div> @@ -639,7 +639,7 @@ exports[`should render correctly with validation: pass the correct key for bitbu <div className="spacer-bottom text-right" > - <Connect(withAppState(CreationTooltip)) + <withAppStateContext(CreationTooltip) alm="bitbucket" preventCreation={false} > @@ -650,7 +650,7 @@ exports[`should render correctly with validation: pass the correct key for bitbu > settings.almintegration.create </Button> - </Connect(withAppState(CreationTooltip))> + </withAppStateContext(CreationTooltip)> </div> <AlmBindingDefinitionBox alm="bitbucketcloud" 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 6020027303c..cd60e263b9d 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 @@ -20,6 +20,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router'; +import withAppStateContext from '../../../../app/components/app-state/withAppStateContext'; import Toggle from '../../../../components/controls/Toggle'; import { Alert } from '../../../../components/ui/Alert'; import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker'; @@ -31,14 +32,15 @@ import { AlmSettingsInstance, ProjectAlmBindingResponse } from '../../../../types/alm-settings'; -import { Dict } from '../../../../types/types'; +import { EditionKey } from '../../../../types/editions'; +import { AppState, Dict } from '../../../../types/types'; export interface AlmSpecificFormProps { alm: AlmKeys; instances: AlmSettingsInstance[]; formData: Omit<ProjectAlmBindingResponse, 'alm'>; onFieldChange: (id: keyof ProjectAlmBindingResponse, value: string | boolean) => void; - monorepoEnabled: boolean; + appState: AppState; } interface LabelProps { @@ -140,12 +142,12 @@ function renderField( ); } -export default function AlmSpecificForm(props: AlmSpecificFormProps) { +export function AlmSpecificForm(props: AlmSpecificFormProps) { const { alm, instances, formData: { repository, slug, summaryCommentEnabled, monorepo }, - monorepoEnabled + appState } = props; let formFields: JSX.Element; @@ -275,6 +277,11 @@ export default function AlmSpecificForm(props: AlmSpecificFormProps) { break; } + // This feature trigger will be replaced when SONAR-14349 is implemented + const monorepoEnabled = [EditionKey.enterprise, EditionKey.datacenter].includes( + appState.edition as EditionKey + ); + return ( <> {formFields} @@ -301,3 +308,5 @@ export default function AlmSpecificForm(props: AlmSpecificFormProps) { </> ); } + +export default withAppStateContext(AlmSpecificForm); diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBinding.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBinding.tsx index 6c1369995f4..885689f50c9 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBinding.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBinding.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; import { deleteProjectAlmBinding, getAlmSettings, @@ -34,24 +33,18 @@ import throwGlobalError from '../../../../app/utils/throwGlobalError'; import { withCurrentUser } from '../../../../components/hoc/withCurrentUser'; import { HttpStatus } from '../../../../helpers/request'; import { hasGlobalPermission } from '../../../../helpers/users'; -import { getAppState, Store } from '../../../../store/rootReducer'; import { AlmKeys, AlmSettingsInstance, ProjectAlmBindingConfigurationErrors, ProjectAlmBindingResponse } from '../../../../types/alm-settings'; -import { EditionKey } from '../../../../types/editions'; import { Permissions } from '../../../../types/permissions'; import { Component, CurrentUser } from '../../../../types/types'; import PRDecorationBindingRenderer from './PRDecorationBindingRenderer'; type FormData = Omit<ProjectAlmBindingResponse, 'alm'>; -interface StateProps { - monorepoEnabled: boolean; -} - interface Props { component: Component; currentUser: CurrentUser; @@ -81,7 +74,7 @@ const REQUIRED_FIELDS_BY_ALM: { [AlmKeys.GitLab]: ['repository'] }; -export class PRDecorationBinding extends React.PureComponent<Props & StateProps, State> { +export class PRDecorationBinding extends React.PureComponent<Props, State> { mounted = false; state: State = { formData: { key: '', monorepo: false }, @@ -343,7 +336,7 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, }; render() { - const { currentUser, monorepoEnabled } = this.props; + const { currentUser } = this.props; return ( <PRDecorationBindingRenderer @@ -351,7 +344,6 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, onReset={this.handleReset} onSubmit={this.handleSubmit} onCheckConfiguration={this.handleCheckConfiguration} - monorepoEnabled={monorepoEnabled} isSysAdmin={hasGlobalPermission(currentUser, Permissions.Admin)} {...this.state} /> @@ -359,11 +351,4 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, } } -const mapStateToProps = (state: Store): StateProps => ({ - // This feature trigger will be replaced when SONAR-14349 is implemented - monorepoEnabled: [EditionKey.enterprise, EditionKey.datacenter].includes( - getAppState(state).edition as EditionKey - ) -}); - -export default connect(mapStateToProps)(withCurrentUser(PRDecorationBinding)); +export default withCurrentUser(PRDecorationBinding); 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 2b113aef557..29162ee65e5 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 @@ -50,7 +50,6 @@ export interface PRDecorationBindingRendererProps { onSubmit: () => void; updating: boolean; successfullyUpdated: boolean; - monorepoEnabled: boolean; onCheckConfiguration: () => void; checkingConfiguration: boolean; configurationErrors?: ProjectAlmBindingConfigurationErrors; @@ -78,7 +77,6 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe loading, updating, successfullyUpdated, - monorepoEnabled, checkingConfiguration, configurationErrors, isSysAdmin @@ -169,7 +167,6 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe instances={instances} formData={formData} onFieldChange={props.onFieldChange} - monorepoEnabled={monorepoEnabled} /> )} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/AlmSpecificForm-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/AlmSpecificForm-test.tsx index 4a7151f0ada..cdc9538d83a 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/AlmSpecificForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/AlmSpecificForm-test.tsx @@ -20,8 +20,10 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockAlmSettingsInstance } from '../../../../../helpers/mocks/alm-settings'; +import { mockAppState } from '../../../../../helpers/testMocks'; import { AlmKeys, AlmSettingsInstance } from '../../../../../types/alm-settings'; -import AlmSpecificForm, { AlmSpecificFormProps } from '../AlmSpecificForm'; +import { EditionKey } from '../../../../../types/editions'; +import { AlmSpecificForm, AlmSpecificFormProps } from '../AlmSpecificForm'; it.each([ [AlmKeys.Azure], @@ -48,7 +50,9 @@ it.each([ ); it('should render the monorepo field when the feature is supported', () => { - expect(shallowRender(AlmKeys.Azure, { monorepoEnabled: true })).toMatchSnapshot(); + expect( + shallowRender(AlmKeys.Azure, { appState: mockAppState({ edition: EditionKey.enterprise }) }) + ).toMatchSnapshot(); }); function shallowRender(alm: AlmKeys, props: Partial<AlmSpecificFormProps> = {}) { @@ -63,7 +67,7 @@ function shallowRender(alm: AlmKeys, props: Partial<AlmSpecificFormProps> = {}) monorepo: false }} onFieldChange={jest.fn()} - monorepoEnabled={false} + appState={mockAppState({ edition: EditionKey.developer })} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx index eeb1756458f..793ee25b46b 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx @@ -420,7 +420,6 @@ function shallowRender(props: Partial<PRDecorationBinding['props']> = {}) { <PRDecorationBinding currentUser={mockCurrentUser()} component={mockComponent({ key: PROJECT_KEY })} - monorepoEnabled={false} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBindingRenderer-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBindingRenderer-test.tsx index 96c6dc4d450..a4bf1454e6d 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBindingRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBindingRenderer-test.tsx @@ -154,7 +154,6 @@ function shallowRender(props: Partial<PRDecorationBindingRendererProps> = {}) { onSubmit={jest.fn()} updating={false} successfullyUpdated={false} - monorepoEnabled={false} checkingConfiguration={false} onCheckConfiguration={jest.fn()} isSysAdmin={false} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBinding-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBinding-test.tsx.snap index eb3d7ad054b..aa67734fdaf 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBinding-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBinding-test.tsx.snap @@ -15,7 +15,6 @@ exports[`should render correctly 1`] = ` isSysAdmin={false} isValid={false} loading={true} - monorepoEnabled={false} onCheckConfiguration={[Function]} onFieldChange={[Function]} onReset={[Function]} 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 1ebb0bf76f4..f1bf70fdb61 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 @@ -172,7 +172,7 @@ exports[`should render correctly: when there are configuration errors (admin use /> </div> </div> - <AlmSpecificForm + <withAppStateContext(AlmSpecificForm) alm="github" formData={ Object { @@ -204,7 +204,6 @@ exports[`should render correctly: when there are configuration errors (admin use }, ] } - monorepoEnabled={false} onFieldChange={[MockFunction]} /> <div @@ -743,7 +742,7 @@ exports[`should render correctly: with a valid and saved form 1`] = ` /> </div> </div> - <AlmSpecificForm + <withAppStateContext(AlmSpecificForm) alm="github" formData={ Object { @@ -775,7 +774,6 @@ exports[`should render correctly: with a valid and saved form 1`] = ` }, ] } - monorepoEnabled={false} onFieldChange={[MockFunction]} /> <div diff --git a/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx index 53cf6c5c629..486c80de4c3 100644 --- a/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx @@ -18,12 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import { ClipboardButton } from '../../../components/controls/clipboard'; import { Alert } from '../../../components/ui/Alert'; import { toShortNotSoISOString } from '../../../helpers/dates'; import { translate } from '../../../helpers/l10n'; -import { getAppState, Store } from '../../../store/rootReducer'; +import { AppState } from '../../../types/types'; import PageActions from './PageActions'; export interface Props { @@ -31,22 +31,14 @@ export interface Props { loading: boolean; logLevel: string; onLogLevelChange: () => void; - productionDatabase: boolean; + appState: AppState; serverId?: string; showActions: boolean; version?: string; } export function PageHeader(props: Props) { - const { - isCluster, - loading, - logLevel, - serverId, - showActions, - version, - productionDatabase - } = props; + const { isCluster, loading, logLevel, serverId, showActions, version, appState } = props; return ( <header className="page-header"> <h1 className="page-title">{translate('system_info.page')}</h1> @@ -67,7 +59,7 @@ export function PageHeader(props: Props) { )} {serverId && version && ( <div className="system-info-copy-paste-id-info boxed-group "> - {!productionDatabase && ( + {!appState.productionDatabase && ( <Alert className="width-100" variant="warning"> {translate('system.not_production_database_warning')} </Alert> @@ -109,8 +101,4 @@ Date: ${toShortNotSoISOString(Date.now())} ); } -const mapStateToProps = (store: Store) => ({ - productionDatabase: getAppState(store).productionDatabase -}); - -export default connect(mapStateToProps)(PageHeader); +export default withAppStateContext(PageHeader); diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx index b79bdf364e1..e8725a325af 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockAppState } from '../../../../helpers/testMocks'; import { PageHeader, Props } from '../PageHeader'; jest.mock('../../../../helpers/dates', () => ({ @@ -28,7 +29,11 @@ jest.mock('../../../../helpers/dates', () => ({ it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); expect( - shallowRender({ productionDatabase: false, serverId: 'foo-bar', version: '7.7.0.1234' }) + shallowRender({ + appState: mockAppState({ productionDatabase: false }), + serverId: 'foo-bar', + version: '7.7.0.1234' + }) ).toMatchSnapshot('on embedded database'); expect(shallowRender({ loading: true, showActions: false })).toMatchSnapshot(); expect(shallowRender({ serverId: 'foo-bar', version: '7.7.0.1234' })).toMatchSnapshot(); @@ -41,7 +46,7 @@ function shallowRender(props: Partial<Props> = {}) { loading={false} logLevel="INFO" onLogLevelChange={jest.fn()} - productionDatabase={true} + appState={mockAppState({ productionDatabase: true })} showActions={true} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/App-test.tsx.snap index 9e6b4ffab23..274ea824bba 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/App-test.tsx.snap @@ -16,11 +16,11 @@ exports[`should render correctly: cluster sysinfo 1`] = ` <div className="page-notifs" > - <Connect(withCurrentUser(Connect(withAppState(UpdateNotification)))) + <Connect(withCurrentUser(withAppStateContext(UpdateNotification))) dismissable={false} /> </div> - <Connect(PageHeader) + <withAppStateContext(PageHeader) isCluster={true} loading={false} logLevel="DEBUG" @@ -209,7 +209,7 @@ exports[`should render correctly: loading 1`] = ` <div className="page-notifs" > - <Connect(withCurrentUser(Connect(withAppState(UpdateNotification)))) + <Connect(withCurrentUser(withAppStateContext(UpdateNotification))) dismissable={false} /> </div> @@ -232,11 +232,11 @@ exports[`should render correctly: stand-alone sysinfo 1`] = ` <div className="page-notifs" > - <Connect(withCurrentUser(Connect(withAppState(UpdateNotification)))) + <Connect(withCurrentUser(withAppStateContext(UpdateNotification))) dismissable={false} /> </div> - <Connect(PageHeader) + <withAppStateContext(PageHeader) isCluster={false} loading={false} logLevel="DEBUG" 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 344f0b1a1ce..1263177bf8f 100644 --- a/server/sonar-web/src/main/js/components/docs/DocLink.tsx +++ b/server/sonar-web/src/main/js/components/docs/DocLink.tsx @@ -19,13 +19,13 @@ */ import * as React from 'react'; import { Link } from 'react-router'; +import withAppStateContext from '../../app/components/app-state/withAppStateContext'; import DetachIcon from '../../components/icons/DetachIcon'; import { isSonarCloud } from '../../helpers/system'; import { AppState } from '../../types/types'; -import { withAppState } from '../hoc/withAppState'; interface OwnProps { - appState: Pick<AppState, 'canAdmin'>; + appState: AppState; customProps?: { [k: string]: any; }; @@ -66,14 +66,13 @@ export class DocLink extends React.PureComponent<Props> { {children} </SonarQubeAdminLink> ); - } else { - const url = '/documentation' + href; - return ( - <Link to={url} {...other}> - {children} - </Link> - ); } + const url = '/documentation' + href; + return ( + <Link to={url} {...other}> + {children} + </Link> + ); } return ( @@ -90,7 +89,7 @@ export class DocLink extends React.PureComponent<Props> { } } -export default withAppState(DocLink); +export default withAppStateContext(DocLink); interface SonarCloudLinkProps { children: React.ReactNode; @@ -100,10 +99,9 @@ interface SonarCloudLinkProps { function SonarCloudLink({ children, url }: SonarCloudLinkProps) { if (!isSonarCloud()) { return <>{children}</>; - } else { - const to = `/${url.substr(SONARCLOUD_LINK.length)}`; - return <Link to={to}>{children}</Link>; } + const to = `/${url.substr(SONARCLOUD_LINK.length)}`; + return <Link to={to}>{children}</Link>; } interface SonarQubeLinkProps { @@ -114,14 +112,13 @@ interface SonarQubeLinkProps { function SonarQubeLink({ children, url }: SonarQubeLinkProps) { if (isSonarCloud()) { return <>{children}</>; - } else { - const to = `/${url.substr(SONARQUBE_LINK.length)}`; - return ( - <Link target="_blank" to={to}> - {children} - </Link> - ); } + const to = `/${url.substr(SONARQUBE_LINK.length)}`; + return ( + <Link target="_blank" to={to}> + {children} + </Link> + ); } interface SonarQubeAdminLinkProps { @@ -133,12 +130,11 @@ interface SonarQubeAdminLinkProps { function SonarQubeAdminLink({ canAdmin, children, url }: SonarQubeAdminLinkProps) { if (isSonarCloud() || !canAdmin) { return <>{children}</>; - } else { - const to = `/${url.substr(SONARQUBE_ADMIN_LINK.length)}`; - return ( - <Link target="_blank" to={to}> - {children} - </Link> - ); } + const to = `/${url.substr(SONARQUBE_ADMIN_LINK.length)}`; + return ( + <Link target="_blank" to={to}> + {children} + </Link> + ); } diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx index 246452af3ca..86e613815e0 100644 --- a/server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx +++ b/server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { isSonarCloud } from '../../../helpers/system'; +import { mockAppState } from '../../../helpers/testMocks'; import { DocLink } from '../DocLink'; jest.mock('../../../helpers/system', () => ({ @@ -29,7 +30,7 @@ jest.mock('../../../helpers/system', () => ({ it('should render simple link', () => { expect( shallow( - <DocLink appState={{ canAdmin: false }} href="http://sample.com"> + <DocLink appState={mockAppState({ canAdmin: false })} href="http://sample.com"> link text </DocLink> ) @@ -39,7 +40,7 @@ it('should render simple link', () => { it('should render documentation link', () => { expect( shallow( - <DocLink appState={{ canAdmin: false }} href="/foo/bar"> + <DocLink appState={mockAppState({ canAdmin: false })} href="/foo/bar"> link text </DocLink> ) @@ -49,7 +50,7 @@ it('should render documentation link', () => { it('should render sonarcloud link on sonarcloud', () => { (isSonarCloud as jest.Mock).mockImplementationOnce(() => true); const wrapper = shallow( - <DocLink appState={{ canAdmin: false }} href="/#sonarcloud#/foo/bar"> + <DocLink appState={mockAppState({ canAdmin: false })} href="/#sonarcloud#/foo/bar"> link text </DocLink> ); @@ -60,7 +61,7 @@ it('should render sonarcloud link on sonarcloud', () => { it('should not render sonarcloud link on sonarcloud', () => { (isSonarCloud as jest.Mock).mockImplementationOnce(() => false); const wrapper = shallow( - <DocLink appState={{ canAdmin: false }} href="/#sonarcloud#/foo/bar"> + <DocLink appState={mockAppState({ canAdmin: false })} href="/#sonarcloud#/foo/bar"> link text </DocLink> ); @@ -69,7 +70,7 @@ it('should not render sonarcloud link on sonarcloud', () => { it('should render sonarqube link on sonarqube', () => { const wrapper = shallow( - <DocLink appState={{ canAdmin: false }} href="/#sonarqube#/foo/bar"> + <DocLink appState={mockAppState({ canAdmin: false })} href="/#sonarqube#/foo/bar"> link text </DocLink> ); @@ -80,7 +81,7 @@ it('should render sonarqube link on sonarqube', () => { it('should not render sonarqube link on sonarcloud', () => { (isSonarCloud as jest.Mock).mockImplementationOnce(() => true); const wrapper = shallow( - <DocLink appState={{ canAdmin: false }} href="/#sonarqube#/foo/bar"> + <DocLink appState={mockAppState({ canAdmin: false })} href="/#sonarqube#/foo/bar"> link text </DocLink> ); @@ -89,7 +90,7 @@ it('should not render sonarqube link on sonarcloud', () => { it('should render sonarqube admin link on sonarqube for admin', () => { const wrapper = shallow( - <DocLink appState={{ canAdmin: true }} href="/#sonarqube-admin#/foo/bar"> + <DocLink appState={mockAppState({ canAdmin: true })} href="/#sonarqube-admin#/foo/bar"> link text </DocLink> ); @@ -99,7 +100,7 @@ it('should render sonarqube admin link on sonarqube for admin', () => { it('should not render sonarqube admin link on sonarqube for non-admin', () => { const wrapper = shallow( - <DocLink appState={{ canAdmin: false }} href="/#sonarqube-admin#/foo/bar"> + <DocLink appState={mockAppState({ canAdmin: false })} href="/#sonarqube-admin#/foo/bar"> link text </DocLink> ); @@ -109,7 +110,7 @@ it('should not render sonarqube admin link on sonarqube for non-admin', () => { it('should not render sonarqube admin link on sonarcloud', () => { (isSonarCloud as jest.Mock).mockImplementationOnce(() => true); const wrapper = shallow( - <DocLink appState={{ canAdmin: true }} href="/#sonarqube-admin#/foo/bar"> + <DocLink appState={mockAppState({ canAdmin: true })} href="/#sonarqube-admin#/foo/bar"> link text </DocLink> ); @@ -119,7 +120,7 @@ it('should not render sonarqube admin link on sonarcloud', () => { it('should render documentation anchor', () => { expect( shallow( - <DocLink appState={{ canAdmin: false }} href="#quality-profiles"> + <DocLink appState={mockAppState({ canAdmin: false })} href="#quality-profiles"> link text </DocLink> ) diff --git a/server/sonar-web/src/main/js/components/tutorials/__tests__/__snapshots__/TutorialSelectionRenderer-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/__tests__/__snapshots__/TutorialSelectionRenderer-test.tsx.snap index e1250221011..e9acf7bd30b 100644 --- a/server/sonar-web/src/main/js/components/tutorials/__tests__/__snapshots__/TutorialSelectionRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/__tests__/__snapshots__/TutorialSelectionRenderer-test.tsx.snap @@ -487,7 +487,7 @@ exports[`should render correctly: gitlab tutorial 1`] = ` exports[`should render correctly: jenkins tutorial 1`] = ` <Fragment> - <Connect(JenkinsTutorial) + <Connect(withAppStateContext(JenkinsTutorial)) almBinding={ Object { "alm": "github", 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 bdfbe237752..3d8c44c888b 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 @@ -20,12 +20,12 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router'; +import withAppStateContext from '../../../../app/components/app-state/withAppStateContext'; import { Alert } from '../../../../components/ui/Alert'; import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; import { AlmKeys } from '../../../../types/alm-settings'; import { AppState } from '../../../../types/types'; -import { withAppState } from '../../../hoc/withAppState'; import SentenceWithHighlights from '../../components/SentenceWithHighlights'; export interface PublishStepsProps { @@ -83,4 +83,4 @@ export function PublishSteps(props: PublishStepsProps) { ); } -export default withAppState(PublishSteps); +export default withAppStateContext(PublishSteps); diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/ClangGCC-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/ClangGCC-test.tsx.snap index 53c9be6755f..2b40b0cdf5f 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/ClangGCC-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/ClangGCC-test.tsx.snap @@ -117,7 +117,7 @@ unzip build-wrapper.zip" translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run.ccpp" /> </li> - <Connect(withAppState(PublishSteps)) /> + <withAppStateContext(PublishSteps) /> </ol> </Fragment> `; @@ -239,7 +239,7 @@ unzip build-wrapper.zip" translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run.ccpp" /> </li> - <Connect(withAppState(PublishSteps)) /> + <withAppStateContext(PublishSteps) /> </ol> </Fragment> `; @@ -361,7 +361,7 @@ Expand-Archive -Path 'build-wrapper.zip' -DestinationPath '.'" translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run.ccpp" /> </li> - <Connect(withAppState(PublishSteps)) /> + <withAppStateContext(PublishSteps) /> </ol> </Fragment> `; diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/DotNet-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/DotNet-test.tsx.snap index 5ca310653e6..ab15daebc76 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/DotNet-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/DotNet-test.tsx.snap @@ -34,7 +34,7 @@ exports[`should render correctly 1`] = ` translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run" /> </li> - <Connect(withAppState(PublishSteps)) /> + <withAppStateContext(PublishSteps) /> </ol> </Fragment> `; diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap index d46bb3b80f4..47f592f2def 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap @@ -42,7 +42,7 @@ exports[`should render correctly 1`] = ` /> </li> </ul> - <Connect(withAppState(PublishSteps)) /> + <withAppStateContext(PublishSteps) /> </ol> </Fragment> `; diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap index 06c9d9ce809..3eaf0afd712 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap @@ -42,7 +42,7 @@ exports[`should render correctly 1`] = ` /> </li> </ul> - <Connect(withAppState(PublishSteps)) /> + <withAppStateContext(PublishSteps) /> </ol> </Fragment> `; diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/Other-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/Other-test.tsx.snap index 44c9b28a3c9..87f7bf39c72 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/Other-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/Other-test.tsx.snap @@ -34,7 +34,7 @@ exports[`should render correctly 1`] = ` translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run" /> </li> - <Connect(withAppState(PublishSteps)) /> + <withAppStateContext(PublishSteps) /> </ol> </Fragment> `; diff --git a/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/AnalysisCommand.tsx b/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/AnalysisCommand.tsx index 4605bebcc0c..31c332fa14f 100644 --- a/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/AnalysisCommand.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/AnalysisCommand.tsx @@ -19,8 +19,8 @@ */ import { Dictionary } from 'lodash'; import * as React from 'react'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import { AppState, Component } from '../../../types/types'; -import { withAppState } from '../../hoc/withAppState'; import { CompilationInfo } from '../components/CompilationInfo'; import CreateYmlFile from '../components/CreateYmlFile'; import { BuildTools } from '../types'; @@ -67,4 +67,4 @@ export function AnalysisCommand(props: AnalysisCommandProps) { ); } -export default withAppState(AnalysisCommand); +export default withAppStateContext(AnalysisCommand); diff --git a/server/sonar-web/src/main/js/components/tutorials/components/AllSet.tsx b/server/sonar-web/src/main/js/components/tutorials/components/AllSet.tsx index a503bbc0baa..20b11cbb19d 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/AllSet.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/AllSet.tsx @@ -18,11 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import { translate } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/system'; import { AlmKeys } from '../../../types/alm-settings'; import { AppState } from '../../../types/types'; -import { withAppState } from '../../hoc/withAppState'; import SentenceWithHighlights from './SentenceWithHighlights'; export interface AllSetProps { @@ -96,4 +96,4 @@ export function AllSet(props: AllSetProps) { ); } -export default withAppState(AllSet); +export default withAppStateContext(AllSet); diff --git a/server/sonar-web/src/main/js/components/tutorials/components/__tests__/__snapshots__/AllSetStep-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/components/__tests__/__snapshots__/AllSetStep-test.tsx.snap index 81965c60b99..25a6c4c6240 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/__tests__/__snapshots__/AllSetStep-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/components/__tests__/__snapshots__/AllSetStep-test.tsx.snap @@ -14,7 +14,7 @@ exports[`should render correctly: step content 1`] = ` <div className="boxed-group-inner" > - <Connect(withAppState(AllSet)) + <withAppStateContext(AllSet) alm="azure" willRefreshAutomatically={true} /> diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/AnalysisCommand.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/AnalysisCommand.tsx index 142b08f6b0d..4038f37237e 100644 --- a/server/sonar-web/src/main/js/components/tutorials/github-action/AnalysisCommand.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/AnalysisCommand.tsx @@ -18,8 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import { AppState, Component } from '../../../types/types'; -import { withAppState } from '../../hoc/withAppState'; import { BuildTools } from '../types'; import CFamily from './commands/CFamily'; import DotNet from './commands/DotNet'; @@ -70,4 +70,4 @@ export function AnalysisCommand(props: AnalysisCommandProps) { return null; } -export default withAppState(AnalysisCommand); +export default withAppStateContext(AnalysisCommand); diff --git a/server/sonar-web/src/main/js/components/tutorials/gitlabci/YmlFileStep.tsx b/server/sonar-web/src/main/js/components/tutorials/gitlabci/YmlFileStep.tsx index 85b65c8186a..bc6ec8c0039 100644 --- a/server/sonar-web/src/main/js/components/tutorials/gitlabci/YmlFileStep.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/gitlabci/YmlFileStep.tsx @@ -19,10 +19,10 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import { ClipboardIconButton } from '../../../components/controls/clipboard'; import { translate } from '../../../helpers/l10n'; import { AppState } from '../../../types/types'; -import { withAppState } from '../../hoc/withAppState'; import FinishButton from '../components/FinishButton'; import GithubCFamilyExampleRepositories from '../components/GithubCFamilyExampleRepositories'; import Step from '../components/Step'; @@ -112,4 +112,4 @@ export function YmlFileStep(props: YmlFileStepProps) { ); } -export default withAppState(YmlFileStep); +export default withAppStateContext(YmlFileStep); diff --git a/server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/__snapshots__/GitLabCITutorial-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/__snapshots__/GitLabCITutorial-test.tsx.snap index 6082843f8d0..e3e33091f2b 100644 --- a/server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/__snapshots__/GitLabCITutorial-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/__snapshots__/GitLabCITutorial-test.tsx.snap @@ -78,7 +78,7 @@ exports[`should render correctly 1`] = ` onOpen={[Function]} open={false} /> - <Connect(withAppState(YmlFileStep)) + <withAppStateContext(YmlFileStep) finished={false} onDone={[Function]} onOpen={[Function]} diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/JenkinsTutorial.tsx b/server/sonar-web/src/main/js/components/tutorials/jenkins/JenkinsTutorial.tsx index e8ed96e6bb9..15ddfb7fa86 100644 --- a/server/sonar-web/src/main/js/components/tutorials/jenkins/JenkinsTutorial.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/JenkinsTutorial.tsx @@ -19,15 +19,16 @@ */ import * as React from 'react'; import { connect } from 'react-redux'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import { translate } from '../../../helpers/l10n'; -import { getAppState, getCurrentUserSetting, Store } from '../../../store/rootReducer'; +import { getCurrentUserSetting, Store } from '../../../store/rootReducer'; import { setCurrentUserSetting } from '../../../store/users'; import { AlmKeys, AlmSettingsInstance, ProjectAlmBindingResponse } from '../../../types/alm-settings'; -import { Component, CurrentUserSetting } from '../../../types/types'; +import { AppState, Component, CurrentUserSetting } from '../../../types/types'; import AllSetStep from '../components/AllSetStep'; import JenkinsfileStep from './JenkinsfileStep'; import MultiBranchPipelineStep from './MultiBranchPipelineStep'; @@ -39,7 +40,7 @@ import WebhookStep from './WebhookStep'; export interface JenkinsTutorialProps { almBinding?: AlmSettingsInstance; baseUrl: string; - branchesEnabled: boolean; + appState: AppState; component: Component; projectBinding?: ProjectAlmBindingResponse; setCurrentUserSetting: (setting: CurrentUserSetting) => void; @@ -62,7 +63,7 @@ export function JenkinsTutorial(props: JenkinsTutorialProps) { const { almBinding, baseUrl, - branchesEnabled, + appState, component, projectBinding, skipPreReqs, @@ -101,7 +102,7 @@ export function JenkinsTutorial(props: JenkinsTutorialProps) { <> <PreRequisitesStep alm={alm} - branchesEnabled={branchesEnabled} + branchesEnabled={!!appState.branchesEnabled} finished={step > Steps.PreRequisites} onDone={() => setStep(Steps.MultiBranchPipeline)} onOpen={() => setStep(Steps.PreRequisites)} @@ -115,7 +116,7 @@ export function JenkinsTutorial(props: JenkinsTutorialProps) { skipNextTime={skipPreReqs} /> - {branchesEnabled ? ( + {appState.branchesEnabled ? ( <MultiBranchPipelineStep alm={alm} almBinding={almBinding} @@ -138,7 +139,7 @@ export function JenkinsTutorial(props: JenkinsTutorialProps) { <WebhookStep alm={alm} almBinding={almBinding} - branchesEnabled={branchesEnabled} + branchesEnabled={!!appState.branchesEnabled} finished={step > Steps.Webhook} onDone={() => setStep(Steps.Jenkinsfile)} onOpen={() => setStep(Steps.Webhook)} @@ -167,15 +168,12 @@ export function JenkinsTutorial(props: JenkinsTutorialProps) { ); } -const mapStateToProps = ( - state: Store -): Pick<JenkinsTutorialProps, 'branchesEnabled' | 'skipPreReqs'> => { +const mapStateToProps = (state: Store): Pick<JenkinsTutorialProps, 'skipPreReqs'> => { return { - branchesEnabled: Boolean(getAppState(state).branchesEnabled), skipPreReqs: getCurrentUserSetting(state, USER_SETTING_SKIP_BITBUCKET_PREREQS) === 'true' }; }; const mapDispatchToProps = { setCurrentUserSetting }; -export default connect(mapStateToProps, mapDispatchToProps)(JenkinsTutorial); +export default connect(mapStateToProps, mapDispatchToProps)(withAppStateContext(JenkinsTutorial)); diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-test.tsx b/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-test.tsx index 4fb31b5e9b7..beb1264b452 100644 --- a/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-test.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-test.tsx @@ -21,6 +21,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockProjectBitbucketBindingResponse } from '../../../../helpers/mocks/alm-settings'; import { mockComponent } from '../../../../helpers/mocks/component'; +import { mockAppState } from '../../../../helpers/testMocks'; import { AlmKeys } from '../../../../types/alm-settings'; import JenkinsfileStep from '../JenkinsfileStep'; import { JenkinsTutorial, JenkinsTutorialProps } from '../JenkinsTutorial'; @@ -31,7 +32,9 @@ import WebhookStep from '../WebhookStep'; it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot('default'); - expect(shallowRender({ branchesEnabled: false })).toMatchSnapshot('branches not enabled'); + expect(shallowRender({ appState: mockAppState({ branchesEnabled: false }) })).toMatchSnapshot( + 'branches not enabled' + ); expect(shallowRender({ projectBinding: undefined })).toMatchSnapshot('no project binding'); }); @@ -133,7 +136,7 @@ function shallowRender(props: Partial<JenkinsTutorialProps> = {}) { return shallow<JenkinsTutorialProps>( <JenkinsTutorial baseUrl="" - branchesEnabled={true} + appState={mockAppState({ branchesEnabled: true })} component={mockComponent()} projectBinding={mockProjectBitbucketBindingResponse()} setCurrentUserSetting={jest.fn()} diff --git a/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx index 9af265ff697..6826d56c336 100644 --- a/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx +++ b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx @@ -19,19 +19,19 @@ */ import { filter, flatMap, isEmpty, negate } from 'lodash'; import * as React from 'react'; +import withAppStateContext from '../../app/components/app-state/withAppStateContext'; import { translate } from '../../helpers/l10n'; import { EditionKey } from '../../types/editions'; import { SystemUpgrade } from '../../types/system'; import { AppState } from '../../types/types'; import { ResetButtonLink } from '../controls/buttons'; import Modal from '../controls/Modal'; -import { withAppState } from '../hoc/withAppState'; import { Alert, AlertVariant } from '../ui/Alert'; import SystemUpgradeItem from './SystemUpgradeItem'; import { UpdateUseCase } from './utils'; interface Props { - appState: Pick<AppState, 'edition' | 'version'>; + appState: AppState; onClose: () => void; systemUpgrades: SystemUpgrade[][]; latestLTS: string; @@ -120,4 +120,4 @@ export class SystemUpgradeForm extends React.PureComponent<Props, State> { } } -export default withAppState(SystemUpgradeForm); +export default withAppStateContext(SystemUpgradeForm); diff --git a/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeForm-test.tsx b/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeForm-test.tsx index 9de6742c42a..c3e37a9c693 100644 --- a/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeForm-test.tsx +++ b/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeForm-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockAppState } from '../../../helpers/testMocks'; import { EditionKey } from '../../../types/editions'; import { SystemUpgradeForm } from '../SystemUpgradeForm'; import { UpdateUseCase } from '../utils'; @@ -76,7 +77,7 @@ it.each([...Object.values(UpdateUseCase), undefined])( expect( shallow( <SystemUpgradeForm - appState={{ edition: EditionKey.community, version: '5.6.3' }} + appState={mockAppState({ edition: EditionKey.community, version: '5.6.3' })} onClose={jest.fn()} systemUpgrades={UPGRADES} latestLTS="9.1" diff --git a/server/sonar-web/src/main/js/store/appState.ts b/server/sonar-web/src/main/js/store/appState.ts index dfe4553cb3a..c3175010645 100644 --- a/server/sonar-web/src/main/js/store/appState.ts +++ b/server/sonar-web/src/main/js/store/appState.ts @@ -17,28 +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 { AppState, Extension } from '../types/types'; +import { AppState } from '../types/types'; import { ActionType } from './utils/actions'; export const enum Actions { SetAppState = 'SET_APP_STATE', - SetAdminPages = 'SET_ADMIN_PAGES', RequireAuthorization = 'REQUIRE_AUTHORIZATION' } export type Action = | ActionType<typeof setAppState, Actions.SetAppState> - | ActionType<typeof setAdminPages, Actions.SetAdminPages> | ActionType<typeof requireAuthorization, Actions.RequireAuthorization>; export function setAppState(appState: AppState) { return { type: Actions.SetAppState, appState }; } -export function setAdminPages(adminPages: Extension[]) { - return { type: Actions.SetAdminPages, adminPages }; -} - export function requireAuthorization() { return { type: Actions.RequireAuthorization }; } @@ -57,9 +51,6 @@ export default function(state: AppState = defaultValue, action: Action): AppStat if (action.type === Actions.SetAppState) { return { ...state, ...action.appState }; } - if (action.type === Actions.SetAdminPages) { - return { ...state, adminPages: action.adminPages }; - } if (action.type === Actions.RequireAuthorization) { return { ...state, authorizationError: true }; } diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index 574818ce47b..01544faf61e 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -84,7 +84,6 @@ export interface AnalysisEvent { } export interface AppState { - adminPages?: Extension[]; authenticationError?: boolean; authorizationError?: boolean; branchesEnabled?: boolean; |