]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15909 Extract AppState Redux
authorGuillaume Peoc'h <guillaume.peoch@sonarsource.com>
Wed, 2 Feb 2022 14:36:37 +0000 (15:36 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 9 Feb 2022 20:02:55 +0000 (20:02 +0000)
128 files changed:
server/sonar-web/src/main/js/api/nav.ts
server/sonar-web/src/main/js/app/components/AdminContainer.tsx
server/sonar-web/src/main/js/app/components/App.tsx
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/app/components/GlobalFooter.tsx
server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx [deleted file]
server/sonar-web/src/main/js/app/components/PageTracker.tsx
server/sonar-web/src/main/js/app/components/SimpleContainer.tsx
server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx
server/sonar-web/src/main/js/app/components/StartupModal.tsx
server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx
server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx
server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx
server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/AdminContainer-test.tsx.snap
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap
server/sonar-web/src/main/js/app/components/app-state/AppStateContext.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/app-state/AppStateContextProvider.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/app-state/__tests__/withAppStateContext-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/app-state/withAppStateContext.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx
server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx
server/sonar-web/src/main/js/app/components/indexation/IndexationContextProvider.tsx
server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationContextProvider-test.tsx
server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationContextProvider-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx
server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Header-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap
server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx
server/sonar-web/src/main/js/app/index.ts
server/sonar-web/src/main/js/app/utils/startReactApp.tsx
server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx
server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-test.tsx
server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingCount.tsx
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/StatPendingCount-test.tsx
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Stats-test.tsx.snap
server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordApp.tsx
server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx
server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordApp-test.tsx.snap
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsIssues-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx
server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx
server/sonar-web/src/main/js/apps/overview/components/App.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/App.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx
server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/AllHoldersList-test.tsx
server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/AppContainer.ts [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectBaseline/routes.ts
server/sonar-web/src/main/js/apps/projectBranches/components/LifetimeInformation.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LifetimeInformation-test.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx
server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Conditions-test.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsContent-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/AllCategoriesList-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AdditionalCategories-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsAppRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegration.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/CreationTooltip.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-test.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTabRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBinding.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/AlmSpecificForm-test.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBindingRenderer-test.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBinding-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx
server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/components/docs/DocLink.tsx
server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx
server/sonar-web/src/main/js/components/hoc/withAppState.tsx [deleted file]
server/sonar-web/src/main/js/components/tutorials/__tests__/__snapshots__/TutorialSelectionRenderer-test.tsx.snap
server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/PublishSteps.tsx
server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/ClangGCC-test.tsx.snap
server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/DotNet-test.tsx.snap
server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap
server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap
server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/__tests__/__snapshots__/Other-test.tsx.snap
server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/AnalysisCommand.tsx
server/sonar-web/src/main/js/components/tutorials/components/AllSet.tsx
server/sonar-web/src/main/js/components/tutorials/components/__tests__/__snapshots__/AllSetStep-test.tsx.snap
server/sonar-web/src/main/js/components/tutorials/github-action/AnalysisCommand.tsx
server/sonar-web/src/main/js/components/tutorials/gitlabci/YmlFileStep.tsx
server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/__snapshots__/GitLabCITutorial-test.tsx.snap
server/sonar-web/src/main/js/components/tutorials/jenkins/JenkinsTutorial.tsx
server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-test.tsx
server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx
server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeForm-test.tsx
server/sonar-web/src/main/js/store/appState.ts
server/sonar-web/src/main/js/types/types.ts

index 61fa7d6e3cb0e97cd205faedacf0c812721aa8f5..974fb5fc1b30e32b9a46600ebc3bf00b066ed390 100644 (file)
@@ -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);
 }
index c66a0678b3544bf4e5d0dc51e5af2531170fe484..8a690681ac789fe6f229caea81bdef8f14d60f9d 100644 (file)
  */
 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);
index e1ba399c6059c2566fe595812a2b12a41d405ea7..b7e22a74638add71ce0f96742502f8a1ba9f74c3 100644 (file)
@@ -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() {
index e979ec1979d562a1f634287cc318f96d8441156b..7a362ba5da7de84694bde68931fa02b1cf37b59b 100644 (file)
@@ -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))
+);
index c9423bff3318731742d13a7639f8bcd7efa2cec6..d00fbc0cf73702f839b357c05975131a39605f2c 100644 (file)
@@ -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">
index e52b89d107a0c82ec66e099cb1c90b5279a2ac45..da00469d33ce0dcd78d1923103b3033db22573b4 100644 (file)
@@ -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/GlobalFooterContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx
deleted file mode 100644 (file)
index e0bf748..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { 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;
-}
-
-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 default connect(mapStateToProps)(GlobalFooter);
index 8a3e610f4d28befd5a0d4b9a7d00cb2e59e4987e..02c47e467fb4486ab8b3c3dde902b4b72aa6617b 100644 (file)
@@ -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)));
index bcad1eb490f90177853ea0df3aa279b2678dcd2d..0a9e52d54dd5fd696c6dea89c5916447f3051367 100644 (file)
@@ -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>
   );
 }
index 126dd5436767975daf8eb1d731011501913f4801..c9564386eaaa360d7e12e5fe4bab53e5254365b6 100644 (file)
@@ -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>
     </>
   );
index 4b6606246a2705faf473b2aa3d443089bfe7c246..ff8216a49d1c1a79bb9fe0c7da0f41b17f01dbff 100644 (file)
@@ -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)));
index e39878b02460a442451dd423f063fef165369c14..c0ca09c8ee131aa78139bd8c1edc00811213fc3c 100644 (file)
  */
 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>
   );
 }
index 6f8972a7ec31b84fab91fcad4f1bea9c2e05cc9a..7758e327c695a4fdb721ab22f08d19e55658eee5 100644 (file)
@@ -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}
+    />
   );
 }
index d8e1c91662979b1b9d4a153a6eaaea7a15856edd..5ea91b924dbe8bebaa3813e4206ab2a8708bc862 100644 (file)
@@ -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} />
+  );
 }
index 1d1035ebfc4ccdc16bb54692ee61ab820da4228a..4a07e47012b54b6a3bea378577f9c5b58b164777 100644 (file)
@@ -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() }}
index db431ca16f0a3074aaaa9a34f0065cfdf54ae1b9..a12b91340b1c02504b472a04636cf9e91ce92f20 100644 (file)
@@ -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>
 `;
index 1e1d574f0a03147be5d90d54772daf1d6d0d7381..58f58f0bfd2a0518f100f370653b6d9bd8dd4df9 100644 (file)
@@ -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>
 `;
index 61e974fab751cf43e01154f46dbee86f5a5d5125..d5450c04210c74687a589cee61273f51106413af 100644 (file)
@@ -149,6 +149,11 @@ exports[`should render the only logged in information 1`] = `
     >
       Community Edition
     </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      footer.version_x.1.0
+    </li>
     <li
       className="page-footer-menu-item"
     >
diff --git a/server/sonar-web/src/main/js/app/components/app-state/AppStateContext.tsx b/server/sonar-web/src/main/js/app/components/app-state/AppStateContext.tsx
new file mode 100644 (file)
index 0000000..1e2dcf4
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { AppState } from '../../../types/types';
+
+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/app-state/AppStateContextProvider.tsx b/server/sonar-web/src/main/js/app/components/app-state/AppStateContextProvider.tsx
new file mode 100644 (file)
index 0000000..0b7336d
--- /dev/null
@@ -0,0 +1,35 @@
+/* eslint-disable no-console */
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { AppState } from '../../../types/types';
+import { AppStateContext } from './AppStateContext';
+
+export interface AppStateContextProviderProps {
+  appState: AppState;
+}
+
+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 (file)
index 0000000..d6323fb
--- /dev/null
@@ -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/app/components/app-state/withAppStateContext.tsx b/server/sonar-web/src/main/js/app/components/app-state/withAppStateContext.tsx
new file mode 100644 (file)
index 0000000..6410751
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { getWrappedDisplayName } from '../../../components/hoc/utils';
+import { AppState } from '../../../types/types';
+import { AppStateContext } from './AppStateContext';
+
+export interface WithAppStateContextProps {
+  appState: AppState;
+}
+
+export default function withAppStateContext<P>(
+  WrappedComponent: React.ComponentType<P & WithAppStateContextProps>
+) {
+  return class WithAppStateContext extends React.PureComponent<
+    Omit<P, keyof WithAppStateContextProps>
+  > {
+    static displayName = getWrappedDisplayName(WrappedComponent, 'withAppStateContext');
+
+    render() {
+      return (
+        <AppStateContext.Consumer>
+          {appState => <WrappedComponent appState={appState} {...(this.props as P)} />}
+        </AppStateContext.Consumer>
+      );
+    }
+  };
+}
index 986a581768e2c017003e941e1b955bcd7c15dac7..28974085d08dcc67e4df5a16ed8614167ddad507 100644 (file)
@@ -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);
index 1f1fd89fd30a926059ab23fc62b3e41d9e8e9343..d3b0559d35ce1fba86953aa667f44f6688b58b39 100644 (file)
  * 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);
index 84c997f6b543aadcf8cf133a60d6e220c9e614ec..091fc03024d2ed0f6f0338edaaed90824b0b6f05 100644 (file)
  */
 /* 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);
index eed129e5a53504a477ede64adb18cf502cc8bbc9..c6c383cf1581fd4f9c7bb853d900ab40a66c0fff 100644 (file)
  */
 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>
   );
index a7c918f76fe688be4ff89c6fd5170269c4b1344d..a966efb2160c68755805ac164feef6521c6eadff 100644 (file)
@@ -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",
     }
   }
 >
index 5466afe3f323cffb047b71ff040fff787d53a68f..37770276d4fefdaba9e3a1da6e715620794c04b9 100644 (file)
 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);
index 7d08885238ae7c51c269b2ec4f0a90153db87ad5..7f7d3c3284394b6eb7ba32a0309b4d6a00babc5e 100644 (file)
@@ -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);
index aed4d489ece59f79003988c6a1e75366d20c7318..354a592c1794cad16460b8c5e796286ed23b27de 100644 (file)
@@ -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}
     />
index 17a7293eb309474a59d2b67a87d4c3fde2334392..2d65f7eefa0053c888ecfe312b8aa826882eb0af 100644 (file)
@@ -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}
index 4c9f5933de50ceb4f9ce985faee586c1b3e09d57..0a372d2d7b805fc189132b694ea5772768a55db8 100644 (file)
@@ -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 {
index cfed20bb43135d4d1689f9a58df6045ebd63a621..03f114eee5b423d1dc1d1af6e5c239593291c5cc 100644 (file)
@@ -22,7 +22,7 @@ exports[`renders correctly: default 1`] = `
 `;
 
 exports[`renders correctly: license issue 1`] = `
-<Connect(withAppState(ComponentNavLicenseNotif))
+<withAppStateContext(ComponentNavLicenseNotif)
   currentTask={
     Object {
       "analysisId": "x123",
index dbb95fd354c04d3e48b6ed0bf6bb1080688250ad..c3f9e27795d526ab7025b505fa61cb1f908b9602 100644 (file)
@@ -49,7 +49,7 @@ exports[`should render correctly 1`] = `
       favorite={false}
       qualifier="TRK"
     />
-    <Connect(withAppState(Component))
+    <withAppStateContext(Component)
       branchLikes={
         Array [
           Object {
index 0a85e8a9e140dcdb9d472721e2fa62d3e4cb5953..2e619a4e0493a452ecb370db7a49447f2a4a7ac6 100644 (file)
 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));
index b9f73a60aefc3b2f4e113d815338d8e4c47f0c53..488135ebd8b1566c7077ff36615afbaa52aa3bda 100644 (file)
@@ -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)
   };
 };
 
index 8da1d583c0ff0f15f2adf1484320b39322f5e28d..daf8af77711bfcbae5e383e9bae099092a296c4c 100644 (file)
@@ -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);
index 0a8bd89a46a58658023a03532a705819f3444911..49b64ed497cb75ab60e6789a87e01926800f1578 100644 (file)
@@ -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} />);
 }
index f18528c24cdaa5351e086c0c8402ae6b84e34678..1a56df67d0ff2dda58c55eb52be99ad377b035a7 100644 (file)
  */
 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
   };
index d3f0165206c588fbf35553c4d9271c6c547c8e7c..cc530594fb38f9be21213534fe63b426b1bf709e 100644 (file)
@@ -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,
index 2195b772fa32705827146c5aff7d97d311487076..8958a1c02d6b093b16306f8e7e99f191ae7d89e4 100644 (file)
@@ -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));
index 02e3c39a0630dfa0f0fa1cb4a36faf6a42570923..d94e52dbd7ed50fb433b2314b1184f5cd86aa10d 100644 (file)
@@ -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);
index aa7fda54aa1ad8da9c594d23c0df9883db6293a2..89456a949bbd13c637f94c57da153c2832305d81 100644 (file)
@@ -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
index 4854062f011b6d36a436e6ef351d33ba524cd565..84c09c6ec852ac7c68d46a0a3a28eac15f9702b4 100644 (file)
@@ -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
   };
 };
 
index b4d6a18eca09ce43fa5c84047796643c163de141..1b9740af7603f36d3c0193c88a9219b9b790acce 100644 (file)
@@ -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}
     />
   );
index 6c2ef4975492d2f76f5f32bce9deac21f37d1b69..d1cc1e097c736914382fc161dd66b5105f46b911 100644 (file)
  * 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);
index f0714f5a1be60ec4569b50738ab60c0560d9dc87..4a4086955a67eaf9160b4ef74a23ce43f18c82cd 100644 (file)
@@ -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}
index c352f81491ec8dcf619a7c0dccd35e07f6078504..1253b96bb1f180658f31bedeeff67610f399ef34 100644 (file)
@@ -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}
   />
index 80c821e4c794cc02cb5de419b1a9f1e1d4d92cbc..5bb5d8b601b175c0dc53dd6e53ae6fb71dcfebf1 100644 (file)
  * 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));
index cf173422826e4f4d5632dba6a4f8347e73bcdee0..fe8e3e54412bea4479b5df5de75b2558af35833e 100644 (file)
 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}
     />
index 5371a8f4386386c7b975be1581de6b107f18af64..e7e4585e6ae0f838f9033479c59fd2940fd12bd8 100644 (file)
@@ -2,7 +2,6 @@
 
 exports[`should render correctly: admin is not using the default password 1`] = `
 <ChangeAdminPasswordAppRenderer
-  canAdmin={true}
   confirmPasswordValue=""
   location={
     Object {
index ef4601199f1ac38f40ce291cbeba579e8df19e91..fa64d3cee56ebe7d4253999a2899824ea9cd41f3 100644 (file)
@@ -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);
index 207b08648f77169bf1cf51fc6df51a93d3fd62fb..7aeb0542100d57e821dddd49e233543040cfba43 100644 (file)
@@ -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}
     />
index e7230ad913a51710873db39bae9dd406ccca6d0b..5de95675322da663fe7050896b44027970a77f07 100644 (file)
@@ -156,7 +156,7 @@ exports[`should render correctly: loaded 1`] = `
         }
       }
     />
-    <Connect(withAppState(RuleDetailsIssues))
+    <withAppStateContext(RuleDetailsIssues)
       ruleDetails={
         Object {
           "createdAt": "2014-12-16T17:26:54+0100",
index 7e4819ac4847f2d6c57224f2a7bd3f3b104ecc5d..3cd2701916e441d9bb3a547fdae0973b0fad1277 100644 (file)
@@ -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);
index a97188e9969ecee9a3c41f2d6ae62b0c6f2a5176..6f250980373e7b5c90f2b98f7bb397ec335966dd 100644 (file)
@@ -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));
index f9cee16a180326420742457959d30a2a3ec9478f..4be1b325d6ab087a9079b69578efedd0e94e00f9 100644 (file)
@@ -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()}
index 59c21d7117071c2358e3227aabdfd28208ae9a33..541f478f55cc2d19f7cb8bc1c0a6088bb5ffafc0 100644 (file)
 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()}
index 4b824a163f9b5f85377c230d09781953211cc793..70474361ec7794b4bfa0204c91c08da3baa532e3 100644 (file)
@@ -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,
index 7ba8b943083d793805ee9117f93eb4890cb7e840..379c082bfe8be51d76c5e0ad852533503344d7fa 100644 (file)
 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));
index fbafb1047d827a6f4a67d06daf152c40ef2b9d24..d306400526fe974a4f2ca389c6b5f7e9c32ee2aa 100644 (file)
@@ -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
     });
 
index bfa2e17b3593b2821a1f9acad2465da241df0eba..665b63e6ddc7f2169ac84188ba80c58592f0da60 100644 (file)
@@ -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));
index 8f1f508aca0abc9b81801ae1dc39a7417f65dc64..f5fea8b899a5fc5dcf0a2f451df29b6e31ad049e 100644 (file)
 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);
index fa5469f91b554ff11f98cca5208bea89c1bba213..7dbbdfa0f9fc4ff8bd8b7fe30b409ca6d2165b2b 100644 (file)
@@ -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} />
+  );
 }
index def9d1e4ae7996aecb2b747a1d12651ebb1953c1..0f2d5f9874819a18887000a5cd9cb01505894325 100644 (file)
@@ -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);
index 354bbf20c362ec03398ba1837a3bcbd3a31bc2eb..7ca580ec0dbbbfb2d3aecee7589bb62dac500c53 100644 (file)
@@ -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()}
index 9ccdf81dfce971c3f84c5a5e9041800eac77ee7b..bf85f340bc336d93b3731279aa3b27c7a449ed80 100644 (file)
@@ -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]}
index eb380cbc7b11cbc6e141a32d6c827cd1aa953fb3..d515f2294e7fdd1289519f38f7751208978cb847 100644 (file)
@@ -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/AppContainer.ts b/server/sonar-web/src/main/js/apps/projectBaseline/components/AppContainer.ts
deleted file mode 100644 (file)
index 1712073..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { 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
-});
-
-export default connect(mapStateToProps)(App);
index f05c9404271a9f3f8708663eb18bd49350d26c47..e99eb125f1df9ba2b2b9af06347017318dad0dc7 100644 (file)
@@ -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}
     />
index 16e3be4e88a5601d701026fde275d3f15937fa7d..9d8f2bd42e4df7bcb192a1c664a8a813cd66cc8e 100644 (file)
@@ -21,7 +21,7 @@ import { lazyLoadComponent } from '../../components/lazyLoadComponent';
 
 const routes = [
   {
-    indexRoute: { component: lazyLoadComponent(() => import('./components/AppContainer')) }
+    indexRoute: { component: lazyLoadComponent(() => import('./components/App')) }
   }
 ];
 
index 05c1e3b2f71be4d2aa9948f10cdbf4437164f737..3e855acdfa6657623b82ada221443936fa53d23d 100644 (file)
  * 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);
index 65326bee132c85006133a877c66fc46ecb65515e..f34d9e24983df1d12b7eacc4a2ed85c511075a64 100644 (file)
@@ -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} />
+  );
 }
index 0bd2000c616ba913aeca45b48659dfa624f371f0..e673ebd9ff5a72ef973a6536962277061452584f 100644 (file)
@@ -11,7 +11,7 @@ exports[`should render correctly 1`] = `
     <h1>
       project_branch_pull_request.page
     </h1>
-    <Connect(LifetimeInformation) />
+    <withAppStateContext(LifetimeInformation) />
   </header>
   <BranchLikeTabs
     branchLikes={
index 0ad4fcc38dedb257ffa601dfde31cfb07efdc0fc..cd067423adfac2bb012ac15a81e900a69013cf2b 100644 (file)
@@ -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);
index be94169551eb32f311d8b09266e1c7d7f9cbc90d..38fa2a56eaf4b3ab8e28da72c141d72d28507ca2 100644 (file)
@@ -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));
index 011651322386e667ab140fc9b44d4907961a3b04..c1d5e5491caa69e7af321f4ef972d765667b9b8c 100644 (file)
  */
 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')));
index 268fbd19754e37f766b4721388a8f8f92a6ea550..4fe00dc27852d58bbc4cd5cd4ab3a2c23a153680 100644 (file)
@@ -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)));
index 819e956a14a20885d83ccae46041f305951830c1..63b97a12434c65c79b27e89327e5ef6f2eb5d164 100644 (file)
@@ -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}
     />
index 6f89a3843751a318c0a6149c20fcd76d81762838..8dff4f4dd701dac29584850ce95903701ec0c8db 100644 (file)
@@ -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)
index 7b090bbc3ee20dcc48da1265413fd7280eecdb8f..be9c7f036206705eadd1713fc18ff857cb059be1 100644 (file)
@@ -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
 });
 
index 8f47af0fdf420200877901375996f41a476d2ebc..4d19a37e27a54160f1dcf0d46c699631389ac615 100644 (file)
@@ -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);
index cc02fe3436f79cd4695578e31c374dd75bc3b5a0..653e059a15537e17faa8ead5ef347940c5908a18 100644 (file)
@@ -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}
     />
index 11dc6ea01f02f629cfbe7112df05216207d36bac..b4e3f13b637ebf58d5d791c8f853ecbbafdaaf93 100644 (file)
@@ -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}
     />
index d7ae7b87b7d32baba7457cc149b9e2ee047a90a2..81dbb739289393cb45f46d09d2ed020ab9f0d941 100644 (file)
  */
 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));
index c63143a38dac95c7760d04571239de1d293a1b24..2963e8c21f23e33daa8bc0e723b530a7bc6fbcd6 100644 (file)
@@ -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={{
index 456f401e581d1701956e0a808bcc400cb1f06807..7354a115ce829930ff1cc41b823acdf296719e82 100644 (file)
@@ -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 [
index c05f1fb30fcc7e3100cb036d513ad0710e9f831f..69722c98a05db6c193f7084600ce50dbdf5d61ff 100644 (file)
 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);
index 35e919e9f31f4a974b1c8f27b313af4909d07530..3d7a1401872df76089469a50722800177a4cc47c 100644 (file)
@@ -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=""
index 7d3b5667d2bdaf6f0a0e35307f2cf32474221310..8787399d313611913b3015b788b1cd620d4c1fbc 100644 (file)
@@ -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 [],
index 1b1ea74df0c51d9b706bc2ab6bc3367675e47e42..a40cc74b2c11b418432f3f562b2448f632abe751 100644 (file)
@@ -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",
index f26b1f7ffc2c76dcee6ba7125c6a2ecfc85c52dc..7a54c0bc12b1a4f0eab6db510f5121c61348825f 100644 (file)
@@ -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));
index 147f90d8c46472fbc3ab07d8fd214ae5a4ca2592..9f53a41396f788f0cf86861ab14761f0810b145a 100644 (file)
@@ -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);
index c6f66f233bb582f74a0a55a71122d4b371664fcb..aa701feea924d6bc6f8a7c19a58a20844c465d2b 100644 (file)
@@ -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()}
index ba36a1e7f3f8aeeb1bb6db9845306986aadb94df..b987913a570db98e74a508dbeaae39381cd395f1 100644 (file)
@@ -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"
index 6020027303c950f28dcc222e716078b2f80c836c..cd60e263b9d95cc0db7fa68539c52ed2e0575bd2 100644 (file)
@@ -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);
index 6c1369995f427e4342222aaa1e5ab75337fff73a..885689f50c999f8e7efa8322c8d8ac615ac2d7af 100644 (file)
@@ -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);
index 2b113aef557d65278965feba84d6d0bfe2097672..29162ee65e5b101324d7c91b32f6456c6bc95fd6 100644 (file)
@@ -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}
           />
         )}
 
index 4a7151f0ada54d57a896f2c02eeeef1323cbd9ef..cdc9538d83af264673c5533285c4ab25e4ea6ac0 100644 (file)
 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}
     />
   );
index eeb1756458f59c6729fc2e90851b46a0e867d576..793ee25b46bc8202d91f3990eeb12bcdbfab4bcb 100644 (file)
@@ -420,7 +420,6 @@ function shallowRender(props: Partial<PRDecorationBinding['props']> = {}) {
     <PRDecorationBinding
       currentUser={mockCurrentUser()}
       component={mockComponent({ key: PROJECT_KEY })}
-      monorepoEnabled={false}
       {...props}
     />
   );
index 96c6dc4d450a29918ce9c99f0897201081927496..a4bf1454e6da7e10457cf27b1c0f2b2c5bb3cc55 100644 (file)
@@ -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}
index eb3d7ad054b2d75bd15db1f15d7482015fb070ad..aa67734fdaf89c4f70f41e1b0f3f1b52bd008767 100644 (file)
@@ -15,7 +15,6 @@ exports[`should render correctly 1`] = `
   isSysAdmin={false}
   isValid={false}
   loading={true}
-  monorepoEnabled={false}
   onCheckConfiguration={[Function]}
   onFieldChange={[Function]}
   onReset={[Function]}
index 1ebb0bf76f4c38ced300cba06f9ac69e6cb46db7..f1bf70fdb61a36cbba22e53ded1d2316d709ce65 100644 (file)
@@ -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
index 53cf6c5c6295618b8826b359b8d6fc9432e72712..486c80de4c3c43a44ac90dc2a6a29fd51a690a58 100644 (file)
  * 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);
index b79bdf364e133cd7305fd0a5aca171a9eb163b2f..e8725a325af5f8c87bbda657ca4676f55eee8b2e 100644 (file)
@@ -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}
     />
index 9e6b4ffab230da9e60eaad0fc78078a4e2984ce3..274ea824bba5320d21139aef44d043e3dff895d2 100644 (file)
@@ -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"
index 344f0b1a1ce42affb081e525a54edea3efdafff0..1263177bf8f6a4b8e667bb9b6e69645da589c13c 100644 (file)
  */
 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>
+  );
 }
index 246452af3ca35362c5c2a55a1fc705b18d7925df..86e613815e07ecf4e5a052ba724ce6c0a4fe515b 100644 (file)
@@ -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/hoc/withAppState.tsx b/server/sonar-web/src/main/js/components/hoc/withAppState.tsx
deleted file mode 100644 (file)
index 6e343d2..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import * as React from 'react';
-import { connect } from 'react-redux';
-import { getAppState, Store } from '../../store/rootReducer';
-import { AppState } from '../../types/types';
-import { getWrappedDisplayName } from './utils';
-
-export function withAppState<P>(
-  WrappedComponent: React.ComponentType<P & { appState: Partial<AppState> }>
-) {
-  class Wrapper extends React.Component<P & { appState: AppState }> {
-    static displayName = getWrappedDisplayName(WrappedComponent, 'withAppState');
-
-    render() {
-      return <WrappedComponent {...this.props} />;
-    }
-  }
-
-  function mapStateToProps(state: Store) {
-    return { appState: getAppState(state) };
-  }
-
-  return connect(mapStateToProps)(Wrapper);
-}
index e125022101102962a2e08b57b6c651af6eab9bb4..e9acf7bd30b7147996c8a23a0114241903a9407d 100644 (file)
@@ -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",
index bdfbe23775237a5e0c268620fa94e976d9b1e957..3d8c44c888b94400eb45015794fdc33db679cb31 100644 (file)
 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);
index 53c9be6755fb0b3c635f713046712a5dc8329f4d..2b40b0cdf5fa7c2c739b9c8986bef19063045022 100644 (file)
@@ -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>
 `;
index 5ca310653e61ad33f736974f8dc9ee8ed3cc77e3..ab15daebc76910446193869895eee641ecef7e93 100644 (file)
@@ -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>
 `;
index d46bb3b80f44caec5619e1a3876925bb90c1a7cf..47f592f2defcc73da87793960488f479066e2fc0 100644 (file)
@@ -42,7 +42,7 @@ exports[`should render correctly 1`] = `
         />
       </li>
     </ul>
-    <Connect(withAppState(PublishSteps)) />
+    <withAppStateContext(PublishSteps) />
   </ol>
 </Fragment>
 `;
index 06c9d9ce809565d7a39c87b9a30f21cd45d8932e..3eaf0afd7128651df375990d51d6bef25653f8ea 100644 (file)
@@ -42,7 +42,7 @@ exports[`should render correctly 1`] = `
         />
       </li>
     </ul>
-    <Connect(withAppState(PublishSteps)) />
+    <withAppStateContext(PublishSteps) />
   </ol>
 </Fragment>
 `;
index 44c9b28a3c992d71d3fa5226eaea55bb818e84dd..87f7bf39c726430327e40b7c87dcfd776e654773 100644 (file)
@@ -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>
 `;
index 4605bebcc0c0de95185a5c56b1d6c35cf8808bad..31c332fa14f17cfc6c35b5525f6315a93ca7a77b 100644 (file)
@@ -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);
index a503bbc0baa8eb67eb7019be8cf650eccac41ad9..20b11cbb19d9fc8c5158e69b6cf8d076dc74abfc 100644 (file)
  * 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);
index 81965c60b9976e819b29f46b7c001cb7e96882ad..25a6c4c6240c6a8f3609835b2620f441dfe78da6 100644 (file)
@@ -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}
   />
index 142b08f6b0d6e3a27f110e33f5067a7cd7489a9d..4038f37237e029f9f1a81c5f1d8bc348aa43301e 100644 (file)
@@ -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);
index 85b65c8186a100ceba3df3a948e409130768cf74..bc6ec8c0039ff1fbacaeb21dafea125631444087 100644 (file)
  */
 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);
index 6082843f8d0146e9e76ce99da01efa0c4bd680d3..e3e33091f2bf5e0df4fff9f814c8fff22258a9e4 100644 (file)
@@ -78,7 +78,7 @@ exports[`should render correctly 1`] = `
     onOpen={[Function]}
     open={false}
   />
-  <Connect(withAppState(YmlFileStep))
+  <withAppStateContext(YmlFileStep)
     finished={false}
     onDone={[Function]}
     onOpen={[Function]}
index e8ed96e6bb9d803b0408dd0292047fde00685d01..15ddfb7fa8697f35b9ffcff1ce3abad0a840d234 100644 (file)
  */
 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));
index 4fb31b5e9b770334f05626ed59b2ce0bf798fd34..beb1264b452899752dab4f541fd85da31d9c99a7 100644 (file)
@@ -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()}
index 9af265ff69755ea49e2ef91386e8c0ec71752196..6826d56c33691aa9eeda6f4f71fa999200e3506a 100644 (file)
  */
 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);
index 9de6742c42a57cf1ea9a434d78d1afc23b460e5b..c3e37a9c693236a1a7a712b3ea35decb39af7a68 100644 (file)
@@ -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"
index dfe4553cb3a00e828b399dcf8e489247aa352f14..c31750106457829497aa247e2ff22e2cc07c1656 100644 (file)
  * 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 };
   }
index 574818ce47b03e58f6bc04915bf338427751aef7..01544faf61e08567102fdaf5b0d316edf58e04ee 100644 (file)
@@ -84,7 +84,6 @@ export interface AnalysisEvent {
 }
 
 export interface AppState {
-  adminPages?: Extension[];
   authenticationError?: boolean;
   authorizationError?: boolean;
   branchesEnabled?: boolean;