]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15992 Remove Global Settings from redux
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 8 Mar 2022 14:21:03 +0000 (15:21 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 11 Mar 2022 20:02:49 +0000 (20:02 +0000)
108 files changed:
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/GlobalFooter.tsx
server/sonar-web/src/main/js/app/components/PageTracker.tsx
server/sonar-web/src/main/js/app/components/StartupModal.tsx
server/sonar-web/src/main/js/app/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx
server/sonar-web/src/main/js/app/components/app-state/AppStateContext.tsx
server/sonar-web/src/main/js/app/components/app-state/AppStateContextProvider.tsx
server/sonar-web/src/main/js/app/components/app-state/__tests__/AppStateContextProvider-test.tsx
server/sonar-web/src/main/js/app/components/app-state/__tests__/__snapshots__/AppStateContextProvider-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/app-state/__tests__/withAppStateContext-test.tsx
server/sonar-web/src/main/js/app/components/app-state/withAppStateContext.tsx
server/sonar-web/src/main/js/app/components/extensions/Extension.tsx
server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx
server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
server/sonar-web/src/main/js/app/components/indexation/IndexationContextProvider.tsx
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/branch-like/BranchLikeNavigation.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavBranding-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavBranding-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-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/change-admin-password/ChangeAdminPasswordApp.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx
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/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx [deleted file]
server/sonar-web/src/main/js/apps/marketplace/MarketplaceAppContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/marketplace/__tests__/MarketplaceAppContainer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/MarketplaceAppContainer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/routes.ts
server/sonar-web/src/main/js/apps/overview/components/App.tsx
server/sonar-web/src/main/js/apps/overview/components/IssueRating.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueRating-test.tsx.snap
server/sonar-web/src/main/js/apps/permission-templates/components/App.tsx
server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx
server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/UserHolder-test.tsx.snap
server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/LifetimeInformation.tsx
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/ApplicationCreation.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/PermissionItem-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsUser-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistory-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx
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/pullRequestDecorationBinding/AlmSpecificForm.tsx
server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts [deleted file]
server/sonar-web/src/main/js/apps/settings/store/actions.ts [deleted file]
server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts [deleted file]
server/sonar-web/src/main/js/apps/settings/store/values.ts [deleted file]
server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearch-test.tsx.snap
server/sonar-web/src/main/js/components/controls/ComponentReportActions.tsx
server/sonar-web/src/main/js/components/docs/DocLink.tsx
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.tsx.snap
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.tsx.snap
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.tsx.snap
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetAssigneePopup-test.tsx.snap
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SimilarIssuesPopup-test.tsx.snap
server/sonar-web/src/main/js/components/measure/Measure.tsx
server/sonar-web/src/main/js/components/measure/RatingTooltipContent.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx
server/sonar-web/src/main/js/components/measure/__tests__/RatingTooltipContent-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/Measure-test.tsx.snap
server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/RatingTooltipContent-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/measure/utils.ts
server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/PublishSteps.tsx
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/github-action/AnalysisCommand.tsx
server/sonar-web/src/main/js/components/tutorials/gitlabci/YmlFileStep.tsx
server/sonar-web/src/main/js/components/tutorials/jenkins/JenkinsTutorial.tsx
server/sonar-web/src/main/js/components/ui/Avatar.tsx
server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.tsx
server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx
server/sonar-web/src/main/js/helpers/analytics.js [deleted file]
server/sonar-web/src/main/js/helpers/measures.ts
server/sonar-web/src/main/js/helpers/testMocks.ts
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
server/sonar-web/src/main/js/store/rootReducer.ts
server/sonar-web/src/main/js/types/appstate.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/extension.ts
server/sonar-web/src/main/js/types/settings.ts
server/sonar-web/src/main/js/types/types.ts

index 8a690681ac789fe6f229caea81bdef8f14d60f9d..0f05906ec49f32ef987aa43e296bfa3d36dd9e39 100644 (file)
@@ -24,8 +24,9 @@ import { getPendingPlugins } from '../../api/plugins';
 import { getSystemStatus, waitSystemUPStatus } from '../../api/system';
 import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
 import { translate } from '../../helpers/l10n';
+import { AppState } from '../../types/appstate';
 import { PendingPluginResult } from '../../types/plugins';
-import { AppState, Extension, SysStatus } from '../../types/types';
+import { Extension, SysStatus } from '../../types/types';
 import AdminContext, { defaultPendingPlugins, defaultSystemStatus } from './AdminContext';
 import withAppStateContext from './app-state/withAppStateContext';
 import SettingsNav from './nav/settings/SettingsNav';
index b7e22a74638add71ce0f96742502f8a1ba9f74c3..e7dcd7d065414ca17720681b500a7e4e0b67f23c 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { connect } from 'react-redux';
 import { lazyLoadComponent } from '../../components/lazyLoadComponent';
-import { getGlobalSettingValue, Store } from '../../store/rootReducer';
+import { AppState } from '../../types/appstate';
+import { GlobalSettingKeys } from '../../types/settings';
+import withAppStateContext from './app-state/withAppStateContext';
 import KeyboardShortcutsModal from './KeyboardShortcutsModal';
 
 const PageTracker = lazyLoadComponent(() => import('./PageTracker'));
 
 interface Props {
-  enableGravatar: boolean;
-  gravatarServerUrl: string;
+  appState: AppState;
 }
 
 export class App extends React.PureComponent<Props> {
@@ -68,8 +68,19 @@ export class App extends React.PureComponent<Props> {
   };
 
   renderPreconnectLink = () => {
+    const {
+      appState: { settings }
+    } = this.props;
+
+    const enableGravatar = settings[GlobalSettingKeys.EnableGravatar] === 'true';
+    const gravatarServerUrl = settings[GlobalSettingKeys.GravatarServerUrl];
+
+    if (!enableGravatar || !gravatarServerUrl) {
+      return null;
+    }
+
     const parser = document.createElement('a');
-    parser.href = this.props.gravatarServerUrl;
+    parser.href = gravatarServerUrl;
     if (parser.hostname !== window.location.hostname) {
       return <link href={parser.origin} rel="preconnect" />;
     }
@@ -79,7 +90,7 @@ export class App extends React.PureComponent<Props> {
   render() {
     return (
       <>
-        <PageTracker>{this.props.enableGravatar && this.renderPreconnectLink()}</PageTracker>
+        <PageTracker>{this.renderPreconnectLink()}</PageTracker>
         {this.props.children}
         <KeyboardShortcutsModal />
       </>
@@ -87,13 +98,4 @@ export class App extends React.PureComponent<Props> {
   }
 }
 
-const mapStateToProps = (state: Store) => {
-  const enableGravatar = getGlobalSettingValue(state, 'sonar.lf.enableGravatar');
-  const gravatarServerUrl = getGlobalSettingValue(state, 'sonar.lf.gravatarServerUrl');
-  return {
-    enableGravatar: Boolean(enableGravatar && enableGravatar.value === 'true'),
-    gravatarServerUrl: (gravatarServerUrl && gravatarServerUrl.value) || ''
-  };
-};
-
-export default connect(mapStateToProps)(App);
+export default withAppStateContext(App);
index 61aafd19ee90f7c8fd12e30d029822cc382d79dd..e663080820ae9d198f2ff83f83276956ddd884c6 100644 (file)
@@ -39,10 +39,11 @@ import {
   ProjectAlmBindingConfigurationErrors,
   ProjectAlmBindingResponse
 } from '../../types/alm-settings';
+import { AppState } from '../../types/appstate';
 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 { Component, Status } from '../../types/types';
 import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
 import withAppStateContext from './app-state/withAppStateContext';
 import ComponentContainerNotFound from './ComponentContainerNotFound';
index da00469d33ce0dcd78d1923103b3033db22573b4..fae270252e2ff61fb3236b4963499b35443ce2a4 100644 (file)
@@ -23,8 +23,8 @@ import InstanceMessage from '../../components/common/InstanceMessage';
 import { Alert } from '../../components/ui/Alert';
 import { getEdition } from '../../helpers/editions';
 import { translate, translateWithParameters } from '../../helpers/l10n';
+import { AppState } from '../../types/appstate';
 import { EditionKey } from '../../types/editions';
-import { AppState } from '../../types/types';
 import withAppStateContext from './app-state/withAppStateContext';
 import GlobalFooterBranding from './GlobalFooterBranding';
 
index 02c47e467fb4486ab8b3c3dde902b4b72aa6617b..7ae1adbf0a9d38a29222eda36d4eb5770e887932 100644 (file)
  */
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
-import { connect } from 'react-redux';
 import { Location, withRouter } from '../../components/hoc/withRouter';
-import { gtm } from '../../helpers/analytics';
 import { installScript } from '../../helpers/extensions';
 import { getWebAnalyticsPageHandlerFromCache } from '../../helpers/extensionsHandler';
 import { getInstance } from '../../helpers/system';
-import { getGlobalSettingValue, Store } from '../../store/rootReducer';
-import { AppState } from '../../types/types';
+import { AppState } from '../../types/appstate';
 import withAppStateContext from './app-state/withAppStateContext';
 
 interface Props {
   location: Location;
-  trackingIdGTM?: string;
   appState: AppState;
 }
 
@@ -43,54 +39,37 @@ export class PageTracker extends React.Component<Props, State> {
   state: State = {};
 
   componentDidMount() {
-    const { trackingIdGTM, appState } = this.props;
+    const { appState } = this.props;
 
     if (appState.webAnalyticsJsPath && !getWebAnalyticsPageHandlerFromCache()) {
       installScript(appState.webAnalyticsJsPath, 'head');
     }
-
-    if (trackingIdGTM) {
-      gtm(trackingIdGTM);
-    }
   }
 
   trackPage = () => {
-    const { location, trackingIdGTM } = this.props;
+    const { location } = this.props;
     const { lastLocation } = this.state;
-    const { dataLayer } = window as any;
     const locationChanged = location.pathname !== lastLocation;
     const webAnalyticsPageChange = getWebAnalyticsPageHandlerFromCache();
 
     if (webAnalyticsPageChange && locationChanged) {
       this.setState({ lastLocation: location.pathname });
       setTimeout(() => webAnalyticsPageChange(location.pathname), 500);
-    } else if (dataLayer && dataLayer.push && trackingIdGTM && location.pathname !== '/') {
-      this.setState({ lastLocation: location.pathname });
-      setTimeout(() => dataLayer.push({ event: 'render-end' }), 500);
     }
   };
 
   render() {
-    const { trackingIdGTM, appState } = this.props;
+    const { appState } = this.props;
 
     return (
       <Helmet
         defaultTitle={getInstance()}
         defer={false}
-        onChangeClientState={
-          trackingIdGTM || appState.webAnalyticsJsPath ? this.trackPage : undefined
-        }>
+        onChangeClientState={appState.webAnalyticsJsPath ? this.trackPage : undefined}>
         {this.props.children}
       </Helmet>
     );
   }
 }
 
-const mapStateToProps = (state: Store) => {
-  const trackingIdGTM = getGlobalSettingValue(state, 'sonar.analytics.gtm.trackingId');
-  return {
-    trackingIdGTM: trackingIdGTM && trackingIdGTM.value
-  };
-};
-
-export default withRouter(connect(mapStateToProps)(withAppStateContext(PageTracker)));
+export default withRouter(withAppStateContext(PageTracker));
index ff8216a49d1c1a79bb9fe0c7da0f41b17f01dbff..5b7c3915672ecabe0046e1884d30bde55de9b0d4 100644 (file)
@@ -28,8 +28,9 @@ import { hasMessage } from '../../helpers/l10n';
 import { get, save } from '../../helpers/storage';
 import { isLoggedIn } from '../../helpers/users';
 import { getCurrentUser, Store } from '../../store/rootReducer';
+import { AppState } from '../../types/appstate';
 import { EditionKey } from '../../types/editions';
-import { AppState, CurrentUser } from '../../types/types';
+import { CurrentUser } from '../../types/types';
 import withAppStateContext from './app-state/withAppStateContext';
 
 const LicensePromptModal = lazyLoadComponent(
index 75b9d805b88eaf04316b1660991427c554c0da9b..842155e2977cbfb285bc18403f3bef9391e0710a 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { connect } from 'react-redux';
+import { mockAppState } from '../../../helpers/testMocks';
 import { App } from '../App';
 
-jest.mock('react-redux', () => ({
-  connect: jest.fn(() => (a: any) => a)
-}));
-
-jest.mock('../../../store/rootReducer', () => ({
-  getGlobalSettingValue: jest.fn((_, key: string) => ({
-    value: key === 'sonar.lf.enableGravatar' ? 'true' : 'http://gravatar.com'
-  }))
-}));
-
 it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot('default');
   expect(
-    shallowRender({ enableGravatar: true, gravatarServerUrl: 'http://example.com' })
+    shallowRender({
+      appState: mockAppState({
+        settings: {
+          'sonar.lf.enableGravatar': 'true',
+          'sonar.lf.gravatarServerUrl': 'http://example.com'
+        }
+      })
+    })
   ).toMatchSnapshot('with gravatar');
 });
 
@@ -44,17 +41,16 @@ it('should correctly set the scrollbar width as a custom property', () => {
   expect(document.body.style.getPropertyValue('--sbw')).toBe('0px');
 });
 
-describe('redux', () => {
-  it('should correctly map state props', () => {
-    const [mapStateToProps] = (connect as jest.Mock).mock.calls[0];
-
-    expect(mapStateToProps({})).toEqual({
-      enableGravatar: true,
-      gravatarServerUrl: 'http://gravatar.com'
-    });
-  });
-});
-
 function shallowRender(props: Partial<App['props']> = {}) {
-  return shallow<App>(<App enableGravatar={false} gravatarServerUrl="" {...props} />);
+  return shallow<App>(
+    <App
+      appState={mockAppState({
+        settings: {
+          'sonar.lf.enableGravatar': 'false',
+          'sonar.lf.gravatarServerUrl': ''
+        }
+      })}
+      {...props}
+    />
+  );
 }
index 5da29b3146334b31cade5d71bf9494c962a915e3..5e7d3dfde6cac777f96fbd0b69ee997d5fc8be92 100644 (file)
@@ -19,7 +19,6 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { gtm } from '../../../helpers/analytics';
 import { installScript } from '../../../helpers/extensions';
 import { getWebAnalyticsPageHandlerFromCache } from '../../../helpers/extensionsHandler';
 import { mockAppState, mockLocation } from '../../../helpers/testMocks';
@@ -33,7 +32,6 @@ jest.mock('../../../helpers/extensionsHandler', () => ({
   getWebAnalyticsPageHandlerFromCache: jest.fn().mockReturnValue(undefined)
 }));
 
-jest.mock('../../../helpers/analytics', () => ({ gtm: jest.fn() }));
 beforeAll(() => {
   jest.useFakeTimers();
 });
@@ -52,7 +50,6 @@ it('should not trigger if no analytics system is given', () => {
   const wrapper = shallowRender();
   expect(wrapper).toMatchSnapshot();
   expect(installScript).not.toHaveBeenCalled();
-  expect(gtm).not.toHaveBeenCalled();
 });
 
 it('should work for WebAnalytics plugin', () => {
@@ -70,22 +67,6 @@ it('should work for WebAnalytics plugin', () => {
   expect(pageChange).toHaveBeenCalledWith('/path');
 });
 
-it('should work for Google Tag Manager', () => {
-  (window as any).dataLayer = [];
-  const { dataLayer } = window as any;
-  const push = jest.spyOn(dataLayer, 'push');
-  const wrapper = shallowRender({ trackingIdGTM: '123' });
-
-  expect(wrapper.find('Helmet').prop('onChangeClientState')).toBe(wrapper.instance().trackPage);
-  expect(gtm).toBeCalled();
-  expect(dataLayer).toHaveLength(0);
-
-  wrapper.instance().trackPage();
-  jest.runAllTimers();
-  expect(push).toBeCalledWith({ event: 'render-end' });
-  expect(dataLayer).toHaveLength(1);
-});
-
 function shallowRender(props: Partial<PageTracker['props']> = {}) {
   return shallow<PageTracker>(
     <PageTracker appState={mockAppState()} location={mockLocation()} {...props} />
index 1e2dcf47d4f56fd96fca1f5b58e57b40f0a70743..a8e130c84d2dd94cea0aa9c6f59968de85ba5218 100644 (file)
@@ -19,7 +19,7 @@
  */
 
 import * as React from 'react';
-import { AppState } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
 
 const defaultAppState = {
   authenticationError: false,
index 1d48066c47a6cbe0498902693d3e674e072a2aa6..43c937b651f996357a441db8cad8a1cae85a0476 100644 (file)
  */
 
 import * as React from 'react';
-import { connect } from 'react-redux';
-import { setValues } from '../../../apps/settings/store/actions';
-import { AppState } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
 import { AppStateContext } from './AppStateContext';
 
 export interface AppStateContextProviderProps {
-  setValues: typeof setValues;
   appState: AppState;
 }
 
-export function AppStateContextProvider({
-  setValues,
+export default function AppStateContextProvider({
   appState,
   children
 }: React.PropsWithChildren<AppStateContextProviderProps>) {
-  React.useEffect(() => {
-    setValues(
-      Object.keys(appState.settings),
-      Object.entries(appState.settings).map(([key, value]) => ({ key, value }))
-    );
-  });
-
   return <AppStateContext.Provider value={appState}>{children}</AppStateContext.Provider>;
 }
-const mapDispatchToProps = { setValues };
-export default connect(null, mapDispatchToProps)(AppStateContextProvider);
index 05d5fe55e21fbd5505dd10a69835a1c22ff89a6f..57e3f0a1e0aef37d6f0055761f5f41ac0481f894 100644 (file)
@@ -21,20 +21,18 @@ import { mount } from 'enzyme';
 import * as React from 'react';
 import { mockAppState } from '../../../../helpers/testMocks';
 import { waitAndUpdate } from '../../../../helpers/testUtils';
-import { AppStateContextProvider, AppStateContextProviderProps } from '../AppStateContextProvider';
+import AppStateContextProvider, { AppStateContextProviderProps } from '../AppStateContextProvider';
 
 it('should set value correctly', async () => {
-  const setValues = jest.fn();
+  const appState = mockAppState({ settings: { 'sonar.lf.logoUrl': 'whatevs/' } });
   const wrapper = render({
-    appState: mockAppState({ settings: { foo: 'bar' } }),
-    setValues
+    appState
   });
   await waitAndUpdate(wrapper);
-  expect(setValues).toHaveBeenCalledWith(['foo'], [{ key: 'foo', value: 'bar' }]);
+
+  expect(wrapper).toMatchSnapshot();
 });
 
 function render(override?: Partial<AppStateContextProviderProps>) {
-  return mount(
-    <AppStateContextProvider appState={mockAppState()} setValues={jest.fn()} {...override} />
-  );
+  return mount(<AppStateContextProvider appState={mockAppState()} {...override} />);
 }
diff --git a/server/sonar-web/src/main/js/app/components/app-state/__tests__/__snapshots__/AppStateContextProvider-test.tsx.snap b/server/sonar-web/src/main/js/app/components/app-state/__tests__/__snapshots__/AppStateContextProvider-test.tsx.snap
new file mode 100644 (file)
index 0000000..1e81e16
--- /dev/null
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should set value correctly 1`] = `
+<AppStateContextProvider
+  appState={
+    Object {
+      "edition": "community",
+      "productionDatabase": true,
+      "qualifiers": Array [
+        "TRK",
+      ],
+      "settings": Object {
+        "sonar.lf.logoUrl": "whatevs/",
+      },
+      "version": "1.0",
+    }
+  }
+/>
+`;
index d6323fb0f811eacd2dab63482b2b09f9d54cc3c3..78e009131c20ebdffa7af024eb442686ba4481d0 100644 (file)
@@ -20,7 +20,7 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { mockAppState } from '../../../../helpers/testMocks';
-import { AppState } from '../../../../types/types';
+import { AppState } from '../../../../types/appstate';
 import withAppStateContext from '../withAppStateContext';
 
 const appState = mockAppState();
index 64107510cf1b2acff0db0a5921bb684a0b97e621..fa432377d1e2526d3d34e7e8453cf7bbf55abf5b 100644 (file)
@@ -20,7 +20,7 @@
 
 import * as React from 'react';
 import { getWrappedDisplayName } from '../../../components/hoc/utils';
-import { AppState } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
 import { AppStateContext } from './AppStateContext';
 
 export interface WithAppStateContextProps {
index aeb29e3d72a7512a5544e9daaa7fad72da6d5fdb..db90d3e034223869421ad85b88800359e869882f 100644 (file)
@@ -27,8 +27,9 @@ import { getCurrentL10nBundle, translate } from '../../../helpers/l10n';
 import { getBaseUrl } from '../../../helpers/system';
 import { addGlobalErrorMessage } from '../../../store/globalMessages';
 import { getCurrentUser, Store } from '../../../store/rootReducer';
+import { AppState } from '../../../types/appstate';
 import { ExtensionStartMethod } from '../../../types/extension';
-import { AppState, CurrentUser, Dict, Extension as TypeExtension } from '../../../types/types';
+import { CurrentUser, Dict, Extension as TypeExtension } from '../../../types/types';
 import * as theme from '../../theme';
 import getStore from '../../utils/getStore';
 import withAppStateContext from '../app-state/withAppStateContext';
index d3b0559d35ce1fba86953aa667f44f6688b58b39..0c4206497c2f45bd2e0183302450d8426e4c6ba8 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { AppState } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
 import withAppStateContext from '../app-state/withAppStateContext';
 import NotFound from '../NotFound';
 import Extension from './Extension';
index e86c3868d16cc840b037b0cc3daa529d69d1f79e..68a380c179aff9f3d77e30e6211c11679eee6526 100644 (file)
@@ -70,6 +70,7 @@ import DateFormatter from '../../../components/intl/DateFormatter';
 import DateFromNow from '../../../components/intl/DateFromNow';
 import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import Measure from '../../../components/measure/Measure';
+import RatingTooltipContent from '../../../components/measure/RatingTooltipContent';
 import { Alert } from '../../../components/ui/Alert';
 import CoverageRating from '../../../components/ui/CoverageRating';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
@@ -237,6 +238,7 @@ const exposeLibraries = () => {
         Radio,
         RadioToggle,
         Rating,
+        RatingTooltipContent,
         ReloadButton,
         ResetButtonLink,
         SearchBox,
index 091fc03024d2ed0f6f0338edaaed90824b0b6f05..e2b4e58a53826ae3855b5a2d20911a14e098fbe8 100644 (file)
@@ -19,8 +19,8 @@
  */
 /* eslint-disable react/no-unused-state */
 import * as React from 'react';
+import { AppState } from '../../../types/appstate';
 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';
index 37770276d4fefdaba9e3a1da6e715620794c04b9..7c9e217e1ab9f07d49cd0cd0e33adda6d2595c6e 100644 (file)
@@ -22,9 +22,9 @@ import { Link } from 'react-router';
 import { isValidLicense } from '../../../../api/marketplace';
 import { Alert } from '../../../../components/ui/Alert';
 import { translate, translateWithParameters } from '../../../../helpers/l10n';
+import { AppState } from '../../../../types/appstate';
 import { ComponentQualifier } from '../../../../types/component';
 import { Task } from '../../../../types/tasks';
-import { AppState } from '../../../../types/types';
 import withAppStateContext from '../../app-state/withAppStateContext';
 
 interface Props {
index 7f7d3c3284394b6eb7ba32a0309b4d6a00babc5e..7b33626b62a962e8ef4d9c9706e5a49b1c93efd0 100644 (file)
@@ -30,9 +30,10 @@ import NavBarTabs from '../../../../components/ui/NavBarTabs';
 import { getBranchLikeQuery, isPullRequest } from '../../../../helpers/branch-like';
 import { hasMessage, translate, translateWithParameters } from '../../../../helpers/l10n';
 import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls';
+import { AppState } from '../../../../types/appstate';
 import { BranchLike, BranchParameters } from '../../../../types/branch-like';
 import { ComponentQualifier, isPortfolioLike } from '../../../../types/component';
-import { AppState, Component, Extension } from '../../../../types/types';
+import { Component, Extension } from '../../../../types/types';
 import withAppStateContext from '../../app-state/withAppStateContext';
 import './Menu.css';
 
index 2e619a4e0493a452ecb370db7a49447f2a4a7ac6..3688efac0b2906289eb95ff21af1a7b44b6d81d3 100644 (file)
@@ -21,8 +21,9 @@ import classNames from 'classnames';
 import * as React from 'react';
 import Toggler from '../../../../../components/controls/Toggler';
 import { ProjectAlmBindingResponse } from '../../../../../types/alm-settings';
+import { AppState } from '../../../../../types/appstate';
 import { BranchLike } from '../../../../../types/branch-like';
-import { AppState, Component } from '../../../../../types/types';
+import { Component } from '../../../../../types/types';
 import withAppStateContext from '../../../app-state/withAppStateContext';
 import './BranchLikeNavigation.css';
 import CurrentBranchLike from './CurrentBranchLike';
index 2706aa5cffbea63fc29104104b837d844a357813..6a952f3cc4bdeeb32f621a76c94f284ddfbbe2e6 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { connect } from 'react-redux';
 import { Link } from 'react-router';
 import { translate } from '../../../../helpers/l10n';
 import { getBaseUrl } from '../../../../helpers/system';
-import { getGlobalSettingValue, Store } from '../../../../store/rootReducer';
+import { AppState } from '../../../../types/appstate';
+import { GlobalSettingKeys } from '../../../../types/settings';
+import withAppStateContext from '../../app-state/withAppStateContext';
 
-interface StateProps {
-  customLogoUrl?: string;
-  customLogoWidth?: string | number;
+export interface GlobalNavBrandingProps {
+  appState: AppState;
 }
 
-export function GlobalNavBranding({ customLogoUrl, customLogoWidth }: StateProps) {
+export function GlobalNavBranding({ appState: { settings } }: GlobalNavBrandingProps) {
+  const customLogoUrl = settings[GlobalSettingKeys.LogoUrl];
+  const customLogoWidth = settings[GlobalSettingKeys.LogoWidth];
+
   const title = translate('layout.sonar.slogan');
   const url = customLogoUrl || `${getBaseUrl()}/images/logo.svg?v=6.6`;
   const width = customLogoUrl ? customLogoWidth || 100 : 83;
@@ -41,13 +44,4 @@ export function GlobalNavBranding({ customLogoUrl, customLogoWidth }: StateProps
   );
 }
 
-const mapStateToProps = (state: Store): StateProps => {
-  const customLogoUrl = getGlobalSettingValue(state, 'sonar.lf.logoUrl');
-  const customLogoWidth = getGlobalSettingValue(state, 'sonar.lf.logoWidthPx');
-  return {
-    customLogoUrl: customLogoUrl && customLogoUrl.value,
-    customLogoWidth: customLogoWidth && customLogoWidth.value
-  };
-};
-
-export default connect(mapStateToProps)(GlobalNavBranding);
+export default withAppStateContext(GlobalNavBranding);
index daf8af77711bfcbae5e383e9bae099092a296c4c..230bdffe274e5597d00c92fa26c4a451bcd46d86 100644 (file)
@@ -25,8 +25,9 @@ import Dropdown from '../../../../components/controls/Dropdown';
 import DropdownIcon from '../../../../components/icons/DropdownIcon';
 import { translate } from '../../../../helpers/l10n';
 import { getQualityGatesUrl } from '../../../../helpers/urls';
+import { AppState } from '../../../../types/appstate';
 import { ComponentQualifier } from '../../../../types/component';
-import { AppState, CurrentUser, Extension } from '../../../../types/types';
+import { CurrentUser, Extension } from '../../../../types/types';
 import withAppStateContext from '../../app-state/withAppStateContext';
 
 interface Props {
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavBranding-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavBranding-test.tsx
new file mode 100644 (file)
index 0000000..f182f4d
--- /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 { GlobalNavBranding, GlobalNavBrandingProps } from '../GlobalNavBranding';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(
+    shallowRender({
+      appState: mockAppState({
+        settings: {
+          'sonar.lf.logoUrl': 'http://sonarsource.com/custom-logo.svg'
+        }
+      })
+    })
+  ).toMatchSnapshot('with logo');
+  expect(
+    shallowRender({
+      appState: mockAppState({
+        settings: {
+          'sonar.lf.logoUrl': 'http://sonarsource.com/custom-logo.svg',
+          'sonar.lf.logoWidthPx': '200'
+        }
+      })
+    })
+  ).toMatchSnapshot('with logo and width');
+});
+
+function shallowRender(overrides: Partial<GlobalNavBrandingProps> = {}) {
+  return shallow(<GlobalNavBranding appState={mockAppState()} {...overrides} />);
+}
index cc530594fb38f9be21213534fe63b426b1bf709e..964e19a9cdbd092c3cd88e424f17ac96d5c5092d 100644 (file)
@@ -6,7 +6,7 @@ exports[`should render correctly: anonymous users 1`] = `
   height={48}
   id="global-navigation"
 >
-  <Connect(GlobalNavBranding) />
+  <withAppStateContext(GlobalNavBranding) />
   <withAppStateContext(GlobalNavMenu)
     currentUser={
       Object {
@@ -47,7 +47,7 @@ exports[`should render correctly: logged in users 1`] = `
   height={48}
   id="global-navigation"
 >
-  <Connect(GlobalNavBranding) />
+  <withAppStateContext(GlobalNavBranding) />
   <withAppStateContext(GlobalNavMenu)
     currentUser={
       Object {
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavBranding-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavBranding-test.tsx.snap
new file mode 100644 (file)
index 0000000..5419fd4
--- /dev/null
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<Link
+  className="navbar-brand"
+  onlyActiveOnIndex={false}
+  style={Object {}}
+  to="/"
+>
+  <img
+    alt="layout.sonar.slogan"
+    height={30}
+    src="/images/logo.svg?v=6.6"
+    title="layout.sonar.slogan"
+    width={83}
+  />
+</Link>
+`;
+
+exports[`should render correctly: with logo 1`] = `
+<Link
+  className="navbar-brand"
+  onlyActiveOnIndex={false}
+  style={Object {}}
+  to="/"
+>
+  <img
+    alt="layout.sonar.slogan"
+    height={30}
+    src="http://sonarsource.com/custom-logo.svg"
+    title="layout.sonar.slogan"
+    width={100}
+  />
+</Link>
+`;
+
+exports[`should render correctly: with logo and width 1`] = `
+<Link
+  className="navbar-brand"
+  onlyActiveOnIndex={false}
+  style={Object {}}
+  to="/"
+>
+  <img
+    alt="layout.sonar.slogan"
+    height={30}
+    src="http://sonarsource.com/custom-logo.svg"
+    title="layout.sonar.slogan"
+    width="200"
+  />
+</Link>
+`;
index b509fb25cd24b2b97490bc08303d2d26046ea6db..f131dfc9555bd2ec0fdb839ba44f37de42af7e27 100644 (file)
@@ -60,7 +60,7 @@ exports[`should render the right interface for logged in user 1`] = `
     href="#"
     title="Skywalker"
   >
-    <Connect(Avatar)
+    <withAppStateContext(Avatar)
       name="Skywalker"
       size={32}
     />
index 8958a1c02d6b093b16306f8e7e99f191ae7d89e4..b399276ddf045d11e38c3931ed836ae405db409d 100644 (file)
@@ -27,9 +27,10 @@ import SystemUpgradeButton from '../../../components/upgrade/SystemUpgradeButton
 import { sortUpgrades, UpdateUseCase } from '../../../components/upgrade/utils';
 import { translate } from '../../../helpers/l10n';
 import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users';
+import { AppState } from '../../../types/appstate';
 import { Permissions } from '../../../types/permissions';
 import { SystemUpgrade } from '../../../types/system';
-import { AppState, CurrentUser, Dict } from '../../../types/types';
+import { CurrentUser, Dict } from '../../../types/types';
 import withAppStateContext from '../app-state/withAppStateContext';
 import './UpdateNotification.css';
 
index 88081787e945b0af0d5c58b128810c6d4e27ceb0..f63328930f44b2605b2dc4ba23333674915c1143 100644 (file)
@@ -21,7 +21,7 @@ import { installExtensionsHandler, installWebAnalyticsHandler } from '../helpers
 import { loadL10nBundle } from '../helpers/l10n';
 import { parseJSON, request } from '../helpers/request';
 import { getBaseUrl, getSystemStatus } from '../helpers/system';
-import { AppState } from '../types/types';
+import { AppState } from '../types/appstate';
 import './styles/sonar.ts';
 
 installWebAnalyticsHandler();
index 0fa4217844bf810f8147707cf3b61603829a5540..6854fa5fbb75e1c990e5a3feb20fb8595478955c 100644 (file)
@@ -60,7 +60,8 @@ import webhooksRoutes from '../../apps/webhooks/routes';
 import withIndexationGuard from '../../components/hoc/withIndexationGuard';
 import { lazyLoadComponent } from '../../components/lazyLoadComponent';
 import getHistory from '../../helpers/getHistory';
-import { AppState, CurrentUser } from '../../types/types';
+import { AppState } from '../../types/appstate';
+import { CurrentUser } from '../../types/types';
 import App from '../components/App';
 import AppStateContextProvider from '../components/app-state/AppStateContextProvider';
 import GlobalContainer from '../components/GlobalContainer';
index df64cb4c24ba5fa09f011bb3d5231c106fcf4f4c..da2e3196a93412f60cf5bf7b6d07c8645f684a29 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { connect } from 'react-redux';
-import { getGlobalSettingValue, Store } from '../../../store/rootReducer';
+import { getValues } from '../../../api/settings';
 import { AdminPageExtension } from '../../../types/extension';
+import { SettingsKey } from '../../../types/settings';
 import { Extension } from '../../../types/types';
-import { fetchValues } from '../../settings/store/actions';
 import '../style.css';
 import { HousekeepingPolicy, RangeOption } from '../utils';
 import AuditAppRenderer from './AuditAppRenderer';
 
 interface Props {
-  auditHousekeepingPolicy: HousekeepingPolicy;
-  fetchValues: typeof fetchValues;
   adminPages: Extension[];
 }
 
 interface State {
   dateRange?: { from?: Date; to?: Date };
-  hasGovernanceExtension?: boolean;
   downloadStarted: boolean;
+  housekeepingPolicy: HousekeepingPolicy;
   selection: RangeOption;
 }
 
-export class AuditApp extends React.PureComponent<Props, State> {
+export default class AuditApp extends React.PureComponent<Props, State> {
   constructor(props: Props) {
     super(props);
-    const hasGovernanceExtension = Boolean(
-      props.adminPages?.find(e => e.key === AdminPageExtension.GovernanceConsole)
-    );
+
     this.state = {
       downloadStarted: false,
-      selection: RangeOption.Today,
-      hasGovernanceExtension
+      housekeepingPolicy: HousekeepingPolicy.Monthly,
+      selection: RangeOption.Today
     };
   }
 
   componentDidMount() {
-    const { hasGovernanceExtension } = this.state;
-
-    if (hasGovernanceExtension) {
-      this.props.fetchValues(['sonar.dbcleaner.auditHousekeeping']);
+    if (this.hasGovernanceExtension()) {
+      this.fetchHouseKeepingPolicy();
     }
   }
 
   componentDidUpdate(prevProps: Props) {
-    if (prevProps.adminPages !== this.props.adminPages) {
-      const hasGovernanceExtension = Boolean(
-        this.props.adminPages?.find(e => e.key === AdminPageExtension.GovernanceConsole)
-      );
-      this.setState({
-        hasGovernanceExtension
-      });
+    if (prevProps.adminPages !== this.props.adminPages && this.hasGovernanceExtension()) {
+      this.fetchHouseKeepingPolicy();
     }
   }
 
+  fetchHouseKeepingPolicy = async () => {
+    const results = await getValues({ keys: SettingsKey.AuditHouseKeeping });
+
+    this.setState({
+      housekeepingPolicy:
+        (results[0]?.value as HousekeepingPolicy | undefined) ?? HousekeepingPolicy.Monthly
+    });
+  };
+
+  hasGovernanceExtension = () => {
+    return Boolean(
+      this.props.adminPages?.find(e => e.key === AdminPageExtension.GovernanceConsole)
+    );
+  };
+
   handleDateSelection = (dateRange: { from?: Date; to?: Date }) =>
     this.setState({ dateRange, downloadStarted: false, selection: RangeOption.Custom });
 
@@ -85,10 +88,7 @@ export class AuditApp extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { hasGovernanceExtension, ...auditAppRendererProps } = this.state;
-    const { auditHousekeepingPolicy } = this.props;
-
-    if (!hasGovernanceExtension) {
+    if (!this.hasGovernanceExtension()) {
       return null;
     }
 
@@ -97,20 +97,8 @@ export class AuditApp extends React.PureComponent<Props, State> {
         handleDateSelection={this.handleDateSelection}
         handleOptionSelection={this.handleOptionSelection}
         handleStartDownload={this.handleStartDownload}
-        housekeepingPolicy={auditHousekeepingPolicy || HousekeepingPolicy.Monthly}
-        {...auditAppRendererProps}
+        {...this.state}
       />
     );
   }
 }
-
-const mapDispatchToProps = { fetchValues };
-
-const mapStateToProps = (state: Store) => {
-  const settingValue = getGlobalSettingValue(state, 'sonar.dbcleaner.auditHousekeeping');
-  return {
-    auditHousekeepingPolicy: settingValue?.value as HousekeepingPolicy
-  };
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(AuditApp);
index 1b9740af7603f36d3c0193c88a9219b9b790acce..baac6e88cdaf29f9596930acd16f2d15d8942bb5 100644 (file)
 import { subDays } from 'date-fns';
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { getValues } from '../../../../api/settings';
 import { waitAndUpdate } from '../../../../helpers/testUtils';
 import { AdminPageExtension } from '../../../../types/extension';
 import { HousekeepingPolicy, RangeOption } from '../../utils';
-import { AuditApp } from '../AuditApp';
+import AuditApp from '../AuditApp';
 import AuditAppRenderer from '../AuditAppRenderer';
 
+jest.mock('../../../../api/settings', () => ({
+  getValues: jest.fn().mockResolvedValue([])
+}));
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
 it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot();
 });
 
 it('should do nothing if governance is not available', async () => {
-  const fetchValues = jest.fn();
-  const wrapper = shallowRender({ fetchValues, adminPages: [] });
+  const wrapper = shallowRender({ adminPages: [] });
   await waitAndUpdate(wrapper);
 
   expect(wrapper.type()).toBeNull();
-  expect(fetchValues).not.toBeCalled();
+  expect(getValues).not.toBeCalled();
 });
 
-it('should fetch houskeeping policy on mount', async () => {
-  const fetchValues = jest.fn();
-  const wrapper = shallowRender({ fetchValues });
+it('should handle housekeeping policy', async () => {
+  (getValues as jest.Mock).mockResolvedValueOnce([{ value: HousekeepingPolicy.Weekly }]);
+
+  const wrapper = shallowRender();
+
   await waitAndUpdate(wrapper);
-  expect(fetchValues).toBeCalled();
+
+  expect(wrapper.find(AuditAppRenderer).props().housekeepingPolicy).toBe(HousekeepingPolicy.Weekly);
 });
 
 it('should handle date selection', () => {
@@ -76,11 +87,22 @@ it('should handle predefined selection', () => {
   expect(wrapper.state().dateRange).toBeUndefined();
 });
 
+it('should handle update to admin pages', async () => {
+  const wrapper = shallowRender({ adminPages: [] });
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper.type()).toBeNull();
+  expect(getValues).not.toBeCalled();
+
+  wrapper.setProps({ adminPages: [{ key: AdminPageExtension.GovernanceConsole, name: 'name' }] });
+  await waitAndUpdate(wrapper);
+
+  expect(getValues).toBeCalled();
+});
+
 function shallowRender(props: Partial<AuditApp['props']> = {}) {
   return shallow<AuditApp>(
     <AuditApp
-      auditHousekeepingPolicy={HousekeepingPolicy.Monthly}
-      fetchValues={jest.fn()}
       adminPages={[{ key: AdminPageExtension.GovernanceConsole, name: 'name' }]}
       {...props}
     />
index d1cc1e097c736914382fc161dd66b5105f46b911..de54ad439fb0a6e48d457adc667098b0c83e46dc 100644 (file)
@@ -24,7 +24,7 @@ import { ClearButton } from '../../../components/controls/buttons';
 import ConfirmButton from '../../../components/controls/ConfirmButton';
 import Tooltip from '../../../components/controls/Tooltip';
 import { translate } from '../../../helpers/l10n';
-import { AppState } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
 
 export interface Props {
   appState: AppState;
index 5bb5d8b601b175c0dc53dd6e53ae6fb71dcfebf1..3582400a715e3fb0d8a88997c1ea6a4a0e890a14 100644 (file)
@@ -21,7 +21,7 @@ import * as React from 'react';
 import { changePassword } from '../../api/users';
 import withAppStateContext from '../../app/components/app-state/withAppStateContext';
 import { Location, withRouter } from '../../components/hoc/withRouter';
-import { AppState } from '../../types/types';
+import { AppState } from '../../types/appstate';
 import ChangeAdminPasswordAppRenderer from './ChangeAdminPasswordAppRenderer';
 import { DEFAULT_ADMIN_LOGIN, DEFAULT_ADMIN_PASSWORD } from './constants';
 
index fa64d3cee56ebe7d4253999a2899824ea9cd41f3..5c282e3ee5ff1d751ee4f8ce2ed218d31f555039 100644 (file)
@@ -26,7 +26,8 @@ import DeferredSpinner from '../../../components/ui/DeferredSpinner';
 import { translate } from '../../../helpers/l10n';
 import { formatMeasure } from '../../../helpers/measures';
 import { getIssuesUrl } from '../../../helpers/urls';
-import { AppState, RuleDetails } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
+import { RuleDetails } from '../../../types/types';
 
 interface Props {
   appState: AppState;
index 3cd2701916e441d9bb3a547fdae0973b0fad1277..4471a7923b043488225d8d098c833ea5b7a9db66 100644 (file)
@@ -24,7 +24,7 @@ import ChevronsIcon from '../../../components/icons/ChevronsIcon';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { getBaseUrl } from '../../../helpers/system';
 import { AlmKeys } from '../../../types/alm-settings';
-import { AppState } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
 import { CreateProjectModes } from './types';
 
 export interface CreateProjectModeSelectionProps {
index 6f250980373e7b5c90f2b98f7bb397ec335966dd..4c4a3d7bbb6f02456aa2d798e6e38de22f96bb23 100644 (file)
@@ -27,7 +27,8 @@ import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
 import { translate } from '../../../helpers/l10n';
 import { getProjectUrl } from '../../../helpers/urls';
 import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
-import { AppState, LoggedInUser } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
+import { LoggedInUser } from '../../../types/types';
 import AlmBindingDefinitionForm from '../../settings/components/almIntegration/AlmBindingDefinitionForm';
 import AzureProjectCreate from './AzureProjectCreate';
 import BitbucketCloudProjectCreate from './BitbucketCloudProjectCreate';
index f5a71c751a2ed5c31637ce16ccc2c45682f0d546..073c704f38a0b5e361dd56d6a361ea60da825fdc 100644 (file)
@@ -65,7 +65,7 @@ exports[`should show warnning when not all projects are accessible 1`] = `
           displayReset={true}
           onReset={[Function]}
         />
-        <Connect(Sidebar)
+        <withAppStateContext(Sidebar)
           component={
             Object {
               "breadcrumbs": Array [],
index fd455980dc0c2acb6bac5fb4a098f375a839b41e..4a80bd6dd9631c0854848f6ab78e4e1eb677ac3e 100644 (file)
@@ -18,9 +18,9 @@
  * 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 { isBranch, isPullRequest } from '../../../helpers/branch-like';
-import { getGlobalSettingValue, Store } from '../../../store/rootReducer';
+import { AppState } from '../../../types/appstate';
 import { BranchLike } from '../../../types/branch-like';
 import { ComponentQualifier, isApplication, isPortfolioLike } from '../../../types/component';
 import {
@@ -29,6 +29,7 @@ import {
   ReferencedLanguage,
   ReferencedRule
 } from '../../../types/issues';
+import { GlobalSettingKeys } from '../../../types/settings';
 import { Component, Dict, UserBase } from '../../../types/types';
 import { Query } from '../utils';
 import AssigneeFacet from './AssigneeFacet';
@@ -48,6 +49,7 @@ import TagFacet from './TagFacet';
 import TypeFacet from './TypeFacet';
 
 export interface Props {
+  appState: AppState;
   branchLike?: BranchLike;
   component: Component | undefined;
   createdAfterIncludesTime: boolean;
@@ -64,7 +66,6 @@ export interface Props {
   referencedLanguages: Dict<ReferencedLanguage>;
   referencedRules: Dict<ReferencedRule>;
   referencedUsers: Dict<UserBase>;
-  disableDeveloperAggregatedInfo: boolean;
 }
 
 export class Sidebar extends React.PureComponent<Props> {
@@ -108,6 +109,7 @@ export class Sidebar extends React.PureComponent<Props> {
 
   render() {
     const {
+      appState: { settings },
       component,
       createdAfterIncludesTime,
       facets,
@@ -116,6 +118,9 @@ export class Sidebar extends React.PureComponent<Props> {
       branchLike
     } = this.props;
 
+    const disableDeveloperAggregatedInfo =
+      settings[GlobalSettingKeys.DeveloperAggregatedInfoDisabled] === 'true';
+
     const branch =
       (isBranch(branchLike) && branchLike.name) ||
       (isPullRequest(branchLike) && branchLike.branch) ||
@@ -255,7 +260,7 @@ export class Sidebar extends React.PureComponent<Props> {
           />
         )}
         {this.renderComponentFacets()}
-        {!this.props.myIssues && !this.props.disableDeveloperAggregatedInfo && (
+        {!this.props.myIssues && !disableDeveloperAggregatedInfo && (
           <AssigneeFacet
             assigned={query.assigned}
             assignees={query.assignees}
@@ -269,7 +274,7 @@ export class Sidebar extends React.PureComponent<Props> {
             stats={facets.assignees}
           />
         )}
-        {displayAuthorFacet && !this.props.disableDeveloperAggregatedInfo && (
+        {displayAuthorFacet && !disableDeveloperAggregatedInfo && (
           <AuthorFacet
             author={query.author}
             component={component}
@@ -287,14 +292,4 @@ export class Sidebar extends React.PureComponent<Props> {
   }
 }
 
-export const mapStateToProps = (state: Store) => {
-  const disableDeveloperAggregatedInfo = getGlobalSettingValue(
-    state,
-    'sonar.developerAggregatedInfo.disabled'
-  );
-  return {
-    disableDeveloperAggregatedInfo: disableDeveloperAggregatedInfo?.value === true.toString()
-  };
-};
-
-export default connect(mapStateToProps)(Sidebar);
+export default withAppStateContext(Sidebar);
index 8e7b29ca2af028c669106c0f853514100f2dc0eb..916e17efba173d09f6fd29b430b87034027cf5c2 100644 (file)
@@ -21,12 +21,11 @@ import { shallow, ShallowWrapper } from 'enzyme';
 import { flatten } from 'lodash';
 import * as React from 'react';
 import { mockComponent } from '../../../../helpers/mocks/component';
-import { getGlobalSettingValue } from '../../../../store/rootReducer';
+import { mockAppState } from '../../../../helpers/testMocks';
+import { GlobalSettingKeys } from '../../../../types/settings';
 import { ComponentQualifier } from '../../../../types/component';
 import { Query } from '../../utils';
-import { mapStateToProps, Sidebar } from '../Sidebar';
-
-jest.mock('../../../../store/rootReducer', () => ({ getGlobalSettingValue: jest.fn() }));
+import { Sidebar } from '../Sidebar';
 
 it('should render facets for global page', () => {
   expect(renderSidebar()).toMatchSnapshot();
@@ -52,16 +51,13 @@ it('should render facets when my issues are selected', () => {
 });
 
 it('should not render developer nominative facets when asked not to', () => {
-  expect(renderSidebar({ disableDeveloperAggregatedInfo: true })).toMatchSnapshot();
-});
-
-it('should init the component with the proper store value', () => {
-  mapStateToProps({} as any);
-
-  expect(getGlobalSettingValue).toHaveBeenCalledWith(
-    expect.any(Object),
-    'sonar.developerAggregatedInfo.disabled'
-  );
+  expect(
+    renderSidebar({
+      appState: mockAppState({
+        settings: { [GlobalSettingKeys.DeveloperAggregatedInfoDisabled]: 'true' }
+      })
+    })
+  ).toMatchSnapshot();
 });
 
 const renderSidebar = (props?: Partial<Sidebar['props']>) => {
@@ -69,6 +65,9 @@ const renderSidebar = (props?: Partial<Sidebar['props']>) => {
     mapChildren(
       shallow<Sidebar>(
         <Sidebar
+          appState={mockAppState({
+            settings: { [GlobalSettingKeys.DeveloperAggregatedInfoDisabled]: 'false' }
+          })}
           component={undefined}
           createdAfterIncludesTime={false}
           facets={{}}
@@ -84,7 +83,6 @@ const renderSidebar = (props?: Partial<Sidebar['props']>) => {
           referencedLanguages={{}}
           referencedRules={{}}
           referencedUsers={{}}
-          disableDeveloperAggregatedInfo={false}
           {...props}
         />
       )
index 6e5f27e4e47a21738aee5f57f7230a9476cb08d5..ee6aa25506d1f2907501a11e827f3a79137b8043 100644 (file)
@@ -41,7 +41,7 @@ exports[`should render 1`] = `
 
 exports[`test behavior should correctly render facet item 1`] = `
 <React.Fragment>
-  <Connect(Avatar)
+  <withAppStateContext(Avatar)
     className="little-spacer-right"
     name="Name Baz"
     size={16}
@@ -52,7 +52,7 @@ exports[`test behavior should correctly render facet item 1`] = `
 
 exports[`test behavior should correctly render facet item 2`] = `
 <React.Fragment>
-  <Connect(Avatar)
+  <withAppStateContext(Avatar)
     className="little-spacer-right"
     name="foo"
     size={16}
@@ -63,7 +63,7 @@ exports[`test behavior should correctly render facet item 2`] = `
 
 exports[`test behavior should correctly render search result correctly 1`] = `
 <React.Fragment>
-  <Connect(Avatar)
+  <withAppStateContext(Avatar)
     className="little-spacer-right"
     name="Name Bar"
     size={16}
@@ -80,7 +80,7 @@ exports[`test behavior should correctly render search result correctly 1`] = `
 
 exports[`test behavior should correctly render search result correctly 2`] = `
 <React.Fragment>
-  <Connect(Avatar)
+  <withAppStateContext(Avatar)
     className="little-spacer-right"
     name="foo"
     size={16}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx
deleted file mode 100644 (file)
index 501b4f9..0000000
+++ /dev/null
@@ -1,74 +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 AdminContext from '../../app/components/AdminContext';
-import withAppStateContext from '../../app/components/app-state/withAppStateContext';
-import { getGlobalSettingValue, Store } from '../../store/rootReducer';
-import { EditionKey } from '../../types/editions';
-import { AppState, RawQuery } from '../../types/types';
-import { fetchValues } from '../settings/store/actions';
-import App from './App';
-
-interface OwnProps {
-  location: { pathname: string; query: RawQuery };
-  appState: AppState;
-}
-
-interface StateToProps {
-  fetchValues: typeof fetchValues;
-  updateCenterActive: boolean;
-}
-
-function WithAdminContext(props: StateToProps & OwnProps) {
-  React.useEffect(() => {
-    props.fetchValues(['sonar.updatecenter.activate']);
-  });
-
-  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}
-          {...propsToPass}
-        />
-      )}
-    </AdminContext.Consumer>
-  );
-}
-
-const mapDispatchToProps = { fetchValues };
-
-const mapStateToProps = (state: Store) => {
-  const updateCenterActive = getGlobalSettingValue(state, 'sonar.updatecenter.activate');
-  return {
-    updateCenterActive: Boolean(updateCenterActive && updateCenterActive.value === 'true')
-  };
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(withAppStateContext(WithAdminContext));
diff --git a/server/sonar-web/src/main/js/apps/marketplace/MarketplaceAppContainer.tsx b/server/sonar-web/src/main/js/apps/marketplace/MarketplaceAppContainer.tsx
new file mode 100644 (file)
index 0000000..77050b3
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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 AdminContext from '../../app/components/AdminContext';
+import withAppStateContext from '../../app/components/app-state/withAppStateContext';
+import { AppState } from '../../types/appstate';
+import { EditionKey } from '../../types/editions';
+import { GlobalSettingKeys } from '../../types/settings';
+import { RawQuery } from '../../types/types';
+import App from './App';
+
+export interface MarketplaceAppContainerProps {
+  location: { pathname: string; query: RawQuery };
+  appState: AppState;
+}
+
+export function MarketplaceAppContainer(props: MarketplaceAppContainerProps) {
+  const { appState, location } = props;
+
+  const propsToPass = {
+    location,
+    updateCenterActive: appState.settings[GlobalSettingKeys.UpdatecenterActivated] === 'true',
+    currentEdition: appState.edition as EditionKey,
+    standaloneMode: appState.standalone
+  };
+
+  return (
+    <AdminContext.Consumer>
+      {({ fetchPendingPlugins, pendingPlugins }) => (
+        <App
+          fetchPendingPlugins={fetchPendingPlugins}
+          pendingPlugins={pendingPlugins}
+          {...propsToPass}
+        />
+      )}
+    </AdminContext.Consumer>
+  );
+}
+
+export default withAppStateContext(MarketplaceAppContainer);
diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx
deleted file mode 100644 (file)
index d306400..0000000
+++ /dev/null
@@ -1,52 +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 { mockStore } from '../../../helpers/testMocks';
-import { getGlobalSettingValue } from '../../../store/rootReducer';
-import '../AppContainer';
-
-jest.mock('react-redux', () => ({
-  connect: jest.fn(() => (a: any) => a)
-}));
-
-jest.mock('../../../store/rootReducer', () => {
-  return {
-    getGlobalSettingValue: jest.fn()
-  };
-});
-
-describe('redux', () => {
-  it('should correctly map state and dispatch props', () => {
-    const store = mockStore();
-    const updateCenterActive = true;
-    (getGlobalSettingValue as jest.Mock).mockReturnValueOnce({
-      value: `${updateCenterActive}`
-    });
-
-    const [mapStateToProps] = (connect as jest.Mock).mock.calls[0];
-
-    const props = mapStateToProps(store);
-    expect(props).toEqual({
-      updateCenterActive
-    });
-
-    expect(getGlobalSettingValue).toHaveBeenCalledWith(store, 'sonar.updatecenter.activate');
-  });
-});
diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/MarketplaceAppContainer-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/__tests__/MarketplaceAppContainer-test.tsx
new file mode 100644 (file)
index 0000000..476b403
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * 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 React from 'react';
+import { mockAppState, mockLocation } from '../../../helpers/testMocks';
+import { GlobalSettingKeys } from '../../../types/settings';
+import { EditionKey } from '../../../types/editions';
+import { MarketplaceAppContainer, MarketplaceAppContainerProps } from '../MarketplaceAppContainer';
+
+it('should render correctly', () => {
+  expect(shallowRender().dive()).toMatchSnapshot('default');
+  expect(
+    shallowRender({
+      appState: mockAppState({
+        settings: {
+          [GlobalSettingKeys.UpdatecenterActivated]: 'true'
+        }
+      })
+    }).dive()
+  ).toMatchSnapshot('update center active');
+});
+
+function shallowRender(overrides: Partial<MarketplaceAppContainerProps> = {}) {
+  return shallow<MarketplaceAppContainerProps>(
+    <MarketplaceAppContainer
+      appState={mockAppState({ edition: EditionKey.community, standalone: true })}
+      location={mockLocation()}
+      {...overrides}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/MarketplaceAppContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/MarketplaceAppContainer-test.tsx.snap
new file mode 100644 (file)
index 0000000..58d697a
--- /dev/null
@@ -0,0 +1,54 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<withRouter(App)
+  currentEdition="community"
+  fetchPendingPlugins={[Function]}
+  location={
+    Object {
+      "action": "PUSH",
+      "hash": "",
+      "key": "key",
+      "pathname": "/path",
+      "query": Object {},
+      "search": "",
+      "state": Object {},
+    }
+  }
+  pendingPlugins={
+    Object {
+      "installing": Array [],
+      "removing": Array [],
+      "updating": Array [],
+    }
+  }
+  standaloneMode={true}
+  updateCenterActive={false}
+/>
+`;
+
+exports[`should render correctly: update center active 1`] = `
+<withRouter(App)
+  currentEdition="community"
+  fetchPendingPlugins={[Function]}
+  location={
+    Object {
+      "action": "PUSH",
+      "hash": "",
+      "key": "key",
+      "pathname": "/path",
+      "query": Object {},
+      "search": "",
+      "state": Object {},
+    }
+  }
+  pendingPlugins={
+    Object {
+      "installing": Array [],
+      "removing": Array [],
+      "updating": Array [],
+    }
+  }
+  updateCenterActive={true}
+/>
+`;
index c4d59b1b922e08b016fa4382bc782991e4a2af89..211f08c5a0605c63ed705d745c71417554259d01 100644 (file)
@@ -21,7 +21,7 @@ import { lazyLoadComponent } from '../../components/lazyLoadComponent';
 
 const routes = [
   {
-    indexRoute: { component: lazyLoadComponent(() => import('./AppContainer')) }
+    indexRoute: { component: lazyLoadComponent(() => import('./MarketplaceAppContainer')) }
   }
 ];
 
index 665b63e6ddc7f2169ac84188ba80c58592f0da60..0477fff99e9ce3d699693d1af0ba77dcbb8da2fd 100644 (file)
@@ -24,9 +24,10 @@ import { Router, withRouter } from '../../../components/hoc/withRouter';
 import { lazyLoadComponent } from '../../../components/lazyLoadComponent';
 import { isPullRequest } from '../../../helpers/branch-like';
 import { ProjectAlmBindingResponse } from '../../../types/alm-settings';
+import { AppState } from '../../../types/appstate';
 import { BranchLike } from '../../../types/branch-like';
 import { isPortfolioLike } from '../../../types/component';
-import { AppState, Component } from '../../../types/types';
+import { Component } from '../../../types/types';
 import BranchOverview from '../branches/BranchOverview';
 
 const EmptyOverview = lazyLoadComponent(() => import('./EmptyOverview'));
index ec83f1ab9783d38b6eaa6bc17581551e8719f481..2eee2295c08b304e545d66b36790896a517933b5 100644 (file)
@@ -19,7 +19,8 @@
  */
 import * as React from 'react';
 import Tooltip from '../../../components/controls/Tooltip';
-import { getLeakValue, getRatingTooltip } from '../../../components/measure/utils';
+import RatingTooltipContent from '../../../components/measure/RatingTooltipContent';
+import { getLeakValue } from '../../../components/measure/utils';
 import DrilldownLink from '../../../components/shared/DrilldownLink';
 import Rating from '../../../components/ui/Rating';
 import { findMeasure } from '../../../helpers/measures';
@@ -50,10 +51,9 @@ function renderRatingLink(props: IssueRatingProps) {
   }
 
   const value = measure && (useDiffMetric ? getLeakValue(measure) : measure.value);
-  const tooltip = value && getRatingTooltip(rating, Number(value));
 
   return (
-    <Tooltip overlay={tooltip}>
+    <Tooltip overlay={value && <RatingTooltipContent metricKey={rating} value={value} />}>
       <span>
         <DrilldownLink
           branchLike={branchLike}
index f8f24334651152e209c55661ca3c43ad595fe088..6bc60fbd24e08a881dc8ec66445d93fa43dce9b8 100644 (file)
@@ -8,7 +8,12 @@ exports[`should render correctly for bugs 1`] = `
     metric_domain.Reliability
   </span>
   <Tooltip
-    overlay="metric.reliability_rating.tooltip.A"
+    overlay={
+      <withAppStateContext(RatingTooltipContent)
+        metricKey="reliability_rating"
+        value="1.0"
+      />
+    }
   >
     <span>
       <DrilldownLink
@@ -43,7 +48,12 @@ exports[`should render correctly for bugs 2`] = `
     metric_domain.Reliability
   </span>
   <Tooltip
-    overlay="metric.reliability_rating.tooltip.A"
+    overlay={
+      <withAppStateContext(RatingTooltipContent)
+        metricKey="new_reliability_rating"
+        value="1.0"
+      />
+    }
   >
     <span>
       <DrilldownLink
@@ -78,7 +88,12 @@ exports[`should render correctly for code smells 1`] = `
     metric_domain.Maintainability
   </span>
   <Tooltip
-    overlay="metric.sqale_rating.tooltip.A.0.0%"
+    overlay={
+      <withAppStateContext(RatingTooltipContent)
+        metricKey="sqale_rating"
+        value="1.0"
+      />
+    }
   >
     <span>
       <DrilldownLink
@@ -113,7 +128,12 @@ exports[`should render correctly for code smells 2`] = `
     metric_domain.Maintainability
   </span>
   <Tooltip
-    overlay="metric.sqale_rating.tooltip.A.0.0%"
+    overlay={
+      <withAppStateContext(RatingTooltipContent)
+        metricKey="new_maintainability_rating"
+        value="1.0"
+      />
+    }
   >
     <span>
       <DrilldownLink
@@ -148,7 +168,12 @@ exports[`should render correctly for vulnerabilities 1`] = `
     metric_domain.Security
   </span>
   <Tooltip
-    overlay="metric.security_rating.tooltip.A"
+    overlay={
+      <withAppStateContext(RatingTooltipContent)
+        metricKey="security_rating"
+        value="1.0"
+      />
+    }
   >
     <span>
       <DrilldownLink
@@ -183,7 +208,12 @@ exports[`should render correctly for vulnerabilities 2`] = `
     metric_domain.Security
   </span>
   <Tooltip
-    overlay="metric.security_rating.tooltip.A"
+    overlay={
+      <withAppStateContext(RatingTooltipContent)
+        metricKey="new_security_rating"
+        value="1.0"
+      />
+    }
   >
     <span>
       <DrilldownLink
index f5fea8b899a5fc5dcf0a2f451df29b6e31ad049e..178579337eb6f59cdd239e49933091a5c9be7355 100644 (file)
@@ -24,7 +24,8 @@ 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 { AppState, Permission, PermissionTemplate } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
+import { Permission, PermissionTemplate } from '../../../types/types';
 import '../../permissions/styles.css';
 import { mergeDefaultsToTemplates, mergePermissionsToTemplates, sortPermissions } from '../utils';
 import Home from './Home';
index 0f2d5f9874819a18887000a5cd9cb01505894325..8c4a4eb19f07a79ef501bb726faacf04873bbd66 100644 (file)
@@ -20,8 +20,9 @@
 import * as React from 'react';
 import withAppStateContext from '../../../../app/components/app-state/withAppStateContext';
 import ListFooter from '../../../../components/controls/ListFooter';
+import { AppState } from '../../../../types/appstate';
 import { ComponentQualifier } from '../../../../types/component';
-import { AppState, Paging, PermissionGroup, PermissionUser } from '../../../../types/types';
+import { Paging, PermissionGroup, PermissionUser } from '../../../../types/types';
 import HoldersList from '../../shared/components/HoldersList';
 import SearchForm from '../../shared/components/SearchForm';
 import {
index a132c867d355c3273f8def26e22bfe043a40a812..10307a9b05381b294a1ddcdcabdf4daf2c88b0f0 100644 (file)
@@ -90,7 +90,7 @@ exports[`should render correctly: default 1`] = `
     <div
       className="display-flex-center"
     >
-      <Connect(Avatar)
+      <withAppStateContext(Avatar)
         className="text-middle big-spacer-right flex-0"
         name="John Doe"
         size={36}
index d515f2294e7fdd1289519f38f7751208978cb847..4c123c72f03d6dfeb36a5ae08653cf5541a94f4d 100644 (file)
@@ -27,9 +27,9 @@ import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
 import { isBranch, sortBranches } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
+import { AppState } from '../../../types/appstate';
 import { Branch, BranchLike } from '../../../types/branch-like';
 import {
-  AppState,
   Component,
   NewCodePeriod,
   NewCodePeriodSettingType,
index 3e855acdfa6657623b82ada221443936fa53d23d..aabaf35c73067ab8af95cc9237e13afb53ef4a21 100644 (file)
@@ -20,8 +20,8 @@
 import * as React from 'react';
 import { getValues } from '../../../api/settings';
 import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
+import { AppState } from '../../../types/appstate';
 import { SettingsKey } from '../../../types/settings';
-import { AppState } from '../../../types/types';
 import LifetimeInformationRenderer from './LifetimeInformationRenderer';
 
 interface Props {
index cd067423adfac2bb012ac15a81e900a69013cf2b..29043ef9a850c5ff0625e725c71f10410f32b54a 100644 (file)
@@ -23,9 +23,10 @@ import { getStatus } from '../../api/project-dump';
 import withAppStateContext from '../../app/components/app-state/withAppStateContext';
 import throwGlobalError from '../../app/utils/throwGlobalError';
 import { translate } from '../../helpers/l10n';
+import { AppState } from '../../types/appstate';
 import { DumpStatus, DumpTask } from '../../types/project-dump';
 import { TaskStatuses, TaskTypes } from '../../types/tasks';
-import { AppState, Component } from '../../types/types';
+import { Component } from '../../types/types';
 import Export from './components/Export';
 import Import from './components/Import';
 import './styles.css';
index 38fa2a56eaf4b3ab8e28da72c141d72d28507ca2..ed14cc84902e043afa6fa5424bf9b7ce7b8d2683 100644 (file)
@@ -33,8 +33,9 @@ import { translate } from '../../../helpers/l10n';
 import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages';
 import { get, save } from '../../../helpers/storage';
 import { isLoggedIn } from '../../../helpers/users';
+import { AppState } from '../../../types/appstate';
 import { ComponentQualifier } from '../../../types/component';
-import { AppState, CurrentUser, RawQuery } from '../../../types/types';
+import { CurrentUser, RawQuery } from '../../../types/types';
 import { hasFilterParams, hasViewParams, parseUrlQuery, Query } from '../query';
 import '../styles.css';
 import { Facets, Project } from '../types';
index 4fe00dc27852d58bbc4cd5cd4ab3a2c23a153680..dac6645952cad6f1bad8e0a574fac7a92b33ecfe 100644 (file)
@@ -27,9 +27,10 @@ import { Router, withRouter } from '../../../components/hoc/withRouter';
 import { translate } from '../../../helpers/l10n';
 import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../helpers/urls';
 import { hasGlobalPermission } from '../../../helpers/users';
+import { AppState } from '../../../types/appstate';
 import { ComponentQualifier } from '../../../types/component';
 import { Permissions } from '../../../types/permissions';
-import { AppState, LoggedInUser } from '../../../types/types';
+import { LoggedInUser } from '../../../types/types';
 
 export interface ApplicationCreationProps {
   appState: AppState;
index 4d19a37e27a54160f1dcf0d46c699631389ac615..81640c8c16491877206052cfad542fd07442d4b6 100644 (file)
@@ -29,7 +29,8 @@ import SearchBox from '../../components/controls/SearchBox';
 import SelectLegacy from '../../components/controls/SelectLegacy';
 import QualifierIcon from '../../components/icons/QualifierIcon';
 import { translate } from '../../helpers/l10n';
-import { AppState, Visibility } from '../../types/types';
+import { AppState } from '../../types/appstate';
+import { Visibility } from '../../types/types';
 import BulkApplyTemplateModal from './BulkApplyTemplateModal';
 import DeleteModal from './DeleteModal';
 
index ef92de4334b0c5343a48acc72eaf04050cfc894d..14f98297bf7b5b4e4b82d96340ea2ca4d16235e2 100644 (file)
@@ -27,14 +27,9 @@ import ModalButton from '../../../components/controls/ModalButton';
 import { Alert } from '../../../components/ui/Alert';
 import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
 import { isDiffMetric } from '../../../helpers/measures';
+import { AppState } from '../../../types/appstate';
 import { MetricKey } from '../../../types/metrics';
-import {
-  AppState,
-  Condition as ConditionType,
-  Dict,
-  Metric,
-  QualityGate
-} from '../../../types/types';
+import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types';
 import Condition from './Condition';
 import ConditionModal from './ConditionModal';
 
index 5ddfe0ab0bcd5aa600c9368d4a59dfdb049848a9..096ddf281a6819e9972efa93372c41342d7d0292 100644 (file)
@@ -25,7 +25,7 @@ exports[`should render correctly: user 1`] = `
 <div
   className="display-flex-center permission-list-item padded"
 >
-  <Connect(Avatar)
+  <withAppStateContext(Avatar)
     className="spacer-right"
     name="John Doe"
     size={32}
index d479967b848b85a934886a68a38ed2d332883d06..5994dbc36f508faa429df5312f5a1cb5f9246580 100644 (file)
@@ -334,7 +334,7 @@ exports[`should render options correctly: group 1`] = `
 
 exports[`should render options correctly: user 1`] = `
 <React.Fragment>
-  <Connect(Avatar)
+  <withAppStateContext(Avatar)
     hash="A"
     name="name"
     size={16}
index 305d0624f078177cfc810736b36cd00af7b6bdd3..fc1970f5c723fe3e6be8e28a027a1e14b3f27d90 100644 (file)
@@ -8,7 +8,7 @@ exports[`renders 1`] = `
     className="pull-right spacer-top spacer-left spacer-right button-small"
     onClick={[Function]}
   />
-  <Connect(Avatar)
+  <withAppStateContext(Avatar)
     className="pull-left spacer-right"
     name="Luke Skywalker"
     size={32}
index 2475a3723b9f61bd1cb832f3229d902b523c386c..125a1fba9b47d2eac419dc8eb2dc175e2a403536 100644 (file)
@@ -10,7 +10,7 @@ exports[`should render correctly: default 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="Luke Skywalker"
           size={20}
@@ -49,7 +49,7 @@ exports[`should render correctly: default 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="Luke Skywalker"
           size={20}
@@ -88,7 +88,7 @@ exports[`should render correctly: default 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="John Doe"
           size={20}
@@ -130,7 +130,7 @@ exports[`should render correctly: default 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="John Doe"
           size={20}
@@ -172,7 +172,7 @@ exports[`should render correctly: default 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="John Doe"
           size={20}
@@ -255,7 +255,7 @@ exports[`should render correctly: show full list 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="Luke Skywalker"
           size={20}
@@ -294,7 +294,7 @@ exports[`should render correctly: show full list 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="Luke Skywalker"
           size={20}
@@ -333,7 +333,7 @@ exports[`should render correctly: show full list 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="John Doe"
           size={20}
@@ -375,7 +375,7 @@ exports[`should render correctly: show full list 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="John Doe"
           size={20}
@@ -417,7 +417,7 @@ exports[`should render correctly: show full list 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="John Doe"
           size={20}
@@ -459,7 +459,7 @@ exports[`should render correctly: show full list 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="john.doe"
           size={20}
@@ -501,7 +501,7 @@ exports[`should render correctly: show full list 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="John Doe"
           size={20}
@@ -592,7 +592,7 @@ exports[`should render correctly: show full list 1`] = `
       <div
         className="display-flex-center"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           name="John Doe"
           size={20}
index 087a963a423c04fa41090e9d5b8538ad02054514..7d05daab361a97df0a77b02033473e64461abbb4 100644 (file)
@@ -71,7 +71,7 @@ exports[`should render correctly: open with results 1`] = `
         key="john.doe"
         onClick={[Function]}
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="spacer-right"
           name="John Doe"
           size={16}
@@ -83,7 +83,7 @@ exports[`should render correctly: open with results 1`] = `
         key="highlighted"
         onClick={[Function]}
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="spacer-right"
           name="John Doe"
           size={16}
index 69722c98a05db6c193f7084600ce50dbdf5d61ff..bfc388ce5b2667dbac3eb9ff573e54033550647e 100644 (file)
@@ -23,7 +23,8 @@ import * as React from 'react';
 import { IndexLink } from 'react-router';
 import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
 import { getGlobalSettingsUrl, getProjectSettingsUrl } from '../../../helpers/urls';
-import { AppState, Component } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
+import { Component } from '../../../types/types';
 import { getCategoryName } from '../utils';
 import { ADDITIONAL_CATEGORIES } from './AdditionalCategories';
 import CATEGORY_OVERRIDES from './CategoryOverrides';
index 7a54c0bc12b1a4f0eab6db510f5121c61348825f..dd57a2c74649bc85805b951b3cc523a207113f80 100644 (file)
@@ -34,8 +34,9 @@ import {
   AlmSettingsBindingStatus,
   AlmSettingsBindingStatusType
 } from '../../../../types/alm-settings';
+import { AppState } from '../../../../types/appstate';
 import { ExtendedSettingDefinition } from '../../../../types/settings';
-import { AppState, Dict } from '../../../../types/types';
+import { Dict } from '../../../../types/types';
 import AlmIntegrationRenderer from './AlmIntegrationRenderer';
 
 interface Props extends Pick<WithRouterProps, 'location' | 'router'> {
index 9f53a41396f788f0cf86861ab14761f0810b145a..eaca7a893d252778420756be7d3931850a36178c 100644 (file)
@@ -24,8 +24,8 @@ import Tooltip from '../../../../components/controls/Tooltip';
 import { getEdition, getEditionUrl } from '../../../../helpers/editions';
 import { translate } from '../../../../helpers/l10n';
 import { AlmKeys } from '../../../../types/alm-settings';
+import { AppState } from '../../../../types/appstate';
 import { EditionKey } from '../../../../types/editions';
-import { AppState } from '../../../../types/types';
 
 export interface CreationTooltipProps {
   alm: AlmKeys;
index cd60e263b9d95cc0db7fa68539c52ed2e0575bd2..2da3d4765a791b869ac4d6e187ed931291722089 100644 (file)
@@ -32,8 +32,9 @@ import {
   AlmSettingsInstance,
   ProjectAlmBindingResponse
 } from '../../../../types/alm-settings';
+import { AppState } from '../../../../types/appstate';
 import { EditionKey } from '../../../../types/editions';
-import { AppState, Dict } from '../../../../types/types';
+import { Dict } from '../../../../types/types';
 
 export interface AlmSpecificFormProps {
   alm: AlmKeys;
diff --git a/server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts b/server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts
deleted file mode 100644 (file)
index 5fc7dd8..0000000
+++ /dev/null
@@ -1,55 +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 { fetchValues, setValues } from '../actions';
-
-jest.mock('../../../../api/settings', () => {
-  const { mockSettingValue } = jest.requireActual('../../../../helpers/mocks/settings');
-  return {
-    getValues: jest.fn().mockResolvedValue([mockSettingValue()])
-  };
-});
-
-it('should setValues correctly', () => {
-  const dispatch = jest.fn();
-  setValues(['test'], [{ key: 'test', value: 'foo' }])(dispatch);
-  expect(dispatch).toHaveBeenCalledWith({
-    component: undefined,
-    settings: [
-      {
-        key: 'test',
-        value: 'foo'
-      }
-    ],
-    type: 'RECEIVE_VALUES',
-    updateKeys: ['test']
-  });
-});
-
-it('should fetchValue correclty', async () => {
-  const dispatch = jest.fn();
-  await fetchValues(['test'], 'foo')(dispatch);
-  expect(dispatch).toHaveBeenCalledWith({
-    component: 'foo',
-    settings: [{ key: 'test' }],
-    type: 'RECEIVE_VALUES',
-    updateKeys: ['test']
-  });
-  expect(dispatch).toHaveBeenCalledWith({ type: 'CLOSE_ALL_GLOBAL_MESSAGES' });
-});
diff --git a/server/sonar-web/src/main/js/apps/settings/store/actions.ts b/server/sonar-web/src/main/js/apps/settings/store/actions.ts
deleted file mode 100644 (file)
index 2c5430e..0000000
+++ /dev/null
@@ -1,39 +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 { Dispatch } from 'redux';
-import { getValues } from '../../../api/settings';
-import { closeAllGlobalMessages } from '../../../store/globalMessages';
-import { receiveValues } from './values';
-
-export function fetchValues(keys: string[], component?: string) {
-  return (dispatch: Dispatch) =>
-    getValues({ keys: keys.join(), component }).then(settings => {
-      dispatch(receiveValues(keys, settings, component));
-      dispatch(closeAllGlobalMessages());
-    });
-}
-
-export function setValues(
-  keys: string[],
-  settings: Array<{ key: string; value?: string }>,
-  component?: string
-) {
-  return (dispatch: Dispatch) => dispatch(receiveValues(keys, settings, component));
-}
diff --git a/server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts b/server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts
deleted file mode 100644 (file)
index 4bc4663..0000000
+++ /dev/null
@@ -1,31 +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 { combineReducers } from 'redux';
-import values, * as fromValues from './values';
-
-interface State {
-  values: fromValues.State;
-}
-
-export default combineReducers({ values });
-
-export function getValue(state: State, key: string, component?: string) {
-  return fromValues.getValue(state.values, key, component);
-}
diff --git a/server/sonar-web/src/main/js/apps/settings/store/values.ts b/server/sonar-web/src/main/js/apps/settings/store/values.ts
deleted file mode 100644 (file)
index 478a873..0000000
+++ /dev/null
@@ -1,78 +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 { keyBy, omit } from 'lodash';
-import { combineReducers } from 'redux';
-import { ActionType } from '../../../store/utils/actions';
-import { SettingValue } from '../../../types/settings';
-import { Dict } from '../../../types/types';
-
-enum Actions {
-  receiveValues = 'RECEIVE_VALUES'
-}
-
-type Action = ActionType<typeof receiveValues, Actions.receiveValues>;
-
-type SettingsState = Dict<SettingValue>;
-
-export interface State {
-  components: Dict<SettingsState>;
-  global: SettingsState;
-}
-
-export function receiveValues(
-  updateKeys: string[],
-  settings: Array<{ key: string; value?: string }>,
-  component?: string
-) {
-  return { type: Actions.receiveValues, updateKeys, settings, component };
-}
-
-function components(state: State['components'] = {}, action: Action) {
-  const { component: key } = action;
-  if (!key) {
-    return state;
-  }
-  if (action.type === Actions.receiveValues) {
-    const settingsByKey = keyBy(action.settings, 'key');
-    return { ...state, [key]: { ...omit(state[key] || {}, action.updateKeys), ...settingsByKey } };
-  }
-  return state;
-}
-
-function global(state: State['components'] = {}, action: Action) {
-  if (action.type === Actions.receiveValues) {
-    if (action.component) {
-      return state;
-    }
-    const settingsByKey = keyBy(action.settings, 'key');
-    return { ...omit(state, action.updateKeys), ...settingsByKey };
-  }
-
-  return state;
-}
-
-export default combineReducers({ components, global });
-
-export function getValue(state: State, key: string, component?: string): SettingValue | undefined {
-  if (component) {
-    return state.components[component] && state.components[component][key];
-  }
-  return state.global[key];
-}
index 486c80de4c3c43a44ac90dc2a6a29fd51a690a58..b36b03b3c9fef9fd617fa53155784132e97b7813 100644 (file)
@@ -23,7 +23,7 @@ import { ClipboardButton } from '../../../components/controls/clipboard';
 import { Alert } from '../../../components/ui/Alert';
 import { toShortNotSoISOString } from '../../../helpers/dates';
 import { translate } from '../../../helpers/l10n';
-import { AppState } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
 import PageActions from './PageActions';
 
 export interface Props {
index 5e34a0c7b6177e2d1eb3e1aa21add8f9f775a33b..9d62cbde539cb6352b36286003a38e993885d16b 100644 (file)
@@ -5,7 +5,7 @@ exports[`should render correctly 1`] = `
   <td
     className="thin nowrap text-middle"
   >
-    <Connect(Avatar)
+    <withAppStateContext(Avatar)
       name="One"
       size={36}
     />
@@ -92,7 +92,7 @@ exports[`should render correctly without last connection date 1`] = `
   <td
     className="thin nowrap text-middle"
   >
-    <Connect(Avatar)
+    <withAppStateContext(Avatar)
       name="One"
       size={36}
     />
index c3e1c99564e8d4c20eaf34b1a53bcd3bac771270..0d6493812cf117f246d571851939d7c9c7790682 100644 (file)
@@ -88,7 +88,7 @@ exports[`UsersSelectSearchOption should render correctly with email instead of h
   role="listitem"
   title="Administrator"
 >
-  <Connect(Avatar)
+  <withAppStateContext(Avatar)
     name="Administrator"
     size={16}
   />
@@ -113,7 +113,7 @@ exports[`UsersSelectSearchOption should render correctly without all parameters
   role="listitem"
   title="Administrator"
 >
-  <Connect(Avatar)
+  <withAppStateContext(Avatar)
     hash="7daf6c79d4802916d83f6266e24850af"
     name="Administrator"
     size={16}
@@ -139,7 +139,7 @@ exports[`UsersSelectSearchValue should render correctly with a user 1`] = `
   <div
     className="Select-value-label"
   >
-    <Connect(Avatar)
+    <withAppStateContext(Avatar)
       hash="7daf6c79d4802916d83f6266e24850af"
       name="Administrator"
       size={16}
@@ -166,7 +166,7 @@ exports[`UsersSelectSearchValue should render correctly with email instead of ha
   <div
     className="Select-value-label"
   >
-    <Connect(Avatar)
+    <withAppStateContext(Avatar)
       name="Administrator"
       size={16}
     />
index d4403291c370290dc39e8f42d1752734140fba6f..88c25f5d75d8daf4b9e70cda7cf4aae99653c690 100644 (file)
@@ -27,10 +27,11 @@ import withAppStateContext from '../../app/components/app-state/withAppStateCont
 import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { isLoggedIn } from '../../helpers/users';
+import { AppState } from '../../types/appstate';
 import { Branch } from '../../types/branch-like';
 import { ComponentQualifier } from '../../types/component';
 import { ComponentReportStatus } from '../../types/component-report';
-import { AppState, Component, CurrentUser } from '../../types/types';
+import { Component, CurrentUser } from '../../types/types';
 import { withCurrentUser } from '../hoc/withCurrentUser';
 import ComponentReportActionsRenderer from './ComponentReportActionsRenderer';
 
index 1263177bf8f6a4b8e667bb9b6e69645da589c13c..77e6cbd833c25d36c8a8d66a0c255a5e3276a051 100644 (file)
@@ -22,7 +22,7 @@ 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 { AppState } from '../../types/appstate';
 
 interface OwnProps {
   appState: AppState;
index 50e8aacb1583263c8664eab12a3ec54c886f6f86..69b5f81e7f5792887a09f086fd9d53e7b0d9d3c5 100644 (file)
@@ -32,7 +32,7 @@ exports[`should open the popup when the button is clicked 2`] = `
       <span
         className="text-top"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           hash="gravatarhash"
           name=""
@@ -108,7 +108,7 @@ exports[`should render with the action 1`] = `
       <span
         className="text-top"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="little-spacer-right"
           hash="gravatarhash"
           name=""
@@ -133,7 +133,7 @@ exports[`should render without the action when the correct rights are missing 1`
   <span
     className="text-top"
   >
-    <Connect(Avatar)
+    <withAppStateContext(Avatar)
       className="little-spacer-right"
       hash="gravatarhash"
       name=""
index 1a6849c827e48f6b2448e239f600449f2dcb7def..a6864ce05b580c0a7c2de025436ab054e19b9c41 100644 (file)
@@ -20,7 +20,7 @@ exports[`should open the right popups when the buttons are clicked 3`] = `
     className="issue-comment-author"
     title="John Doe"
   >
-    <Connect(Avatar)
+    <withAppStateContext(Avatar)
       className="little-spacer-right"
       hash="gravatarhash"
       name="John Doe"
@@ -118,7 +118,7 @@ exports[`should render correctly a comment that is not updatable 1`] = `
     className="issue-comment-author"
     title="John Doe"
   >
-    <Connect(Avatar)
+    <withAppStateContext(Avatar)
       className="little-spacer-right"
       hash="gravatarhash"
       name="John Doe"
@@ -160,7 +160,7 @@ exports[`should render correctly a comment that is updatable 1`] = `
     className="issue-comment-author"
     title="John Doe"
   >
-    <Connect(Avatar)
+    <withAppStateContext(Avatar)
       className="little-spacer-right"
       hash="gravatarhash"
       name="John Doe"
@@ -255,7 +255,7 @@ exports[`should render correctly a comment with a deleted author 1`] = `
   className="issue-comment-author"
   title="user.x_deleted.john.doe"
 >
-  <Connect(Avatar)
+  <withAppStateContext(Avatar)
     className="little-spacer-right"
     hash="gravatarhash"
     name="john.doe"
index 2bf516e280b4f72a03427ef2ec4c8228ef5b360d..1de31a413cc86c102be8d5815b454f39f35c8387 100644 (file)
@@ -39,7 +39,7 @@ exports[`should render the changelog popup correctly 1`] = `
             className="text-left text-top"
           >
             <p>
-              <Connect(Avatar)
+              <withAppStateContext(Avatar)
                 className="little-spacer-right"
                 hash="gravatarhash"
                 name="John Doe"
@@ -104,7 +104,7 @@ exports[`should render the changelog popup when we have a deleted user 1`] = `
             className="text-left text-top"
           >
             <p>
-              <Connect(Avatar)
+              <withAppStateContext(Avatar)
                 className="little-spacer-right"
                 name="john.doe"
                 size={16}
index b539ed1acf08b811c51cefefdd46536b0e6ac3da..88c0d0ba1cfe5b5dad311c2a323a534fbe044e02 100644 (file)
@@ -33,7 +33,7 @@ exports[`should render correctly 1`] = `
         item="luke"
         key="luke"
       >
-        <Connect(Avatar)
+        <withAppStateContext(Avatar)
           className="spacer-right"
           name="Skywalker"
           size={16}
index 595727bceb6d0abdda207267ee98b8eb9ed9daf1..aabffd0885eba1e67fb3386c45c3b4e8885e6901 100644 (file)
@@ -127,7 +127,7 @@ exports[`should render correctly when assigned 1`] = `
 >
   <span>
     assigned_to
-    <Connect(Avatar)
+    <withAppStateContext(Avatar)
       className="little-spacer-left little-spacer-right"
       name="Luke Skywalker"
       size={16}
@@ -143,7 +143,7 @@ exports[`should render correctly when assigned 2`] = `
 >
   <span>
     assigned_to
-    <Connect(Avatar)
+    <withAppStateContext(Avatar)
       className="little-spacer-left little-spacer-right"
       name="luke"
       size={16}
index 9ed89f92189e56b400027facf04e9372499d8261..8abc4e62781f7c474cb0c3422bdfbfd5e9c887a1 100644 (file)
@@ -22,7 +22,7 @@ import Tooltip from '../../components/controls/Tooltip';
 import Level from '../../components/ui/Level';
 import Rating from '../../components/ui/Rating';
 import { formatMeasure } from '../../helpers/measures';
-import { getRatingTooltip } from './utils';
+import RatingTooltipContent from './RatingTooltipContent';
 
 interface Props {
   className?: string;
@@ -57,8 +57,9 @@ export default function Measure({
     return <span className={className}>{formattedValue != null ? formattedValue : '–'}</span>;
   }
 
-  const tooltip = getRatingTooltip(metricKey, Number(value));
+  const tooltip = <RatingTooltipContent metricKey={metricKey} value={value} />;
   const rating = <Rating small={small} value={value} />;
+
   if (tooltip) {
     return (
       <Tooltip overlay={tooltip}>
diff --git a/server/sonar-web/src/main/js/components/measure/RatingTooltipContent.tsx b/server/sonar-web/src/main/js/components/measure/RatingTooltipContent.tsx
new file mode 100644 (file)
index 0000000..855449a
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * 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 withAppStateContext from '../../app/components/app-state/withAppStateContext';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { formatMeasure, isDiffMetric } from '../../helpers/measures';
+import { AppState } from '../../types/appstate';
+import { MetricKey } from '../../types/metrics';
+import { GlobalSettingKeys } from '../../types/settings';
+import { KNOWN_RATINGS } from './utils';
+
+const RATING_GRID_SIZE = 4;
+const DIFF_METRIC_PREFIX_LENGTH = 4;
+const PERCENT_MULTIPLIER = 100;
+const GRID_INDEX_OFFSET = 2; // Rating of 2 should get index 0 (threshold between 1 and 2)
+
+export interface RatingTooltipContentProps {
+  appState: AppState;
+  metricKey: MetricKey | string;
+  value: number | string;
+}
+
+function getMaintainabilityGrid(ratingGridSetting: string) {
+  const numbers = ratingGridSetting
+    .split(',')
+    .map(s => parseFloat(s))
+    .filter(n => !isNaN(n));
+
+  return numbers.length === RATING_GRID_SIZE ? numbers : [0, 0, 0, 0];
+}
+
+export function RatingTooltipContent(props: RatingTooltipContentProps) {
+  const {
+    appState: { settings },
+    metricKey,
+    value
+  } = props;
+
+  const finalMetricKey = isDiffMetric(metricKey)
+    ? metricKey.slice(DIFF_METRIC_PREFIX_LENGTH)
+    : metricKey;
+
+  if (!KNOWN_RATINGS.includes(finalMetricKey)) {
+    return null;
+  }
+
+  const rating = Number(value);
+  const ratingLetter = formatMeasure(value, 'RATING');
+
+  if (finalMetricKey !== 'sqale_rating' && finalMetricKey !== 'maintainability_rating') {
+    return <>{translate('metric', finalMetricKey, 'tooltip', ratingLetter)}</>;
+  }
+
+  const maintainabilityGrid = getMaintainabilityGrid(settings[GlobalSettingKeys.RatingGrid] ?? '');
+  const maintainabilityRatingThreshold =
+    maintainabilityGrid[Math.floor(rating) - GRID_INDEX_OFFSET];
+
+  return (
+    // Required to correctly satisfy the context typing
+    // eslint-disable-next-line react/jsx-no-useless-fragment
+    <>
+      {rating === 1
+        ? translateWithParameters(
+            'metric.sqale_rating.tooltip.A',
+            formatMeasure(maintainabilityGrid[0] * PERCENT_MULTIPLIER, 'PERCENT')
+          )
+        : translateWithParameters(
+            'metric.sqale_rating.tooltip',
+            ratingLetter,
+            formatMeasure(maintainabilityRatingThreshold * PERCENT_MULTIPLIER, 'PERCENT')
+          )}
+    </>
+  );
+}
+
+export default withAppStateContext(RatingTooltipContent);
index 5bbf237365c4b2b05362ef7f6c5ef26c9450c58b..5fbc431a0b79688dccc8e8976fb42528ffe26acc 100644 (file)
@@ -21,12 +21,6 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import Measure from '../Measure';
 
-jest.mock('../../../helpers/measures', () => {
-  const measures = jest.requireActual('../../../helpers/measures');
-  measures.getRatingTooltip = jest.fn(() => 'tooltip');
-  return measures;
-});
-
 it('renders trivial measure', () => {
   expect(
     shallow(<Measure metricKey="coverage" metricType="PERCENT" value="73.0" />)
@@ -45,18 +39,12 @@ it('renders LEVEL', () => {
   ).toMatchSnapshot();
 });
 
-it('renders known RATING', () => {
+it('renders RATING', () => {
   expect(
     shallow(<Measure metricKey="sqale_rating" metricType="RATING" value="3" />)
   ).toMatchSnapshot();
 });
 
-it('renders unknown RATING', () => {
-  expect(
-    shallow(<Measure metricKey="foo_rating" metricType="RATING" value="4" />)
-  ).toMatchSnapshot();
-});
-
 it('renders undefined measure', () => {
   expect(
     shallow(<Measure metricKey="foo" metricType="PERCENT" value={undefined} />)
diff --git a/server/sonar-web/src/main/js/components/measure/__tests__/RatingTooltipContent-test.tsx b/server/sonar-web/src/main/js/components/measure/__tests__/RatingTooltipContent-test.tsx
new file mode 100644 (file)
index 0000000..4a490d6
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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 { GlobalSettingKeys } from '../../../types/settings';
+import { MetricKey } from '../../../types/metrics';
+import { RatingTooltipContent, RatingTooltipContentProps } from '../RatingTooltipContent';
+
+it('should render maintainability correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('sqale rating');
+  expect(shallowRender({ value: 1 })).toMatchSnapshot('sqale rating A');
+  expect(shallowRender({ appState: mockAppState({ settings: {} }) })).toMatchSnapshot(
+    'sqale rating default grid'
+  );
+  expect(
+    shallowRender({
+      appState: mockAppState({ settings: { [GlobalSettingKeys.RatingGrid]: '0,0.1' } })
+    })
+  ).toMatchSnapshot('sqale rating wrong grid');
+});
+
+it('should render other ratings correctly', () => {
+  expect(shallowRender({ metricKey: MetricKey.security_rating })).toMatchSnapshot(
+    'security rating'
+  );
+  expect(shallowRender({ metricKey: MetricKey.new_security_rating })).toMatchSnapshot(
+    'new security rating'
+  );
+});
+
+it('should ignore non-rating metrics', () => {
+  expect(shallowRender({ metricKey: MetricKey.code_smells }).type()).toBeNull();
+});
+
+function shallowRender(overrides: Partial<RatingTooltipContentProps> = {}) {
+  return shallow(
+    <RatingTooltipContent
+      appState={mockAppState({ settings: { [GlobalSettingKeys.RatingGrid]: '0.05,0.1,0.2,0.4' } })}
+      metricKey={MetricKey.sqale_rating}
+      value={2}
+      {...overrides}
+    />
+  );
+}
index 437ca28a87b549b53b3dda6f269ed4f59f23083d..29e685efb6bb2344d2510b384572ba0aecd16032 100644 (file)
@@ -6,9 +6,14 @@ exports[`renders LEVEL 1`] = `
 />
 `;
 
-exports[`renders known RATING 1`] = `
+exports[`renders RATING 1`] = `
 <Tooltip
-  overlay="tooltip"
+  overlay={
+    <withAppStateContext(RatingTooltipContent)
+      metricKey="sqale_rating"
+      value="3"
+    />
+  }
 >
   <span>
     <Rating
@@ -35,9 +40,3 @@ exports[`renders undefined measure 1`] = `
   â€“
 </span>
 `;
-
-exports[`renders unknown RATING 1`] = `
-<Rating
-  value="4"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/RatingTooltipContent-test.tsx.snap b/server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/RatingTooltipContent-test.tsx.snap
new file mode 100644 (file)
index 0000000..2ce6e0c
--- /dev/null
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render maintainability correctly: sqale rating 1`] = `
+<Fragment>
+  metric.sqale_rating.tooltip.B.5.0%
+</Fragment>
+`;
+
+exports[`should render maintainability correctly: sqale rating A 1`] = `
+<Fragment>
+  metric.sqale_rating.tooltip.A.5.0%
+</Fragment>
+`;
+
+exports[`should render maintainability correctly: sqale rating default grid 1`] = `
+<Fragment>
+  metric.sqale_rating.tooltip.B.0.0%
+</Fragment>
+`;
+
+exports[`should render maintainability correctly: sqale rating wrong grid 1`] = `
+<Fragment>
+  metric.sqale_rating.tooltip.B.0.0%
+</Fragment>
+`;
+
+exports[`should render other ratings correctly: new security rating 1`] = `
+<Fragment>
+  metric.security_rating.tooltip.B
+</Fragment>
+`;
+
+exports[`should render other ratings correctly: security rating 1`] = `
+<Fragment>
+  metric.security_rating.tooltip.B
+</Fragment>
+`;
index aea8911ff2e78ccadef542d506a71eae26227672..8fb375bc9a05eaa83a71ecee34bf1a7de0622c79 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 { getRatingTooltip as nextGetRatingTooltip, isDiffMetric } from '../../helpers/measures';
 import { Dict, Measure, MeasureEnhanced, MeasureIntern, Metric } from '../../types/types';
 
-const KNOWN_RATINGS = [
+export const KNOWN_RATINGS = [
   'sqale_rating',
   'maintainability_rating', // Needed to provide the label for "new_maintainability_rating"
   'reliability_rating',
@@ -39,11 +38,3 @@ export function enhanceMeasure(measure: Measure, metrics: Dict<Metric>): Measure
 export function getLeakValue(measure: MeasureIntern | undefined): string | undefined {
   return measure?.period?.value;
 }
-
-export function getRatingTooltip(metricKey: string, value: number): string | undefined {
-  const finalMetricKey = isDiffMetric(metricKey) ? metricKey.substr(4) : metricKey;
-  if (KNOWN_RATINGS.includes(finalMetricKey)) {
-    return nextGetRatingTooltip(finalMetricKey, value);
-  }
-  return undefined;
-}
index 3d8c44c888b94400eb45015794fdc33db679cb31..13d10fb06ec32c8ec29d821436766020d7828225 100644 (file)
@@ -25,7 +25,7 @@ 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 { AppState } from '../../../../types/appstate';
 import SentenceWithHighlights from '../../components/SentenceWithHighlights';
 
 export interface PublishStepsProps {
index 31c332fa14f17cfc6c35b5525f6315a93ca7a77b..b0e372c194fe5f9db76171699ad435415da2664b 100644 (file)
@@ -20,7 +20,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 { AppState } from '../../../types/appstate';
+import { Component } from '../../../types/types';
 import { CompilationInfo } from '../components/CompilationInfo';
 import CreateYmlFile from '../components/CreateYmlFile';
 import { BuildTools } from '../types';
index 20b11cbb19d9fc8c5158e69b6cf8d076dc74abfc..1a8b2bcf243716e6afa334b08a71a90825bd3f72 100644 (file)
@@ -22,7 +22,7 @@ import withAppStateContext from '../../../app/components/app-state/withAppStateC
 import { translate } from '../../../helpers/l10n';
 import { getBaseUrl } from '../../../helpers/system';
 import { AlmKeys } from '../../../types/alm-settings';
-import { AppState } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
 import SentenceWithHighlights from './SentenceWithHighlights';
 
 export interface AllSetProps {
index 4038f37237e029f9f1a81c5f1d8bc348aa43301e..d0bd6b5c0b2c3528d65a5d3f1034e346266b18e6 100644 (file)
@@ -19,7 +19,8 @@
  */
 import * as React from 'react';
 import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
-import { AppState, Component } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
+import { Component } from '../../../types/types';
 import { BuildTools } from '../types';
 import CFamily from './commands/CFamily';
 import DotNet from './commands/DotNet';
index bc6ec8c0039ff1fbacaeb21dafea125631444087..7b83e9de998e6d6623c4a2c821d20ca94d05fcad 100644 (file)
@@ -22,7 +22,7 @@ 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 { AppState } from '../../../types/appstate';
 import FinishButton from '../components/FinishButton';
 import GithubCFamilyExampleRepositories from '../components/GithubCFamilyExampleRepositories';
 import Step from '../components/Step';
index 15ddfb7fa8697f35b9ffcff1ce3abad0a840d234..bb65db801d6e251a6d2d7c8651801066732e4fb4 100644 (file)
@@ -28,7 +28,8 @@ import {
   AlmSettingsInstance,
   ProjectAlmBindingResponse
 } from '../../../types/alm-settings';
-import { AppState, Component, CurrentUserSetting } from '../../../types/types';
+import { AppState } from '../../../types/appstate';
+import { Component, CurrentUserSetting } from '../../../types/types';
 import AllSetStep from '../components/AllSetStep';
 import JenkinsfileStep from './JenkinsfileStep';
 import MultiBranchPipelineStep from './MultiBranchPipelineStep';
index b9c60bcceef731f342918d22923e2c074b92891d..82eb5fe5b56e6866d71b8b9300488259da3ffc92 100644 (file)
  */
 import classNames from 'classnames';
 import * as React from 'react';
-import { connect } from 'react-redux';
+import withAppStateContext from '../../app/components/app-state/withAppStateContext';
 import GenericAvatar from '../../components/ui/GenericAvatar';
-import { getGlobalSettingValue, Store } from '../../store/rootReducer';
+import { AppState } from '../../types/appstate';
+import { GlobalSettingKeys } from '../../types/settings';
+
+const GRAVATAR_SIZE_MULTIPLIER = 2;
 
 interface Props {
+  appState: AppState;
   className?: string;
-  enableGravatar: boolean;
-  gravatarServerUrl: string;
   hash?: string;
   name?: string;
   size: number;
 }
 
-function Avatar(props: Props) {
-  if (!props.enableGravatar || !props.hash) {
-    if (!props.name) {
+export function Avatar(props: Props) {
+  const {
+    appState: { settings },
+    className,
+    hash,
+    name,
+    size
+  } = props;
+
+  const enableGravatar = settings[GlobalSettingKeys.EnableGravatar] === 'true';
+
+  if (!enableGravatar || !hash) {
+    if (!name) {
       return null;
     }
-    return <GenericAvatar className={props.className} name={props.name} size={props.size} />;
+    return <GenericAvatar className={className} name={name} size={size} />;
   }
 
-  const url = props.gravatarServerUrl
-    .replace('{EMAIL_MD5}', props.hash)
-    .replace('{SIZE}', String(props.size * 2));
+  const gravatarServerUrl = settings[GlobalSettingKeys.GravatarServerUrl] ?? '';
+  const url = gravatarServerUrl
+    .replace('{EMAIL_MD5}', hash)
+    .replace('{SIZE}', String(size * GRAVATAR_SIZE_MULTIPLIER));
 
   return (
     <img
-      alt={props.name}
-      className={classNames(props.className, 'rounded')}
-      height={props.size}
+      alt={name}
+      className={classNames(className, 'rounded')}
+      height={size}
       src={url}
-      width={props.size}
+      width={size}
     />
   );
 }
 
-const mapStateToProps = (state: Store) => {
-  const enableGravatar = getGlobalSettingValue(state, 'sonar.lf.enableGravatar');
-  const gravatarServerUrl = getGlobalSettingValue(state, 'sonar.lf.gravatarServerUrl');
-  return {
-    enableGravatar: Boolean(enableGravatar && enableGravatar.value === 'true'),
-    gravatarServerUrl: (gravatarServerUrl && gravatarServerUrl.value) || ''
-  };
-};
-
-export default connect(mapStateToProps)(Avatar);
-
-export const unconnectedAvatar = Avatar;
+export default withAppStateContext(Avatar);
index b5d138f2e25c4847980a5efddf85ac2fdad7de88..b9bb9fec6813587e1b1f4c14a3f432f1663dd2cf 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { unconnectedAvatar as Avatar } from '../Avatar';
+import { mockAppState } from '../../../helpers/testMocks';
+import { GlobalSettingKeys } from '../../../types/settings';
+import { Avatar } from '../Avatar';
 
 const gravatarServerUrl = 'http://example.com/{EMAIL_MD5}.jpg?s={SIZE}';
 
 it('should be able to render with hash only', () => {
   const avatar = shallow(
     <Avatar
-      enableGravatar={true}
-      gravatarServerUrl={gravatarServerUrl}
+      appState={mockAppState({
+        settings: {
+          [GlobalSettingKeys.EnableGravatar]: 'true',
+          [GlobalSettingKeys.GravatarServerUrl]: gravatarServerUrl
+        }
+      })}
       hash="7daf6c79d4802916d83f6266e24850af"
       name="Foo"
       size={30}
@@ -38,14 +44,14 @@ it('should be able to render with hash only', () => {
 
 it('falls back to dummy avatar', () => {
   const avatar = shallow(
-    <Avatar enableGravatar={false} gravatarServerUrl="" name="Foo Bar" size={30} />
+    <Avatar appState={mockAppState({ settings: {} })} name="Foo Bar" size={30} />
   );
   expect(avatar).toMatchSnapshot();
 });
 
 it('do not fail when name is missing', () => {
   const avatar = shallow(
-    <Avatar enableGravatar={false} gravatarServerUrl="" name={undefined} size={30} />
+    <Avatar appState={mockAppState({ settings: {} })} name={undefined} size={30} />
   );
   expect(avatar.getElement()).toBeNull();
 });
index 6826d56c33691aa9eeda6f4f71fa999200e3506a..d78e057f2f6bab18819c92d31ece330c62ea131f 100644 (file)
@@ -21,9 +21,9 @@ 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 { AppState } from '../../types/appstate';
 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 { Alert, AlertVariant } from '../ui/Alert';
diff --git a/server/sonar-web/src/main/js/helpers/analytics.js b/server/sonar-web/src/main/js/helpers/analytics.js
deleted file mode 100644 (file)
index 317c675..0000000
+++ /dev/null
@@ -1,26 +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.
- */
-// The body of the `gtm` function comes from Google Tag Manager docs; let's keep it like it was written.
-// @ts-ignore
-// prettier-ignore
-// eslint-disable-next-line
-const gtm = id => (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});const f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);}(window,document,'script','dataLayer',id));
-
-module.exports = { gtm };
index 3a6f15040a8e4c871cb56e4802fcd5e453e6e0c6..cedf304914a8a92a7334467036381b863827651e 100644 (file)
@@ -63,68 +63,6 @@ export function isDiffMetric(metricKey: MetricKey | string): boolean {
   return metricKey.indexOf('new_') === 0;
 }
 
-function getRatingGrid(): string {
-  // workaround cyclic dependencies
-  const getStore = require('../app/utils/getStore').default;
-  const { getGlobalSettingValue } = require('../store/rootReducer');
-
-  const store = getStore();
-  const settingValue = getGlobalSettingValue(store.getState(), 'sonar.technicalDebt.ratingGrid');
-  return settingValue ? settingValue.value : '';
-}
-
-let maintainabilityRatingGrid: number[];
-
-function getMaintainabilityRatingGrid(): number[] {
-  if (maintainabilityRatingGrid) {
-    return maintainabilityRatingGrid;
-  }
-
-  const str = getRatingGrid();
-  const numbers = str
-    .split(',')
-    .map(s => parseFloat(s))
-    .filter(n => !isNaN(n));
-
-  if (numbers.length === 4) {
-    maintainabilityRatingGrid = numbers;
-  } else {
-    maintainabilityRatingGrid = [0, 0, 0, 0];
-  }
-
-  return maintainabilityRatingGrid;
-}
-
-function getMaintainabilityRatingTooltip(rating: number): string {
-  const maintainabilityGrid = getMaintainabilityRatingGrid();
-  const maintainabilityRatingThreshold = maintainabilityGrid[Math.floor(rating) - 2];
-
-  if (rating < 2) {
-    return translateWithParameters(
-      'metric.sqale_rating.tooltip.A',
-      formatMeasure(maintainabilityGrid[0] * 100, 'PERCENT')
-    );
-  }
-
-  const ratingLetter = formatMeasure(rating, 'RATING');
-
-  return translateWithParameters(
-    'metric.sqale_rating.tooltip',
-    ratingLetter,
-    formatMeasure(maintainabilityRatingThreshold * 100, 'PERCENT')
-  );
-}
-
-export function getRatingTooltip(metricKey: MetricKey | string, value: number | string): string {
-  const ratingLetter = formatMeasure(value, 'RATING');
-
-  const finalMetricKey = isDiffMetric(metricKey) ? metricKey.substr(4) : metricKey;
-
-  return finalMetricKey === 'sqale_rating' || finalMetricKey === 'maintainability_rating'
-    ? getMaintainabilityRatingTooltip(Number(value))
-    : translate('metric', finalMetricKey, 'tooltip', ratingLetter);
-}
-
 export function getDisplayMetrics(metrics: Metric[]) {
   return metrics.filter(metric => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type));
 }
index fde809bb117f29766955e9f682d51dec3651b4db..d94d804787a724f5d385347499c940b26cf2f4e8 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 { Location } from 'history';
+import { Location, LocationDescriptor } from 'history';
 import { InjectedRouter } from 'react-router';
 import { createStore, Store } from 'redux';
 import { DocumentationEntry } from '../apps/documentation/utils';
 import { Exporter, Profile } from '../apps/quality-profiles/types';
+import { AppState } from '../types/appstate';
+import { EditionKey } from '../types/editions';
 import { Language } from '../types/languages';
 import { DumpStatus, DumpTask } from '../types/project-dump';
 import { TaskStatuses } from '../types/tasks';
@@ -29,7 +31,6 @@ import {
   AlmApplication,
   Analysis,
   AnalysisEvent,
-  AppState,
   Condition,
   CurrentUser,
   FlowLocation,
@@ -117,7 +118,7 @@ export function mockAnalysisEvent(overrides: Partial<AnalysisEvent> = {}): Analy
 
 export function mockAppState(overrides: Partial<AppState> = {}): AppState {
   return {
-    edition: 'community',
+    edition: EditionKey.community,
     productionDatabase: true,
     qualifiers: ['TRK'],
     settings: {},
@@ -526,7 +527,12 @@ export function mockQualityProfileExporter(override?: Partial<Exporter>): Export
   };
 }
 
-export function mockRouter(overrides: { push?: Function; replace?: Function } = {}) {
+export function mockRouter(
+  overrides: {
+    push?: (loc: LocationDescriptor) => void;
+    replace?: (loc: LocationDescriptor) => void;
+  } = {}
+) {
   return {
     createHref: jest.fn(),
     createPath: jest.fn(),
index fbc4f8ace63a080894018c20e57562c081d85440..c1a3ec926e3cb9e75fdcac9430f08eca7db37d0d 100644 (file)
@@ -30,7 +30,8 @@ import { MetricsContext } from '../app/components/metrics/MetricsContext';
 import getStore from '../app/utils/getStore';
 import { RouteWithChildRoutes } from '../app/utils/startReactApp';
 import { Store as State } from '../store/rootReducer';
-import { AppState, Dict, Metric } from '../types/types';
+import { AppState } from '../types/appstate';
+import { Dict, Metric } from '../types/types';
 import { DEFAULT_METRICS } from './mocks/metrics';
 import { mockAppState } from './testMocks';
 
index b7ba18c3020a415dc6690fd1a7ac0b97c26f6982..074d20e2a59de019077fe9b1f5bcf2f48f041fa5 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { combineReducers } from 'redux';
-import settingsApp, * as fromSettingsApp from '../apps/settings/store/rootReducer';
 import { BranchLike } from '../types/branch-like';
 import { CurrentUserSettingNames } from '../types/types';
 import branches, * as fromBranches from './branches';
@@ -29,18 +28,12 @@ export type Store = {
   branches: fromBranches.State;
   globalMessages: fromGlobalMessages.State;
   users: fromUsers.State;
-
-  // apps
-  settingsApp: any;
 };
 
 export default combineReducers<Store>({
   branches,
   globalMessages,
-  users,
-
-  // apps
-  settingsApp
+  users
 });
 
 export function getGlobalMessages(state: Store) {
@@ -55,10 +48,6 @@ export function getCurrentUser(state: Store) {
   return fromUsers.getCurrentUser(state.users);
 }
 
-export function getGlobalSettingValue(state: Store, key: string) {
-  return fromSettingsApp.getValue(state.settingsApp, key);
-}
-
 export function getBranchStatusByBranchLike(
   state: Store,
   component: string,
diff --git a/server/sonar-web/src/main/js/types/appstate.ts b/server/sonar-web/src/main/js/types/appstate.ts
new file mode 100644 (file)
index 0000000..6255514
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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 { EditionKey } from './editions';
+import { GlobalSettingKeys } from './settings';
+import { Extension } from './types';
+
+export interface AppState {
+  authenticationError?: boolean;
+  authorizationError?: boolean;
+  branchesEnabled?: boolean;
+  canAdmin?: boolean;
+  edition?: EditionKey;
+  globalPages?: Extension[];
+  projectImportFeatureEnabled?: boolean;
+  instanceUsesDefaultAdminCredentials?: boolean;
+  multipleAlmEnabled?: boolean;
+  needIssueSync?: boolean;
+  productionDatabase: boolean;
+  qualifiers: string[];
+  settings: { [key in GlobalSettingKeys]?: string };
+  standalone?: boolean;
+  version: string;
+  webAnalyticsJsPath?: string;
+}
index a142cf5686191beb555870f86233a364442d18f5..b444b100e698a9ca03e422464a5b702e1a00453e 100644 (file)
@@ -21,8 +21,9 @@ import { IntlShape } from 'react-intl';
 import { Store as ReduxStore } from 'redux';
 import { Location, Router } from '../components/hoc/withRouter';
 import { Store } from '../store/rootReducer';
+import { AppState } from './appstate';
 import { L10nBundle } from './l10n';
-import { AppState, CurrentUser, Dict } from './types';
+import { CurrentUser, Dict } from './types';
 
 export enum AdminPageExtension {
   GovernanceConsole = 'governance/views_console'
index 909e801bfa657d78930b35715eb3c6c65c7d0092..bf95ca0336bfef48492d42ce8f2a784097a8117d 100644 (file)
 import { Dict } from './types';
 
 export const enum SettingsKey {
+  AuditHouseKeeping = 'sonar.dbcleaner.auditHousekeeping',
   DaysBeforeDeletingInactiveBranchesAndPRs = 'sonar.dbcleaner.daysBeforeDeletingInactiveBranchesAndPRs',
   DefaultProjectVisibility = 'projects.default.visibility',
   ServerBaseUrl = 'sonar.core.serverBaseURL',
   PluginRiskConsent = 'sonar.plugins.risk.consent'
 }
 
+export enum GlobalSettingKeys {
+  LogoUrl = 'sonar.lf.logoUrl',
+  LogoWidth = 'sonar.lf.logoWidthPx',
+  EnableGravatar = 'sonar.lf.enableGravatar',
+  GravatarServerUrl = 'sonar.lf.gravatarServerUrl',
+  RatingGrid = 'sonar.technicalDebt.ratingGrid',
+  DeveloperAggregatedInfoDisabled = 'sonar.developerAggregatedInfo.disabled',
+  UpdatecenterActivated = 'sonar.updatecenter.activate'
+}
+
 export type SettingDefinitionAndValue = {
   definition: ExtendedSettingDefinition;
   settingValue?: SettingValue;
index cbff79bdbc7b09bc53d46edd90f3575c7a45fc3b..2c6342be3dd3d101d4db9a7c4d5f47e530417b95 100644 (file)
@@ -83,25 +83,6 @@ export interface AnalysisEvent {
   };
 }
 
-export interface AppState {
-  authenticationError?: boolean;
-  authorizationError?: boolean;
-  branchesEnabled?: boolean;
-  canAdmin?: boolean;
-  edition: 'community' | 'developer' | 'enterprise' | 'datacenter' | undefined;
-  globalPages?: Extension[];
-  projectImportFeatureEnabled?: boolean;
-  instanceUsesDefaultAdminCredentials?: boolean;
-  multipleAlmEnabled?: boolean;
-  needIssueSync?: boolean;
-  productionDatabase: boolean;
-  qualifiers: string[];
-  settings: Dict<string>;
-  standalone?: boolean;
-  version: string;
-  webAnalyticsJsPath?: string;
-}
-
 export interface Breadcrumb {
   key: string;
   name: string;