From 2b13f016dee62cc813d0374e9f835dee5f4cda28 Mon Sep 17 00:00:00 2001 From: Guillaume Peoc'h Date: Wed, 2 Feb 2022 15:36:37 +0100 Subject: [PATCH] SONAR-15909 Extract AppState Redux --- server/sonar-web/src/main/js/api/nav.ts | 7 +- .../main/js/app/components/AdminContainer.tsx | 30 ++-- .../src/main/js/app/components/App.tsx | 3 +- .../js/app/components/ComponentContainer.tsx | 13 +- .../js/app/components/GlobalContainer.tsx | 5 +- .../main/js/app/components/GlobalFooter.tsx | 25 ++- .../main/js/app/components/PageTracker.tsx | 23 +-- .../js/app/components/SimpleContainer.tsx | 4 +- .../components/SimpleSessionsContainer.tsx | 4 +- .../main/js/app/components/StartupModal.tsx | 29 ++-- .../__tests__/AdminContainer-test.tsx | 17 +- .../__tests__/GlobalFooter-test.tsx | 25 ++- .../components/__tests__/PageTracker-test.tsx | 12 +- .../__tests__/StartupModal-test.tsx | 12 +- .../AdminContainer-test.tsx.snap | 15 +- .../GlobalContainer-test.tsx.snap | 12 +- .../__snapshots__/GlobalFooter-test.tsx.snap | 5 + .../components/app-state/AppStateContext.tsx} | 20 ++- .../AppStateContextProvider.tsx} | 28 ++-- .../__tests__/withAppStateContext-test.tsx | 50 ++++++ .../app-state/withAppStateContext.tsx} | 36 +++-- .../extensions/GlobalAdminPageExtension.tsx | 17 +- .../extensions/GlobalPageExtension.tsx | 20 ++- .../indexation/IndexationContextProvider.tsx | 10 +- .../IndexationContextProvider-test.tsx | 13 +- .../IndexationContextProvider-test.tsx.snap | 7 + .../component/ComponentNavLicenseNotif.tsx | 10 +- .../js/app/components/nav/component/Menu.tsx | 6 +- .../ComponentNavLicenseNotif-test.tsx | 5 +- .../nav/component/__tests__/Menu-test.tsx | 3 +- .../__snapshots__/ComponentNav-test.tsx.snap | 12 +- .../ComponentNavBgTaskNotif-test.tsx.snap | 2 +- .../__snapshots__/Header-test.tsx.snap | 2 +- .../branch-like/BranchLikeNavigation.tsx | 6 +- .../app/components/nav/global/GlobalNav.tsx | 12 +- .../components/nav/global/GlobalNavMenu.tsx | 7 +- .../nav/global/__tests__/GlobalNav-test.tsx | 14 +- .../global/__tests__/GlobalNavMenu-test.tsx | 11 +- .../__snapshots__/GlobalNav-test.tsx.snap | 18 +-- .../UpdateNotification.tsx | 6 +- server/sonar-web/src/main/js/app/index.ts | 16 +- .../src/main/js/app/utils/startReactApp.tsx | 153 +++++++++--------- .../apps/audit-logs/components/AuditApp.tsx | 44 +++-- .../components/__tests__/AuditApp-test.tsx | 5 +- .../components/StatPendingCount.tsx | 16 +- .../__tests__/StatPendingCount-test.tsx | 5 +- .../__snapshots__/Stats-test.tsx.snap | 4 +- .../ChangeAdminPasswordApp.tsx | 21 ++- .../__tests__/ChangeAdminPasswordApp-test.tsx | 37 +---- .../ChangeAdminPasswordApp-test.tsx.snap | 1 - .../components/RuleDetailsIssues.tsx | 6 +- .../__tests__/RuleDetailsIssues-test.tsx | 3 +- .../__snapshots__/RuleDetails-test.tsx.snap | 2 +- .../project/CreateProjectModeSelection.tsx | 6 +- .../apps/create/project/CreateProjectPage.tsx | 6 +- .../CreateProjectModeSelection-test.tsx | 11 +- .../__tests__/CreateProjectPage-test.tsx | 9 +- .../CreateProjectPage-test.tsx.snap | 4 +- .../main/js/apps/marketplace/AppContainer.tsx | 25 ++- .../__tests__/AppContainer-test.tsx | 9 +- .../main/js/apps/overview/components/App.tsx | 6 +- .../permission-templates/components/App.tsx | 15 +- .../components/__tests__/App-test.tsx | 6 +- .../global/components/AllHoldersList.tsx | 42 ++--- .../__tests__/AllHoldersList-test.tsx | 11 +- .../__tests__/__snapshots__/App-test.tsx.snap | 4 +- .../apps/projectBaseline/components/App.tsx | 23 +-- .../components/__tests__/App-test.tsx | 9 +- .../main/js/apps/projectBaseline/routes.ts | 2 +- .../components/LifetimeInformation.tsx | 16 +- .../__tests__/LifetimeInformation-test.tsx | 5 +- .../__tests__/__snapshots__/App-test.tsx.snap | 2 +- .../js/apps/projectDump/ProjectDumpApp.tsx | 6 +- .../apps/projects/components/AllProjects.tsx | 11 +- .../components/AllProjectsContainer.tsx | 6 +- .../components/ApplicationCreation.tsx | 6 +- .../components/__tests__/AllProjects-test.tsx | 5 +- .../__snapshots__/PageHeader-test.tsx.snap | 4 +- .../main/js/apps/projectsManagement/App.tsx | 9 +- .../js/apps/projectsManagement/Search.tsx | 11 +- .../projectsManagement/__tests__/App-test.tsx | 21 ++- .../__tests__/Search-test.tsx | 14 +- .../quality-gates/components/Conditions.tsx | 6 +- .../components/__tests__/Conditions-test.tsx | 4 +- .../DetailsContent-test.tsx.snap | 8 +- .../settings/components/AllCategoriesList.tsx | 17 +- .../__tests__/AllCategoriesList-test.tsx | 7 +- .../AdditionalCategories-test.tsx.snap | 4 +- .../SettingsAppRenderer-test.tsx.snap | 4 +- .../almIntegration/AlmIntegration.tsx | 6 +- .../almIntegration/CreationTooltip.tsx | 4 +- .../__tests__/AlmIntegration-test.tsx | 4 +- .../AlmTabRenderer-test.tsx.snap | 48 +++--- .../AlmSpecificForm.tsx | 17 +- .../PRDecorationBinding.tsx | 21 +-- .../PRDecorationBindingRenderer.tsx | 3 - .../__tests__/AlmSpecificForm-test.tsx | 10 +- .../__tests__/PRDecorationBinding-test.tsx | 1 - .../PRDecorationBindingRenderer-test.tsx | 1 - .../PRDecorationBinding-test.tsx.snap | 1 - .../PRDecorationBindingRenderer-test.tsx.snap | 6 +- .../js/apps/system/components/PageHeader.tsx | 24 +-- .../components/__tests__/PageHeader-test.tsx | 9 +- .../__tests__/__snapshots__/App-test.tsx.snap | 10 +- .../src/main/js/components/docs/DocLink.tsx | 50 +++--- .../docs/__tests__/DocLink-test.tsx | 21 +-- .../TutorialSelectionRenderer-test.tsx.snap | 2 +- .../azure-pipelines/commands/PublishSteps.tsx | 4 +- .../__snapshots__/ClangGCC-test.tsx.snap | 6 +- .../__snapshots__/DotNet-test.tsx.snap | 2 +- .../__snapshots__/JavaGradle-test.tsx.snap | 2 +- .../__snapshots__/JavaMaven-test.tsx.snap | 2 +- .../__snapshots__/Other-test.tsx.snap | 2 +- .../bitbucket-pipelines/AnalysisCommand.tsx | 4 +- .../tutorials/components/AllSet.tsx | 4 +- .../__snapshots__/AllSetStep-test.tsx.snap | 2 +- .../github-action/AnalysisCommand.tsx | 4 +- .../tutorials/gitlabci/YmlFileStep.tsx | 4 +- .../GitLabCITutorial-test.tsx.snap | 2 +- .../tutorials/jenkins/JenkinsTutorial.tsx | 22 ++- .../__tests__/JenkinsTutorial-test.tsx | 7 +- .../components/upgrade/SystemUpgradeForm.tsx | 6 +- .../__tests__/SystemUpgradeForm-test.tsx | 3 +- .../sonar-web/src/main/js/store/appState.ts | 11 +- server/sonar-web/src/main/js/types/types.ts | 1 - 125 files changed, 791 insertions(+), 758 deletions(-) rename server/sonar-web/src/main/js/{apps/projectBaseline/components/AppContainer.ts => app/components/app-state/AppStateContext.tsx} (70%) rename server/sonar-web/src/main/js/app/components/{GlobalFooterContainer.tsx => app-state/AppStateContextProvider.tsx} (56%) create mode 100644 server/sonar-web/src/main/js/app/components/app-state/__tests__/withAppStateContext-test.tsx rename server/sonar-web/src/main/js/{components/hoc/withAppState.tsx => app/components/app-state/withAppStateContext.tsx} (58%) 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; @@ -34,6 +34,9 @@ export function getMarketplaceNavigation(): Promise<{ serverId: string; ncloc: n return getJSON('/api/navigation/marketplace').catch(throwGlobalError); } -export function getSettingsNavigation(): Promise { +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; +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 { +export class AdminContainer extends React.PureComponent { mounted = false; state: State = { pendingPlugins: defaultPendingPlugins, - systemStatus: defaultSystemStatus + systemStatus: defaultSystemStatus, + adminPages: [] }; componentDidMount() { @@ -67,7 +67,7 @@ export class AdminContainer extends React.PureComponent { 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 { }; 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 { pendingPlugins, systemStatus }}> - {this.props.children} + {React.cloneElement(this.props.children, { + adminPages + })} ); } } -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 { parser.href = this.props.gravatarServerUrl; if (parser.hostname !== window.location.hostname) { return ; - } 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: AppState; children: React.ReactElement; location: Pick; registerBranchStatus: (branchLike: BranchLike, component: string, status: Status) => void; @@ -417,7 +417,8 @@ export class ComponentContainer extends React.PureComponent { isPending, projectBinding, projectBindingErrors, - tasksInProgress + tasksInProgress, + warnings } = this.state; const isInProgress = tasksInProgress && tasksInProgress.length > 0; @@ -439,7 +440,7 @@ export class ComponentContainer extends React.PureComponent { onWarningDismiss={this.handleWarningDismiss} projectBinding={projectBinding} projectBindingErrors={projectBindingErrors} - warnings={this.state.warnings} + warnings={warnings} /> )} {loading ? ( @@ -467,4 +468,6 @@ export class ComponentContainer extends React.PureComponent { 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 = } = props; + const { footer = } = props; return ( -
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 ( ); } + +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 { 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 { }; render() { - const { trackingIdGTM, webAnalytics } = this.props; + const { trackingIdGTM, appState } = this.props; return ( + onChangeClientState={ + trackingIdGTM || appState.webAnalyticsJsPath ? this.trackPage : undefined + }> {this.props.children} ); @@ -85,9 +89,8 @@ export class PageTracker extends React.Component { 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) { {children}
- +
); } 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) {
{children}
- +
); 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; router: Pick; -} - -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 { +export class StartupModal extends React.PureComponent { state: State = {}; componentDidMount() { @@ -71,11 +66,11 @@ export class StartupModal extends React.PureComponent { }; 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 { } 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 = {}) { +function shallowRender(props: Partial = {}) { return shallow( + {...props}> +
+ ); } 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( - + ); } 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 = {}) { - return shallow(); + return shallow( + + ); } 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).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 = {}) { return shallow( + > +
+
`; 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`] = ` - +
- + - - +
- + - +
`; 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 @@ -149,6 +149,11 @@ exports[`should render the only logged in information 1`] = ` > Community Edition +
  • + footer.version_x.1.0 +
  • 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 similarity index 70% rename from server/sonar-web/src/main/js/apps/projectBaseline/components/AppContainer.ts rename to 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(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 similarity index 56% rename from server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx rename to 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) { + return {children}; +} 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
    ; + } +} + +const UnderTest = withAppStateContext(Wrapped); + +it('should inject appState', () => { + const wrapper = shallow(); + expect(wrapper.dive().type()).toBe(Wrapped); + expect(wrapper.dive().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 similarity index 58% rename from server/sonar-web/src/main/js/components/hoc/withAppState.tsx rename to 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

    ( - WrappedComponent: React.ComponentType

    }> +export default function withAppStateContext

    ( + WrappedComponent: React.ComponentType

    ) { - class Wrapper extends React.Component

    { - static displayName = getWrappedDisplayName(WrappedComponent, 'withAppState'); + return class WithAppStateContext extends React.PureComponent< + Omit + > { + static displayName = getWrappedDisplayName(WrappedComponent, 'withAppStateContext'); render() { - return ; + return ( + + {appState => } + + ); } - } - - 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 ? : ; } - -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 ? : ; } -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; +export interface IndexationContextProviderProps { + appState: AppState; } export class IndexationContextProvider extends React.PureComponent< - React.PropsWithChildren, + React.PropsWithChildren, 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( - + ); 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`] = 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: AppState; currentTask?: Task; } @@ -67,7 +67,7 @@ export class ComponentNavLicenseNotif extends React.PureComponent }; 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 return ( {currentTask.errorMessage} - {this.props.appState.canAdmin ? ( + {appState.canAdmin ? ( {translate('license.component_navigation.button', currentTask.errorType)}. @@ -100,4 +100,4 @@ export class ComponentNavLicenseNotif extends React.PureComponent } } -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: AppState; branchLike: BranchLike | undefined; branchLikes: BranchLike[] | undefined; component: Component; @@ -620,4 +620,4 @@ export class Menu extends React.PureComponent { } } -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 = {}) { return shallow( 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) { return shallow

    (
    - - - - - - - ; + 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; currentUser: CurrentUser; location: { pathname: string }; } export function GlobalNav(props: GlobalNavProps) { - const { appState, currentUser, location } = props; + const { currentUser, location } = props; return ( - +
      @@ -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: AppState; currentUser: CurrentUser; location: { pathname: string }; } -export default class GlobalNavMenu extends React.PureComponent { +export class GlobalNavMenu extends React.PureComponent { renderProjects() { const active = this.props.location.pathname.startsWith('/projects') && @@ -173,3 +174,5 @@ export default class GlobalNavMenu extends React.PureComponent { ); } } + +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 = {}) { - return shallow( - - ); + return shallow(); } 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" > - - = { interface Props { dismissable: boolean; - appState: Pick; + appState: AppState; currentUser: CurrentUser; } @@ -249,4 +249,4 @@ export class UpdateNotification extends React.PureComponent { } } -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 = new Promise(resolve => { + const appStatePromise: Promise = 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( - - - {renderRedirects()} + + + + {renderRedirects()} - import('../components/FormattingHelp'))} - /> - - import('../components/SimpleContainer'))}> - {maintenanceRoutes} - {setupRoutes} - - - - import('../components/SimpleSessionsContainer') - )}> - + path="formatting/help" + component={lazyLoadComponent(() => import('../components/FormattingHelp'))} + /> + + import('../components/SimpleContainer'))}> + {maintenanceRoutes} + {setupRoutes} - - import('../components/Landing'))} /> + + + import('../components/SimpleSessionsContainer') + )}> + + + + + import('../components/Landing'))} + /> + + + + + + + import('../components/extensions/GlobalPageExtension') + )} + /> + + + + + import('../components/extensions/PortfoliosPage') + )} + /> + + - - - - + {renderComponentRoutes()} + + {renderAdminRoutes()} + + import('../components/ResetPassword'))} + /> - import('../components/extensions/GlobalPageExtension') + import('../../apps/change-admin-password/ChangeAdminPasswordApp') )} /> import('../components/PluginRiskConsent'))} /> - - - import('../components/extensions/PortfoliosPage') - )} + path="not_found" + component={lazyLoadComponent(() => import('../components/NotFound'))} + /> + import('../components/NotFound'))} /> - - - - {renderComponentRoutes()} - - {renderAdminRoutes()} - import('../components/ResetPassword'))} - /> - - import('../../apps/change-admin-password/ChangeAdminPasswordApp') - )} - /> - import('../components/PluginRiskConsent'))} - /> - import('../components/NotFound'))} - /> - import('../components/NotFound'))} - /> - - - + + + , 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 { - 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 { }; render() { - const { hasGovernanceExtension, auditHousekeepingPolicy } = this.props; + const { hasGovernanceExtension, ...auditAppRendererProps } = this.state; + const { auditHousekeepingPolicy } = this.props; + + if (!hasGovernanceExtension) { + return null; + } - return hasGovernanceExtension ? ( + return ( - ) : 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 = {}) { ); 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 {pendingCount} {translate('background_tasks.pending')} - {isSystemAdmin && pendingCount > 0 && ( + {appState.canAdmin && pendingCount > 0 && ( ({ - 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 = {}) { return shallow( - @@ -25,7 +25,7 @@ exports[`should render correctly for a component 1`] = `
      - 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 { passwordValue: '', confirmPasswordValue: '', submitting: false, - success: !props.instanceUsesDefaultAdminCredentials + success: !props.appState.instanceUsesDefaultAdminCredentials }; } @@ -93,7 +92,10 @@ export class ChangeAdminPasswordApp extends React.PureComponent { }; render() { - const { canAdmin, location } = this.props; + const { + appState: { canAdmin }, + location + } = this.props; const { canSubmit, confirmPasswordValue, passwordValue, submitting, success } = this.state; return ( { } } -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 = {}) { return shallow( 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`] = ` ; + appState: AppState; ruleDetails: Pick; } @@ -173,4 +173,4 @@ export class RuleDetailsIssues extends React.PureComponent { } } -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 = {}) { return shallow( 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`] = ` } } /> - ; + 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 { - appState: Pick; + appState: AppState; currentUser: LoggedInUser; } @@ -272,4 +272,4 @@ export class CreateProjectPage extends React.PureComponent { } } -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( { function shallowRender(props: Partial = {}) { return shallow( - - { 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 ( {({ fetchPendingPlugins, pendingPlugins }) => ( - + )} ); } -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: AppState; branchLike?: BranchLike; branchLikes: BranchLike[]; component: Component; @@ -93,4 +93,4 @@ export class App extends React.PureComponent { } } -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 {