]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9747 Make components and global settings independant from each others in the ui
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 24 Aug 2017 07:23:21 +0000 (09:23 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 25 Aug 2017 14:01:06 +0000 (16:01 +0200)
20 files changed:
server/sonar-web/src/main/js/app/components/DefaultHelmetContainer.js
server/sonar-web/src/main/js/app/components/GlobalFooterContainer.js
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.js
server/sonar-web/src/main/js/apps/about/components/AboutApp.js
server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.js
server/sonar-web/src/main/js/apps/settings/components/App.js
server/sonar-web/src/main/js/apps/settings/components/AppContainer.js
server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js
server/sonar-web/src/main/js/apps/settings/components/Definition.js
server/sonar-web/src/main/js/apps/settings/store/actions.js
server/sonar-web/src/main/js/apps/settings/store/rootReducer.js
server/sonar-web/src/main/js/apps/settings/store/values/actions.js
server/sonar-web/src/main/js/apps/settings/store/values/reducer.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingContainer.js
server/sonar-web/src/main/js/apps/update-center/components/UpdateCenterAppContainer.js
server/sonar-web/src/main/js/components/ui/Avatar.js
server/sonar-web/src/main/js/helpers/measures.js
server/sonar-web/src/main/js/store/rootReducer.js
tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectAdministrationTest.java

index 6f339f1fb949a3e5c90f29fd6d09fa6b87f21e6a..36d16d56568016390cebaa0afe5b1aa8978c5e44 100644 (file)
@@ -20,7 +20,7 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import Helmet from 'react-helmet';
-import { getSettingValue } from '../../store/rootReducer';
+import { getGlobalSettingValue } from '../../store/rootReducer';
 
 function DefaultHelmetContainer({ children, sonarqubeDotCom }) {
   return (
@@ -36,7 +36,7 @@ function DefaultHelmetContainer({ children, sonarqubeDotCom }) {
 }
 
 const mapStateToProps = state => ({
-  sonarqubeDotCom: getSettingValue(state, 'sonar.lf.sonarqube.com.enabled')
+  sonarqubeDotCom: getGlobalSettingValue(state, 'sonar.lf.sonarqube.com.enabled')
 });
 
 export default connect(mapStateToProps)(DefaultHelmetContainer);
index 46c834a15d0264a3819e2600ecb61867d47226af..02d44ad5ba4118025792a15ac5782fd22f188d69 100644 (file)
  */
 // @flow
 import { connect } from 'react-redux';
-import { getAppState, getSettingValue } from '../../store/rootReducer';
+import { getAppState, getGlobalSettingValue } from '../../store/rootReducer';
 import GlobalFooter from './GlobalFooter';
 
 const mapStateToProps = state => ({
   sonarqubeVersion: getAppState(state).version,
   productionDatabase: getAppState(state).productionDatabase,
-  sonarqubeDotCom: getSettingValue(state, 'sonar.lf.sonarqube.com.enabled')
+  sonarqubeDotCom: getGlobalSettingValue(state, 'sonar.lf.sonarqube.com.enabled')
 });
 
 export default connect(mapStateToProps)(GlobalFooter);
index 72b1df3f5c6e704e9a28d1ff41a67af89524045d..0650d20d71472f3ec55a62cf82b22a3c041e805c 100644 (file)
@@ -29,7 +29,7 @@ import NavBar from '../../../../components/nav/NavBar';
 import Tooltip from '../../../../components/controls/Tooltip';
 import HelpIcon from '../../../../components/icons-components/HelpIcon';
 import OnboardingModal from '../../../../apps/tutorials/onboarding/OnboardingModal';
-import { getCurrentUser, getAppState, getSettingValue } from '../../../../store/rootReducer';
+import { getCurrentUser, getAppState, getGlobalSettingValue } from '../../../../store/rootReducer';
 import { translate } from '../../../../helpers/l10n';
 import './GlobalNav.css';
 
@@ -140,7 +140,7 @@ class GlobalNav extends React.PureComponent {
 }
 
 const mapStateToProps = state => {
-  const sonarCloudSetting = getSettingValue(state, 'sonar.lf.sonarqube.com.enabled');
+  const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.lf.sonarqube.com.enabled');
 
   return {
     currentUser: getCurrentUser(state),
index 552379967724cd5ac1336bc66f758b78eb76261a..e3e3fb241d6ccdfc7947b92715a4274a06d4a6be 100644 (file)
@@ -21,7 +21,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Link } from 'react-router';
 import { connect } from 'react-redux';
-import { getSettingValue, getCurrentUser } from '../../../../store/rootReducer';
+import { getGlobalSettingValue, getCurrentUser } from '../../../../store/rootReducer';
 import { translate } from '../../../../helpers/l10n';
 
 class GlobalNavBranding extends React.PureComponent {
@@ -52,8 +52,8 @@ class GlobalNavBranding extends React.PureComponent {
 
 const mapStateToProps = state => ({
   currentUser: getCurrentUser(state),
-  customLogoUrl: (getSettingValue(state, 'sonar.lf.logoUrl') || {}).value,
-  customLogoWidth: (getSettingValue(state, 'sonar.lf.logoWidthPx') || {}).value
+  customLogoUrl: (getGlobalSettingValue(state, 'sonar.lf.logoUrl') || {}).value,
+  customLogoWidth: (getGlobalSettingValue(state, 'sonar.lf.logoWidthPx') || {}).value
 });
 
 export default connect(mapStateToProps)(GlobalNavBranding);
index 7aec02cb77f6b076f51ac4da90722a0925ed1670..36ced3119e78d4ad3f63c4a54fc4274cd91dd1f6 100644 (file)
@@ -33,7 +33,7 @@ import AboutStandards from './AboutStandards';
 import AboutScanners from './AboutScanners';
 import { searchProjects } from '../../../api/components';
 import { getFacet } from '../../../api/issues';
-import { getAppState, getCurrentUser, getSettingValue } from '../../../store/rootReducer';
+import { getAppState, getCurrentUser, getGlobalSettingValue } from '../../../store/rootReducer';
 import { translate } from '../../../helpers/l10n';
 import { fetchAboutPageSettings } from '../actions';
 import AboutAppForSonarQubeDotComLazyLoader from './AboutAppForSonarQubeDotComLazyLoader';
@@ -202,8 +202,8 @@ class AboutApp extends React.PureComponent {
 const mapStateToProps = state => ({
   appState: getAppState(state),
   currentUser: getCurrentUser(state),
-  customText: getSettingValue(state, 'sonar.lf.aboutText'),
-  sonarqubeDotCom: getSettingValue(state, 'sonar.lf.sonarqube.com.enabled')
+  customText: getGlobalSettingValue(state, 'sonar.lf.aboutText'),
+  sonarqubeDotCom: getGlobalSettingValue(state, 'sonar.lf.sonarqube.com.enabled')
 });
 
 const mapDispatchToProps = { fetchAboutPageSettings };
index a22a14408d7b4bbace0c4dd77a855d41f01c94ff..2f602ad826e74055a852fc611babbd354ebb4c82 100644 (file)
@@ -25,7 +25,7 @@ import { Link } from 'react-router';
 import OrganizationsList from './OrganizationsList';
 import { translate } from '../../../helpers/l10n';
 import { fetchIfAnyoneCanCreateOrganizations, fetchMyOrganizations } from './actions';
-import { getAppState, getMyOrganizations, getSettingValue } from '../../../store/rootReducer';
+import { getAppState, getMyOrganizations, getGlobalSettingValue } from '../../../store/rootReducer';
 /*:: import type { Organization } from '../../../store/organizations/duck'; */
 
 class UserOrganizations extends React.PureComponent {
@@ -101,7 +101,7 @@ class UserOrganizations extends React.PureComponent {
 }
 
 const mapStateToProps = state => ({
-  anyoneCanCreate: getSettingValue(state, 'sonar.organizations.anyoneCanCreate'),
+  anyoneCanCreate: getGlobalSettingValue(state, 'sonar.organizations.anyoneCanCreate'),
   canAdmin: getAppState(state).canAdmin,
   organizations: getMyOrganizations(state)
 });
index ab5cf8414ffe3193b6cbc54933809e61dceb09c7..704d742b4a2dd8fab8f6ba3a8bf899cd750c7361 100644 (file)
 // @flow
 import React from 'react';
 import Helmet from 'react-helmet';
-import { connect } from 'react-redux';
 import PageHeader from './PageHeader';
 import CategoryDefinitionsList from './CategoryDefinitionsList';
 import AllCategoriesList from './AllCategoriesList';
 import WildcardsHelp from './WildcardsHelp';
-import { fetchSettings } from '../store/actions';
-import { getSettingsAppDefaultCategory } from '../../../store/rootReducer';
 import { translate } from '../../../helpers/l10n';
 import '../styles.css';
 
 /*::
 type Props = {
-  component: { key: string },
+  component?: { key: string },
   defaultCategory: ?string,
   fetchSettings(componentKey: ?string): Promise<*>,
   location: { query: {} }
@@ -45,7 +42,7 @@ type State = {
 };
 */
 
-class App extends React.PureComponent {
+export default class App extends React.PureComponent {
   /*:: props: Props; */
   state /*: State */ = { loaded: false };
 
@@ -55,12 +52,10 @@ class App extends React.PureComponent {
       html.classList.add('dashboard-page');
     }
     const componentKey = this.props.component ? this.props.component.key : null;
-    this.props.fetchSettings(componentKey).then(() => {
-      this.setState({ loaded: true });
-    });
+    this.props.fetchSettings(componentKey).then(() => this.setState({ loaded: true }));
   }
 
-  componentDidUpdate(prevProps) {
+  componentDidUpdate(prevProps /*: Props*/) {
     if (prevProps.component !== this.props.component) {
       const componentKey = this.props.component ? this.props.component.key : null;
       this.props.fetchSettings(componentKey);
@@ -105,9 +100,3 @@ class App extends React.PureComponent {
     );
   }
 }
-
-const mapStateToProps = state => ({
-  defaultCategory: getSettingsAppDefaultCategory(state)
-});
-
-export default connect(mapStateToProps, { fetchSettings })(App);
index 00a3751b00a3e73ba4ea3de62273a8d761c5e38e..8e80ca6cb6fd3513338f1106f92ac788f17dd51c 100644 (file)
  */
 import { connect } from 'react-redux';
 import App from './App';
-import { getComponent } from '../../../store/rootReducer';
+import { fetchSettings } from '../store/actions';
+import { getComponent, getSettingsAppDefaultCategory } from '../../../store/rootReducer';
 
 const mapStateToProps = (state, ownProps) => ({
   component: ownProps.location.query.id
     ? getComponent(state, ownProps.location.query.id)
-    : undefined
+    : undefined,
+  defaultCategory: getSettingsAppDefaultCategory(state)
 });
 
-export default connect(mapStateToProps)(App);
+const mapdispatchToProps = { fetchSettings };
+
+export default connect(mapStateToProps, mapdispatchToProps)(App);
index 81a992dc8949e97c3818462878fc38b8cb187ee3..bc5217855fb00b8cf84707d1f864cb0ec4ee281f 100644 (file)
@@ -28,7 +28,11 @@ function CategoryDefinitionsList(props) {
 }
 
 const mapStateToProps = (state, ownProps) => ({
-  settings: getSettingsAppSettingsForCategory(state, ownProps.category)
+  settings: getSettingsAppSettingsForCategory(
+    state,
+    ownProps.category,
+    ownProps.component ? ownProps.component.key : null
+  )
 });
 
 export default connect(mapStateToProps)(CategoryDefinitionsList);
index cc30120bb5bca09f951d0e9ba094031230f4e53e..d064997d3621ffe50c65b129af1a271c7ee674cd 100644 (file)
@@ -101,7 +101,8 @@ class Definition extends React.PureComponent {
   }
 
   handleCancel() {
-    this.props.cancelChange(this.props.setting.definition.key);
+    const componentKey = this.props.component ? this.props.component.key : null;
+    this.props.cancelChange(this.props.setting.definition.key, componentKey);
     this.props.passValidation(this.props.setting.definition.key);
   }
 
index fab49983ff790cd97666da800dd1e9290f338932..cbdd7eb63baf649e1ed78b052b1f26cf15aed885 100644 (file)
@@ -43,7 +43,7 @@ export const fetchSettings = componentKey => dispatch => {
       return getValues(keys, componentKey);
     })
     .then(settings => {
-      dispatch(receiveValues(settings));
+      dispatch(receiveValues(settings, componentKey));
       dispatch(closeAllGlobalMessages());
     })
     .catch(e => parseError(e).then(message => dispatch(addGlobalErrorMessage(message))));
@@ -65,7 +65,7 @@ export const saveValue = (key, componentKey) => (dispatch, getState) => {
   return setSettingValue(definition, value, componentKey)
     .then(() => getValues(key, componentKey))
     .then(values => {
-      dispatch(receiveValues(values));
+      dispatch(receiveValues(values, componentKey));
       dispatch(cancelChange(key));
       dispatch(passValidation(key));
       dispatch(stopLoading(key));
@@ -84,9 +84,9 @@ export const resetValue = (key, componentKey) => dispatch => {
     .then(() => getValues(key, componentKey))
     .then(values => {
       if (values.length > 0) {
-        dispatch(receiveValues(values));
+        dispatch(receiveValues(values, componentKey));
       } else {
-        dispatch(receiveValues([{ key }]));
+        dispatch(receiveValues([{ key }], componentKey));
       }
       dispatch(passValidation(key));
       dispatch(stopLoading(key));
index d483892a9aaa2d7ec5f366738544c5d0303e92ba..0f22d81cf13bd02f535c348ccf9ae679062eaeca 100644 (file)
@@ -24,8 +24,9 @@ import values, * as fromValues from './values/reducer';
 import settingsPage, * as fromSettingsPage from './settingsPage/reducer';
 import licenses, * as fromLicenses from './licenses/reducer';
 import globalMessages, * as fromGlobalMessages from '../../../store/globalMessages/duck';
-/*:: import type { State as GlobalMessagesState } from '../../../store/globalMessages/duck'; */
 import encryptionPage from './encryptionPage/reducer';
+/*:: import type { State as GlobalMessagesState } from '../../../store/globalMessages/duck'; */
+/*:: import type { State as ValuesState } from './values/reducer'; */
 
 /*::
 type State = {
@@ -34,7 +35,7 @@ type State = {
   globalMessages: GlobalMessagesState,
   licenses: {},
   settingsPage: {},
-  values: {}
+  values: ValuesState
 };
 */
 
@@ -58,12 +59,16 @@ export const getAllCategories = (state /*: State */) =>
 export const getDefaultCategory = (state /*: State */) =>
   fromDefinitions.getDefaultCategory(state.definitions);
 
-export const getValue = (state /*: State */, key /*: string */) =>
-  fromValues.getValue(state.values, key);
+export const getValue = (state /*: State */, key /*: string */, componentKey /*: ?string */) =>
+  fromValues.getValue(state.values, key, componentKey);
 
-export const getSettingsForCategory = (state /*: State */, category /*: string */) =>
+export const getSettingsForCategory = (
+  state /*: State */,
+  category /*: string */,
+  componentKey /*: ?string */
+) =>
   fromDefinitions.getDefinitionsForCategory(state.definitions, category).map(definition => ({
-    ...getValue(state, definition.key),
+    ...getValue(state, definition.key, componentKey),
     definition
   }));
 
index e9087a0b6d54b3eff47241710d5315d365fa37a4..8ccc1d5ec1e46d2606fee12961836115c1456aed 100644 (file)
@@ -27,7 +27,8 @@ export const RECEIVE_VALUES /*: string */ = 'RECEIVE_VALUES';
  * @param {Array} settings
  * @returns {Object}
  */
-export const receiveValues = (settings /*: SettingValue[] */) => ({
+export const receiveValues = (settings /*: SettingValue[] */, componentKey /*: ?string */) => ({
   type: RECEIVE_VALUES,
-  settings
+  settings,
+  componentKey
 });
index d8e42984fd7fe512bb4818e9a2d207821898f994..e08ace6f2bbeb2ce6a9136f06721825c3ac19cd6 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 // @flow
+import { combineReducers } from 'redux';
 import { keyBy } from 'lodash';
 import { RECEIVE_VALUES } from './actions';
 
 /*::
-type State = { [key: string]: {} };
+type SettingsState = { [key: string]: {} };
+type ComponentsState = { [key: string]: SettingsState };
+export type State = { components: ComponentsState, global: SettingsState };
 */
 
-const reducer = (state /*: State */ = {}, action /*: Object */) => {
+const componentsSettings = (state /*: ComponentsState */ = {}, action /*: Object */) => {
+  if (!action.componentKey) {
+    return state;
+  }
+
+  const key = action.componentKey;
+  if (action.type === RECEIVE_VALUES) {
+    const settingsByKey = keyBy(action.settings, 'key');
+    return { ...state, [key]: { ...(state[key] || {}), ...settingsByKey } };
+  }
+
+  return state;
+};
+
+const globalSettings = (state /*: SettingsState */ = {}, action /*: Object */) => {
+  if (action.componentKey) {
+    return state;
+  }
+
   if (action.type === RECEIVE_VALUES) {
     const settingsByKey = keyBy(action.settings, 'key');
     return { ...state, ...settingsByKey };
@@ -42,6 +63,12 @@ const reducer = (state /*: State */ = {}, action /*: Object */) => {
   return state;
 };
 
-export default reducer;
+export default combineReducers({ components: componentsSettings, global: globalSettings });
 
-export const getValue = (state /*: State */, key /*: string */) => state[key];
+export const getValue = (state /*: State */, key /*: string */, componentKey /*: ?string */) => {
+  let settings = state.global;
+  if (componentKey) {
+    settings = state.components[componentKey];
+  }
+  return settings && settings[key];
+};
index a7eecc2e9e492701c5949145358aa45e5ddd97d4..246062de81888cba32ed8444a8d925a6150e9ca3 100644 (file)
@@ -23,11 +23,11 @@ import Onboarding from './Onboarding';
 import {
   getCurrentUser,
   areThereCustomOrganizations,
-  getSettingValue
+  getGlobalSettingValue
 } from '../../../store/rootReducer';
 
 const mapStateToProps = state => {
-  const sonarCloudSetting = getSettingValue(state, 'sonar.lf.sonarqube.com.enabled');
+  const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.lf.sonarqube.com.enabled');
 
   return {
     currentUser: getCurrentUser(state),
index cd5a9f8f105c32ed34acf47a2d33249ef0928193..454396e0a8783c1cb05454428ee77324ced11863 100644 (file)
@@ -21,7 +21,7 @@ import React from 'react';
 import Helmet from 'react-helmet';
 import { connect } from 'react-redux';
 import init from '../init';
-import { getSettingValue } from '../../../store/rootReducer';
+import { getGlobalSettingValue } from '../../../store/rootReducer';
 import { translate } from '../../../helpers/l10n';
 
 class UpdateCenterAppContainer extends React.PureComponent {
@@ -48,7 +48,7 @@ class UpdateCenterAppContainer extends React.PureComponent {
 }
 
 const mapStateToProps = state => ({
-  updateCenterActive: (getSettingValue(state, 'sonar.updatecenter.activate') || {}).value
+  updateCenterActive: (getGlobalSettingValue(state, 'sonar.updatecenter.activate') || {}).value
 });
 
 export default connect(mapStateToProps)(UpdateCenterAppContainer);
index a2a53f474aa59ed6832452a15e2e49d2119badcd..448fb782f4bf985d14ea0b754350764445035dd2 100644 (file)
@@ -22,7 +22,7 @@ import PropTypes from 'prop-types';
 import { connect } from 'react-redux';
 import md5 from 'blueimp-md5';
 import classNames from 'classnames';
-import { getSettingValue } from '../../store/rootReducer';
+import { getGlobalSettingValue } from '../../store/rootReducer';
 
 function stringToColor(str) {
   let hash = 0;
@@ -114,8 +114,8 @@ class Avatar extends React.PureComponent {
 }
 
 const mapStateToProps = state => ({
-  enableGravatar: (getSettingValue(state, 'sonar.lf.enableGravatar') || {}).value === 'true',
-  gravatarServerUrl: (getSettingValue(state, 'sonar.lf.gravatarServerUrl') || {}).value
+  enableGravatar: (getGlobalSettingValue(state, 'sonar.lf.enableGravatar') || {}).value === 'true',
+  gravatarServerUrl: (getGlobalSettingValue(state, 'sonar.lf.gravatarServerUrl') || {}).value
 });
 
 export default connect(mapStateToProps)(Avatar);
index b61bdd143e69af71ca891615fd21bf54634f61e2..1dcccdb95297a5f640be852fac26ec327f941554 100644 (file)
@@ -351,10 +351,10 @@ function shortDurationVariationFormatter(value) {
 function getRatingGrid() {
   // workaround cyclic dependencies
   const getStore = require('../app/utils/getStore').default;
-  const { getSettingValue } = require('../store/rootReducer');
+  const { getGlobalSettingValue } = require('../store/rootReducer');
 
   const store = getStore();
-  const settingValue = getSettingValue(store.getState(), 'sonar.technicalDebt.ratingGrid');
+  const settingValue = getGlobalSettingValue(store.getState(), 'sonar.technicalDebt.ratingGrid');
   return settingValue ? settingValue.value : '';
 }
 
index f4d32f82868f54726187ca06141c994b2fbaab80..9cdd198e860860799ce5856bc87b248896e34518 100644 (file)
@@ -153,7 +153,8 @@ export const getPermissionsAppSelectedPermission = state =>
 
 export const getPermissionsAppError = state => fromPermissionsApp.getError(state.permissionsApp);
 
-export const getSettingValue = (state, key) => fromSettingsApp.getValue(state.settingsApp, key);
+export const getGlobalSettingValue = (state, key) =>
+  fromSettingsApp.getValue(state.settingsApp, key);
 
 export const getSettingsAppDefinition = (state, key) =>
   fromSettingsApp.getDefinition(state.settingsApp, key);
@@ -164,8 +165,8 @@ export const getSettingsAppAllCategories = state =>
 export const getSettingsAppDefaultCategory = state =>
   fromSettingsApp.getDefaultCategory(state.settingsApp);
 
-export const getSettingsAppSettingsForCategory = (state, category) =>
-  fromSettingsApp.getSettingsForCategory(state.settingsApp, category);
+export const getSettingsAppSettingsForCategory = (state, category, componentKey) =>
+  fromSettingsApp.getSettingsForCategory(state.settingsApp, category, componentKey);
 
 export const getSettingsAppChangedValue = (state, key) =>
   fromSettingsApp.getChangedValue(state.settingsApp, key);
index 0cd466e725ba713466382ee11e24bb57c2c0c3f5..8e1a210c12c76006de422d8b6b75c043920ebc15 100644 (file)
@@ -21,6 +21,7 @@ package org.sonarqube.tests.projectAdministration;
 
 import com.sonar.orchestrator.Orchestrator;
 import com.sonar.orchestrator.build.SonarScanner;
+import org.openqa.selenium.By;
 import org.sonarqube.pageobjects.ProjectsManagementPage;
 import org.sonarqube.tests.Category1Suite;
 import java.io.UnsupportedEncodingException;
@@ -47,6 +48,7 @@ import org.sonarqube.ws.client.permission.AddUserToTemplateWsRequest;
 import org.sonarqube.ws.client.permission.CreateTemplateWsRequest;
 import org.sonarqube.ws.client.permission.UsersWsRequest;
 
+import static com.codeborne.selenide.Selenide.$;
 import static org.apache.commons.lang.time.DateUtils.addDays;
 import static org.assertj.core.api.Assertions.assertThat;
 import static util.ItUtils.getComponent;
@@ -192,6 +194,22 @@ public class ProjectAdministrationTest {
       .assertStringSettingValue("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "1");
   }
 
+  @Test
+  public void display_correct_global_setting () throws UnsupportedEncodingException {
+    scanSample(null, null);
+    SettingsPage page = nav.logIn().submitCredentials(adminUser).openSettings("sample")
+      .openCategory("Analysis Scope")
+      .assertSettingDisplayed("sonar.coverage.exclusions")
+      .setStringValue("sonar.coverage.exclusions", "foo")
+      .assertStringSettingValue("sonar.coverage.exclusions", "foo");
+
+    $(".global-navbar-menu ").$(By.linkText("Administration")).click();
+    page
+      .openCategory("Analysis Scope")
+      .assertSettingDisplayed("sonar.coverage.exclusions")
+      .assertStringSettingValue("sonar.coverage.exclusions", "");
+  }
+
   @Test
   public void display_module_settings() throws UnsupportedEncodingException {
     orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));