Browse Source

SONAR-5856 Rewrite Settings page (#1163)

tags/6.1-RC1
Stas Vilchik 7 years ago
parent
commit
d2da7f30d5
100 changed files with 3484 additions and 1948 deletions
  1. 0
    2
      it/it-tests/src/test/java/it/Category1Suite.java
  2. 0
    3
      it/it-tests/src/test/java/it/componentDashboard/DashboardTest.java
  3. 3
    5
      it/it-tests/src/test/java/it/measureHistory/DifferentialPeriodsTest.java
  4. 34
    31
      it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java
  5. 36
    31
      it/it-tests/src/test/java/it/settings/PropertySetsTest.java
  6. 23
    26
      it/it-tests/src/test/java/it/settings/SettingsTestRestartingOrchestrator.java
  7. 0
    72
      it/it-tests/src/test/java/it/settings/SubCategoriesTest.java
  8. 4
    0
      it/it-tests/src/test/java/pageobjects/LoginPage.java
  9. 11
    0
      it/it-tests/src/test/java/pageobjects/Navigation.java
  10. 48
    0
      it/it-tests/src/test/java/pageobjects/settings/PropertySetInput.java
  11. 84
    0
      it/it-tests/src/test/java/pageobjects/settings/SettingsPage.java
  12. 0
    89
      it/it-tests/src/test/resources/componentDashboard/DashboardTest/global_dashboard/global-admin-dashboards.html
  13. 0
    49
      it/it-tests/src/test/resources/measureHistory/DifferentialPeriodsTest/define-leak-period-on-project.html
  14. 0
    49
      it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/module-settings/display-module-settings.html
  15. 0
    64
      it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-settings/only-on-project-settings.html
  16. 0
    64
      it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-settings/override-global-settings.html
  17. 1
    16
      it/it-tests/src/test/resources/serverSystem/ServerSystemTest/missing_ip.html
  18. 1
    16
      it/it-tests/src/test/resources/serverSystem/ServerSystemTest/organisation_must_not_accept_special_chars.html
  19. 1
    21
      it/it-tests/src/test/resources/serverSystem/ServerSystemTest/valid_id.html
  20. 0
    84
      it/it-tests/src/test/resources/settings/PropertySetsTest/auto-generated/create.html
  21. 0
    69
      it/it-tests/src/test/resources/settings/PropertySetsTest/auto-generated/update.html
  22. 0
    139
      it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/all_types.html
  23. 0
    89
      it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/create.html
  24. 0
    119
      it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/delete.html
  25. 0
    94
      it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/reference.html
  26. 2
    7
      it/it-tests/src/test/resources/settings/SettingsTest/encrypt-text.html
  27. 0
    59
      it/it-tests/src/test/resources/settings/SettingsTest/general-settings.html
  28. 2
    7
      it/it-tests/src/test/resources/settings/SettingsTest/generate-secret-key.html
  29. 0
    55
      it/it-tests/src/test/resources/settings/SettingsTest/global-extension-property.html
  30. 0
    55
      it/it-tests/src/test/resources/settings/SettingsTest/hidden-extension-property.html
  31. 0
    54
      it/it-tests/src/test/resources/settings/SettingsTest/hide-passwords.html
  32. 0
    54
      it/it-tests/src/test/resources/settings/SettingsTest/property_relocation.html
  33. 0
    79
      it/it-tests/src/test/resources/settings/SettingsTest/validate-property-type.html
  34. 0
    60
      it/it-tests/src/test/resources/settings/subcategories/global-subcategories-no-default.html
  35. 0
    156
      it/it-tests/src/test/resources/settings/subcategories/global-subcategories.html
  36. 0
    60
      it/it-tests/src/test/resources/settings/subcategories/project-subcategories-no-default.html
  37. 0
    150
      it/it-tests/src/test/resources/settings/subcategories/project-subcategories.html
  38. 1
    6
      it/it-tests/src/test/resources/updateCenter/installed-plugins.html
  39. 2
    7
      it/it-tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html
  40. 1
    0
      server/sonar-web/config/webpack/webpack.config.base.js
  41. 1
    5
      server/sonar-web/scripts/start.js
  42. 69
    0
      server/sonar-web/src/main/js/api/settings.js
  43. 6
    0
      server/sonar-web/src/main/js/api/users.js
  44. 46
    0
      server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.js
  45. 50
    0
      server/sonar-web/src/main/js/apps/settings/app.js
  46. 37
    0
      server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js
  47. 101
    0
      server/sonar-web/src/main/js/apps/settings/components/App.js
  48. 71
    0
      server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js
  49. 37
    0
      server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js
  50. 195
    0
      server/sonar-web/src/main/js/apps/settings/components/Definition.js
  51. 59
    0
      server/sonar-web/src/main/js/apps/settings/components/DefinitionChanges.js
  52. 60
    0
      server/sonar-web/src/main/js/apps/settings/components/DefinitionDefaults.js
  53. 45
    0
      server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js
  54. 28
    0
      server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js
  55. 44
    0
      server/sonar-web/src/main/js/apps/settings/components/PageHeader.js
  56. 62
    0
      server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js
  57. 51
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js
  58. 48
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.js
  59. 29
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.js
  60. 89
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js
  61. 50
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.js
  62. 29
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.js
  63. 40
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.js
  64. 90
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js
  65. 75
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.js
  66. 110
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js
  67. 45
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.js
  68. 60
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.js
  69. 77
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.js
  70. 43
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.js
  71. 97
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.js
  72. 66
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.js
  73. 43
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.js
  74. 60
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.js
  75. 103
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.js
  76. 66
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.js
  77. 28
    0
      server/sonar-web/src/main/js/apps/settings/constants.js
  78. 27
    0
      server/sonar-web/src/main/js/apps/settings/propTypes.js
  79. 85
    0
      server/sonar-web/src/main/js/apps/settings/store/actions.js
  80. 30
    0
      server/sonar-web/src/main/js/apps/settings/store/definitions/actions.js
  81. 56
    0
      server/sonar-web/src/main/js/apps/settings/store/definitions/reducer.js
  82. 55
    0
      server/sonar-web/src/main/js/apps/settings/store/rootReducer.js
  83. 33
    0
      server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/actions.js
  84. 37
    0
      server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/reducer.js
  85. 31
    0
      server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/actions.js
  86. 36
    0
      server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/reducer.js
  87. 38
    0
      server/sonar-web/src/main/js/apps/settings/store/settingsPage/reducer.js
  88. 33
    0
      server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/actions.js
  89. 36
    0
      server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/reducer.js
  90. 30
    0
      server/sonar-web/src/main/js/apps/settings/store/values/actions.js
  91. 34
    0
      server/sonar-web/src/main/js/apps/settings/store/values/reducer.js
  92. 157
    0
      server/sonar-web/src/main/js/apps/settings/styles.css
  93. 140
    0
      server/sonar-web/src/main/js/apps/settings/utils.js
  94. 51
    0
      server/sonar-web/src/main/js/components/controls/Toggle.js
  95. 53
    0
      server/sonar-web/src/main/js/components/controls/__tests__/Toggle-test.js
  96. 51
    0
      server/sonar-web/src/main/js/components/controls/styles.css
  97. 5
    0
      server/sonar-web/src/main/js/helpers/l10n.js
  98. 1
    1
      server/sonar-web/src/main/js/helpers/request.js
  99. 1
    1
      server/sonar-web/src/main/less/components/react-select.less
  100. 0
    0
      server/sonar-web/src/main/less/init/forms.less

+ 0
- 2
it/it-tests/src/test/java/it/Category1Suite.java View File

@@ -49,7 +49,6 @@ import it.qualityGate.QualityGateTest;
import it.qualityGate.QualityGateUiTest;
import it.settings.PropertySetsTest;
import it.settings.SettingsTest;
import it.settings.SubCategoriesTest;
import it.sourceCode.EncodingTest;
import it.sourceCode.HighlightingTest;
import it.sourceCode.ProjectCodeTest;
@@ -71,7 +70,6 @@ import static util.ItUtils.xooPlugin;
BackgroundTasksTest.class,
// settings
PropertySetsTest.class,
SubCategoriesTest.class,
SettingsTest.class,
// i18n
I18nTest.class,

+ 0
- 3
it/it-tests/src/test/java/it/componentDashboard/DashboardTest.java View File

@@ -70,9 +70,6 @@ public class DashboardTest {
// SONAR-2073
"/componentDashboard/DashboardTest/global_dashboard/filter-widget-anonymous.html",

// SONAR-3460
"/componentDashboard/DashboardTest/global_dashboard/global-admin-dashboards.html",

// SONAR-3461
"/componentDashboard/DashboardTest/global_dashboard/default-dashboards.html",


+ 3
- 5
it/it-tests/src/test/java/it/measureHistory/DifferentialPeriodsTest.java View File

@@ -21,7 +21,6 @@ package it.measureHistory;

import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.locator.FileLocation;
import com.sonar.orchestrator.selenium.Selenese;
import it.Category1Suite;
import java.util.Date;
import java.util.List;
@@ -32,8 +31,8 @@ import org.junit.Test;
import org.sonar.wsclient.services.Measure;
import org.sonar.wsclient.services.Resource;
import org.sonar.wsclient.services.ResourceQuery;
import pageobjects.Navigation;
import util.ItUtils;
import util.selenium.SeleneseTest;

import static org.apache.commons.lang.time.DateUtils.addDays;
import static org.assertj.core.api.Assertions.assertThat;
@@ -133,9 +132,8 @@ public class DifferentialPeriodsTest {
assertThat(measures.get(0).getVariation1()).isEqualTo(17);

// Check on ui that it's possible to define leak period on project
new SeleneseTest(Selenese.builder().setHtmlTestsInClasspath("define-leak-period-on-project",
"/measureHistory/DifferentialPeriodsTest/define-leak-period-on-project.html"
).build()).runOn(orchestrator);
Navigation.get(orchestrator).openHomepage().logIn().asAdmin().openSettings("sample")
.assertSettingDisplayed("sonar.timemachine.period1");
}

/**

+ 34
- 31
it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java View File

@@ -23,6 +23,7 @@ import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.SonarScanner;
import com.sonar.orchestrator.selenium.Selenese;
import it.Category1Suite;
import java.io.UnsupportedEncodingException;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.GregorianCalendar;
@@ -34,22 +35,27 @@ import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.wsclient.SonarClient;
import org.sonar.wsclient.base.HttpException;
import org.sonar.wsclient.services.PropertyQuery;
import org.sonar.wsclient.services.ResourceQuery;
import org.sonar.wsclient.user.UserParameters;
import pageobjects.Navigation;
import pageobjects.settings.SettingsPage;
import util.selenium.SeleneseTest;

import static org.assertj.core.api.Assertions.assertThat;
import static util.ItUtils.projectDir;

public class ProjectAdministrationTest {

private static final String DELETE_WS_ENDPOINT = "api/projects/bulk_delete";

@ClassRule
public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;

@Rule
public ExpectedException expectedException = ExpectedException.none();

@Rule
public Navigation nav = Navigation.get(orchestrator);

private static final String PROJECT_KEY = "sample";
private static final String FILE_KEY = "sample:src/main/xoo/sample/Sample.xoo";

@@ -116,7 +122,7 @@ public class ProjectAdministrationTest {

new SeleneseTest(
Selenese.builder().setHtmlTestsInClasspath("project-deletion", "/projectAdministration/ProjectAdministrationTest/project-deletion/project-deletion.html").build())
.runOn(orchestrator);
.runOn(orchestrator);
} finally {
wsClient.userClient().deactivate(projectAdminUser);
}
@@ -156,40 +162,37 @@ public class ProjectAdministrationTest {
assertThat(count("events where category='Version'")).as("Different number of events").isEqualTo(1);
}

/**
* SONAR-3425
*/
@Test
public void project_settings() {
scanSampleWithDate("2012-01-01");

Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("project-settings",
// SONAR-3425
"/projectAdministration/ProjectAdministrationTest/project-settings/override-global-settings.html",

"/projectAdministration/ProjectAdministrationTest/project-settings/only-on-project-settings.html").build();
new SeleneseTest(selenese).runOn(orchestrator);

// GET /api/properties/sonar.exclusions?resource=sample
assertThat(orchestrator.getServer().getAdminWsClient().find(PropertyQuery.createForResource("sonar.exclusions", "sample")).getValue())
.isEqualTo("my-exclusions");

// GET /api/properties?resource=sample
// Note that this WS is used by SonarLint
assertThat(orchestrator.getServer().getAdminWsClient().findAll(PropertyQuery.createForResource(null, "sample"))).isNotEmpty();
public void display_project_settings() throws UnsupportedEncodingException {
scanSample(null, null);

SettingsPage page = nav.logIn().asAdmin().openSettings("sample")
.assertMenuContains("Analysis Scope")
.assertMenuContains("Category 1")
.assertMenuContains("DEV")
.assertMenuContains("project-only")
.assertMenuContains("Xoo")
.assertSettingDisplayed("sonar.dbcleaner.daysBeforeDeletingClosedIssues");

page.openCategory("project-only")
.assertSettingDisplayed("prop_only_on_project");

page.openCategory("General")
.assertStringSettingValue("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "30")
.assertStringSettingValue("sonar.timemachine.period1", "previous_version")
.assertBooleanSettingValue("sonar.dbcleaner.cleanDirectory", true)
.setStringValue("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "1")
.assertStringSettingValue("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "1");
}

/**
* SONAR-4060
*/
@Test
public void display_module_settings() {
public void display_module_settings() throws UnsupportedEncodingException {
orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));

Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("module-settings",
// SONAR-3425
"/projectAdministration/ProjectAdministrationTest/module-settings/display-module-settings.html").build();
new SeleneseTest(selenese).runOn(orchestrator);
nav.logIn().asAdmin()
.openSettings("com.sonarsource.it.samples:multi-modules-sample:module_a")
.assertMenuContains("Analysis Scope")
.assertSettingDisplayed("sonar.coverage.exclusions");
}

private void scanSampleWithDate(String date) {

+ 36
- 31
it/it-tests/src/test/java/it/settings/PropertySetsTest.java View File

@@ -20,20 +20,22 @@
package it.settings;

import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.selenium.Selenese;
import it.Category1Suite;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.sonarqube.ws.Settings;
import org.sonarqube.ws.client.setting.ResetRequest;
import org.sonarqube.ws.client.setting.SetRequest;
import org.sonarqube.ws.client.setting.SettingsService;
import org.sonarqube.ws.client.setting.ValuesRequest;
import util.selenium.SeleneseTest;
import pageobjects.Navigation;
import pageobjects.settings.SettingsPage;

import static com.google.common.collect.Lists.newArrayList;
import static java.util.Arrays.asList;
@@ -47,6 +49,9 @@ public class PropertySetsTest {
@ClassRule
public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;

@Rule
public Navigation nav = Navigation.get(orchestrator);

static SettingsService SETTINGS;

@BeforeClass
@@ -60,37 +65,37 @@ public class PropertySetsTest {
}

@Test
public void support_property_sets() {
Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("property-sets",
"/settings/PropertySetsTest/property-sets/create.html",
"/settings/PropertySetsTest/property-sets/delete.html",
"/settings/PropertySetsTest/property-sets/reference.html",
"/settings/PropertySetsTest/property-sets/all_types.html").build();
// Use the old runner because it fails with the new Selenium runner
orchestrator.executeSelenese(selenese);

// SSF-25 Check that the password has well be setted as now it does not appears in the html source code
assertPropertySet("sonar.demo",
asList(entry("password", "abcde"),
entry("boolean", "true"),
entry("float", "42.0"),
entry("license", "abc"),
entry("list", "AAA"),
entry("metric", "overall_branch_coverage"),
entry("regexp", ".*"),
entry("text", "text")));
public void support_property_sets() throws UnsupportedEncodingException {
SettingsPage page = nav.logIn().asAdmin().openSettings(null).openCategory("DEV")
.assertSettingDisplayed("sonar.test.jira.servers");

page.getPropertySetInput("sonar.test.jira.servers")
.setFieldValue("key", "jira1")
.setFieldValue("url", "http://jira")
.setFieldValue("port", "12345")
.save();

assertPropertySet("sonar.test.jira.servers", asList(
entry("key", "jira1"),
entry("url", "http://jira"),
entry("port", "12345")));
}

@Test
public void support_property_sets_with_auto_generated_keys() {
new SeleneseTest(
Selenese.builder().setHtmlTestsInClasspath("create-auto-generated",
"/settings/PropertySetsTest/auto-generated/create.html").build()).runOn(orchestrator);
assertPropertySet("sonar.autogenerated", asList(entry("value", "FIRST")), asList(entry("value", "SECOND")), asList(entry("value", "THIRD")));

new SeleneseTest(Selenese.builder().setHtmlTestsInClasspath("update-auto-generated",
"/settings/PropertySetsTest/auto-generated/update.html").build()).runOn(orchestrator);
assertPropertySet("sonar.autogenerated", asList(entry("value", "FIRST")), asList(entry("value", "THIRD")));
public void support_property_sets_with_auto_generated_keys() throws UnsupportedEncodingException {
SettingsPage page = nav.logIn().asAdmin().openSettings(null).openCategory("DEV")
.assertSettingDisplayed("sonar.autogenerated");

page.getPropertySetInput("sonar.autogenerated")
.setFieldValue(0, "value", "FIRST")
.setFieldValue(1, "value", "SECOND")
.setFieldValue(2, "value", "THIRD")
.save();

assertPropertySet("sonar.autogenerated",
asList(entry("value", "FIRST")),
asList(entry("value", "SECOND")),
asList(entry("value", "THIRD")));
}

@Test
@@ -124,7 +129,7 @@ public class PropertySetsTest {
assertThat(setting.getFieldValues().getFieldValuesList()).hasSize(fieldsValues.length);
int index = 0;
for (Settings.FieldValues.Value fieldsValue : setting.getFieldValues().getFieldValuesList()) {
assertThat(fieldsValue.getValue()).containsOnly(fieldsValues[index].toArray(new Map.Entry[] {}));
assertThat(fieldsValue.getValue()).containsOnly(fieldsValues[index].toArray(new Map.Entry[]{}));
index++;
}
}

+ 23
- 26
it/it-tests/src/test/java/it/settings/SettingsTestRestartingOrchestrator.java View File

@@ -22,11 +22,13 @@ package it.settings;
import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.SonarScanner;
import com.sonar.orchestrator.selenium.Selenese;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import pageobjects.Navigation;
import util.selenium.SeleneseTest;

import static util.ItUtils.pluginArtifact;
@@ -51,7 +53,7 @@ public class SettingsTestRestartingOrchestrator {
}

@Test
public void test_settings() {
public void test_settings() throws UnsupportedEncodingException {
URL secretKeyUrl = getClass().getResource("/settings/SettingsTest/sonar-secret.txt");
orchestrator = Orchestrator.builderEnv()
.addPlugin(pluginArtifact("settings-plugin"))
@@ -60,33 +62,28 @@ public class SettingsTestRestartingOrchestrator {
.build();
orchestrator.start();

Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_settings",
"/settings/SettingsTest/general-settings.html",

// SONAR-2869 the annotation @Properties can be used on extensions and not only on plugin entry points
"/settings/SettingsTest/hidden-extension-property.html",
"/settings/SettingsTest/global-extension-property.html",
Navigation.get(orchestrator).openHomepage().logIn().asAdmin().openSettings(null)
.assertMenuContains("General")
.assertSettingDisplayed("sonar.dbcleaner.cleanDirectory")
.assertSettingNotDisplayed("settings.extension.hidden")
.assertSettingNotDisplayed("settings.extension.global");

// SONAR-3344 - licenses
"/settings/SettingsTest/ignore-corrupted-license.html",
"/settings/SettingsTest/display-license.html",
"/settings/SettingsTest/display-untyped-license.html",

// SONAR-2084 - encryption
Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_settings",
// test encryption
"/settings/SettingsTest/generate-secret-key.html",
"/settings/SettingsTest/encrypt-text.html",
// SONAR-1378 - property types
"/settings/SettingsTest/validate-property-type.html",
// SONAR-3127 - hide passwords
"/settings/SettingsTest/hide-passwords.html"
).build();
"/settings/SettingsTest/encrypt-text.html"

// test licenses
// TODO enable when license page will be rewritten
// "/settings/SettingsTest/ignore-corrupted-license.html",
// "/settings/SettingsTest/display-license.html",
// "/settings/SettingsTest/display-untyped-license.html"
).build();
new SeleneseTest(selenese).runOn(orchestrator);
}

@Test
public void property_relocation() {
public void property_relocation() throws UnsupportedEncodingException {
orchestrator = Orchestrator.builderEnv()
.addPlugin(pluginArtifact("property-relocation-plugin"))
.addPlugin(xooPlugin())
@@ -101,10 +98,10 @@ public class SettingsTestRestartingOrchestrator {
// should not fail
orchestrator.executeBuilds(withDeprecatedKey, withNewKey);

Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("property_relocation",
"/settings/SettingsTest/property_relocation.html"
).build();
new SeleneseTest(selenese).runOn(orchestrator);
Navigation.get(orchestrator).openHomepage().logIn().asAdmin().openSettings(null)
.assertMenuContains("General")
.assertSettingDisplayed("sonar.newKey")
.assertSettingNotDisplayed("sonar.deprecatedKey");
}

}

+ 0
- 72
it/it-tests/src/test/java/it/settings/SubCategoriesTest.java View File

@@ -1,72 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
package it.settings;

import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.SonarScanner;
import com.sonar.orchestrator.selenium.Selenese;
import it.Category1Suite;
import org.junit.ClassRule;
import org.junit.Test;
import org.sonar.wsclient.services.PropertyQuery;
import util.selenium.SeleneseTest;

import static org.assertj.core.api.Assertions.assertThat;
import static util.ItUtils.projectDir;

public class SubCategoriesTest {

@ClassRule
public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;

/**
* SONAR-3159
*/
@Test
public void should_support_global_subcategories() {
Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("subcategories",
"/settings/subcategories/global-subcategories.html",
// SONAR-4495
"/settings/subcategories/global-subcategories-no-default.html"
).build();
new SeleneseTest(selenese).runOn(orchestrator);
assertThat(getProperty("prop3", null)).isEqualTo("myValue");
}

/**
* SONAR-3159
*/
@Test
public void should_support_project_subcategories() {
orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));

Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("subcategories",
"/settings/subcategories/project-subcategories.html",
// SONAR-4495
"/settings/subcategories/project-subcategories-no-default.html"
).build();
new SeleneseTest(selenese).runOn(orchestrator);
assertThat(getProperty("prop3", "sample")).isEqualTo("myValue2");
}

static String getProperty(String key, String resourceKeyOrId) {
return orchestrator.getServer().getAdminWsClient().find(new PropertyQuery().setKey(key).setResourceKeyOrId(resourceKeyOrId)).getValue();
}
}

+ 4
- 0
it/it-tests/src/test/java/pageobjects/LoginPage.java View File

@@ -36,6 +36,10 @@ public class LoginPage {
return submitCredentials(login, password, Navigation.class);
}

public Navigation asAdmin() {
return submitCredentials("admin", "admin");
}

public LoginPage submitWrongCredentials(String login, String password) {
$("#login").val(login);
$("#password").val(password);

+ 11
- 0
it/it-tests/src/test/java/pageobjects/Navigation.java View File

@@ -24,8 +24,12 @@ import com.codeborne.selenide.Selenide;
import com.codeborne.selenide.SelenideElement;
import com.codeborne.selenide.WebDriverRunner;
import com.sonar.orchestrator.Orchestrator;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import javax.annotation.Nullable;
import org.junit.rules.ExternalResource;
import org.openqa.selenium.By;
import pageobjects.settings.SettingsPage;

import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.page;
@@ -84,6 +88,13 @@ public class Navigation extends ExternalResource {
return open("/background_tasks", BackgroundTasksPage.class);
}

public SettingsPage openSettings(@Nullable String projectKey) throws UnsupportedEncodingException {
String url = projectKey != null ?
"/project/settings?id=" + URLEncoder.encode(projectKey, "UTF-8") :
"/settings";
return open(url, SettingsPage.class);
}

public void open(String relativeUrl) {
Selenide.open(relativeUrl);
}

+ 48
- 0
it/it-tests/src/test/java/pageobjects/settings/PropertySetInput.java View File

@@ -0,0 +1,48 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
package pageobjects.settings;

import com.codeborne.selenide.SelenideElement;

import static com.codeborne.selenide.Condition.exist;

public class PropertySetInput {

private final SelenideElement elt;

public PropertySetInput(SelenideElement elt) {
this.elt = elt;
}

public PropertySetInput setFieldValue(int index, String fieldKey, String value) {
elt.findAll("input[name$=\"[" + fieldKey + "]\"]").get(index).val(value);
return this;
}

public PropertySetInput setFieldValue(String fieldKey, String value) {
return setFieldValue(0, fieldKey, value);
}

public PropertySetInput save() {
elt.find(".js-save-changes").click();
elt.find(".js-save-changes").shouldNot(exist);
return this;
}
}

+ 84
- 0
it/it-tests/src/test/java/pageobjects/settings/SettingsPage.java View File

@@ -0,0 +1,84 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
package pageobjects.settings;

import com.codeborne.selenide.SelenideElement;
import org.openqa.selenium.By;

import static com.codeborne.selenide.Condition.cssClass;
import static com.codeborne.selenide.Condition.exactValue;
import static com.codeborne.selenide.Condition.exist;
import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$;

public class SettingsPage {

public SettingsPage() {
$("#settings-page").shouldBe(visible);
}

public SettingsPage assertMenuContains(String categoryName) {
$(".settings-menu").$(By.linkText(categoryName)).shouldBe(visible);
return this;
}

public SettingsPage assertSettingDisplayed(String settingKey) {
$(".settings-definition[data-key='" + settingKey + "']").shouldBe(visible);
return this;
}

public SettingsPage assertSettingNotDisplayed(String settingKey) {
$(".settings-definition[data-key='" + settingKey + "']").shouldNotBe(visible);
return this;
}

public SettingsPage openCategory(String categoryName) {
$(".settings-menu").$(By.linkText(categoryName)).click();
return this;
}

public SettingsPage assertStringSettingValue(String settingKey, String value) {
$("input[name=\"settings[" + settingKey + "]\"]").shouldHave(exactValue(value));
return this;
}

public SettingsPage assertBooleanSettingValue(String settingKey, boolean value) {
SelenideElement toggle = $("button[name=\"settings[" + settingKey + "]\"]");
if (value) {
toggle.shouldHave(cssClass("boolean-toggle-on"));
} else {
toggle.shouldNotHave(cssClass("boolean-toggle-on"));
}
return this;
}

public SettingsPage setStringValue(String settingKey, String value) {
SelenideElement setting = $(".settings-definition[data-key=\"" + settingKey + "\"]");
setting.find("input").val(value);
setting.find(".js-save-changes").click();
setting.find(".js-save-changes").shouldNot(exist);
return this;
}

public PropertySetInput getPropertySetInput(String settingKey) {
SelenideElement setting = $(".settings-definition[data-key=\"" + settingKey + "\"]");
return new PropertySetInput(setting);
}
}

+ 0
- 89
it/it-tests/src/test/resources/componentDashboard/DashboardTest/global_dashboard/global-admin-dashboards.html View File

@@ -1,89 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>global-admin-dashboards</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">global-admin-dashboards</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/sessions/login</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>link=Home</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Home</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Configure widgets</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Back to dashboard</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/dashboards</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings/index</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>link=Configuration</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>link=Configuration</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Default Dashboards</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 49
it/it-tests/src/test/resources/measureHistory/DifferentialPeriodsTest/define-leak-period-on-project.html View File

@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>display-added-files</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">should_display_added_files_in_differential_drilldown</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/sessions/logout</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/project/settings?id=sample&amp;category=general&amp;subcategory=differentialviews</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>name=commit</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>properties</td>
<td>*Leak Period*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 49
it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/module-settings/display-module-settings.html View File

@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>override-global-settings</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">override-global-settings</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/sessions/logout</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/project/settings/com.sonarsource.it.samples%3Amulti-modules-sample%3Amodule_a</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>name=commit</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>plugins</td>
<td>*Settings*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 64
it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-settings/only-on-project-settings.html View File

@@ -1,64 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>override-global-settings</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">override-global-settings</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/sessions/logout</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/project/settings/sample?category=project-only</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>name=commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=input_prop_only_on_project</td>
<td>foo</td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>id=input_prop_only_on_project</td>
<td>foo</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 64
it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-settings/override-global-settings.html View File

@@ -1,64 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>override-global-settings</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">override-global-settings</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/sessions/logout</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/project/settings/sample?category=exclusions&amp;subcategory=files</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>name=commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=input_sonar.exclusions</td>
<td>my-exclusions</td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>id=input_sonar.exclusions</td>
<td>my-exclusions</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 1
- 16
it/it-tests/src/test/resources/serverSystem/ServerSystemTest/missing_ip.html View File

@@ -60,22 +60,7 @@
</tr>
<tr>
<td>open</td>
<td>/settings/index</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Licenses</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Server ID</td>
<td></td>
</tr>
<tr>
<td>selectFrame</td>
<td>settings_iframe</td>
<td>/server_id_configuration</td>
<td></td>
</tr>
<tr>

+ 1
- 16
it/it-tests/src/test/resources/serverSystem/ServerSystemTest/organisation_must_not_accept_special_chars.html View File

@@ -35,22 +35,7 @@
</tr>
<tr>
<td>open</td>
<td>/settings/index</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Licenses</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Server ID</td>
<td></td>
</tr>
<tr>
<td>selectFrame</td>
<td>settings_iframe</td>
<td>/server_id_configuration</td>
<td></td>
</tr>
<tr>

+ 1
- 21
it/it-tests/src/test/resources/serverSystem/ServerSystemTest/valid_id.html View File

@@ -40,27 +40,7 @@
</tr>
<tr>
<td>open</td>
<td>/settings/index</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Licenses</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Server ID</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>settings_iframe</td>
<td></td>
</tr>
<tr>
<td>selectFrame</td>
<td>settings_iframe</td>
<td>/server_id_configuration</td>
<td></td>
</tr>
<tr>

+ 0
- 84
it/it-tests/src/test/resources/settings/PropertySetsTest/auto-generated/create.html View File

@@ -1,84 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>create</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=DEV</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>1</td>
</tr>
<tr>
<td>type</td>
<td>id=input_value</td>
<td>FIRST</td>
</tr>
<tr>
<td>click</td>
<td>css=button.add_value</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>xpath=(//input[@id='input_value'])[2]</td>
<td>SECOND</td>
</tr>
<tr>
<td>click</td>
<td>css=button.add_value</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>xpath=(//input[@id='input_value'])[3]</td>
<td>THIRD</td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>2</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 69
it/it-tests/src/test/resources/settings/PropertySetsTest/auto-generated/update.html View File

@@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>update</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=DEV</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>1</td>
</tr>
<tr>
<td>click</td>
<td>link=Delete</td>
<td></td>
</tr>
<tr>
<td>waitForVisible</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>2</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 139
it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/all_types.html View File

@@ -1,139 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>all_types</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=DEV</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>1</td>
</tr>
<tr>
<td>type</td>
<td>id=input_text</td>
<td>text</td>
</tr>
<tr>
<td>select</td>
<td>id=input_boolean</td>
<td>label=True</td>
</tr>
<tr>
<td>type</td>
<td>id=input_float</td>
<td>42.0</td>
</tr>
<tr>
<td>type</td>
<td>id=input_license</td>
<td>abc</td>
</tr>
<tr>
<td>select</td>
<td>id=input_metric</td>
<td>label=Overall Condition Coverage</td>
</tr>
<tr>
<td>type</td>
<td>id=input_password</td>
<td>abcde</td>
</tr>
<tr>
<td>type</td>
<td>id=input_regexp</td>
<td>.*</td>
</tr>
<tr>
<td>select</td>
<td>id=input_list</td>
<td>label=AAA</td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>2</td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_text</td>
<td>text</td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_boolean</td>
<td>true</td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_float</td>
<td>42.0</td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_license</td>
<td>abc</td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_metric</td>
<td>overall_branch_coverage</td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_password</td>
<td>{{*******************}}</td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_regexp</td>
<td>exact:.*</td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_list</td>
<td>AAA</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 89
it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/create.html View File

@@ -1,89 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>create</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=DEV</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>1</td>
</tr>
<tr>
<td>type</td>
<td>id=input_key</td>
<td>jira1</td>
</tr>
<tr>
<td>type</td>
<td>id=input_url</td>
<td>http://jira</td>
</tr>
<tr>
<td>type</td>
<td>id=input_port</td>
<td>12345</td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>2</td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_key</td>
<td>jira1</td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_url</td>
<td>exact:http://jira</td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_port</td>
<td>12345</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 119
it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/delete.html View File

@@ -1,119 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>delete</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=DEV</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>1</td>
</tr>
<tr>
<td>type</td>
<td>id=input_key</td>
<td>jira1</td>
</tr>
<tr>
<td>type</td>
<td>id=input_url</td>
<td>http://jira1</td>
</tr>
<tr>
<td>click</td>
<td>css=#block_sonar\.test\.jira\.servers .add_value</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>xpath=(//input[@id='input_key'])[2]</td>
<td>jira2</td>
</tr>
<tr>
<td>type</td>
<td>xpath=(//input[@id='input_url'])[2]</td>
<td>http://jira2</td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>2</td>
</tr>
<tr>
<td>assertValue</td>
<td>xpath=(//input[@id='input_key'])[1]</td>
<td>jira1</td>
</tr>
<tr>
<td>assertValue</td>
<td>xpath=(//input[@id='input_key'])[2]</td>
<td>jira2</td>
</tr>
<tr>
<td>click</td>
<td>xpath=(//a[contains(text(),'Delete')])[3]</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>3</td>
</tr>
<tr>
<td>assertValue</td>
<td>xpath=(//input[@id='input_key'])[1]</td>
<td>jira1</td>
</tr>
<tr>
<td>waitForNotText</td>
<td>xpath=(//input[@id='input_key'])</td>
<td>*jira2*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 94
it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/reference.html View File

@@ -1,94 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>reference</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=DEV</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>1</td>
</tr>
<tr>
<td>type</td>
<td>xpath=(//input[@id='input_key'])[1]</td>
<td>jira1</td>
</tr>
<tr>
<td>click</td>
<td>css=#block_sonar\.test\.jira\.servers .add_value</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>xpath=(//input[@id='input_key'])[2]</td>
<td>jira2</td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>2</td>
</tr>
<tr>
<td>assertSelectOptions</td>
<td>id=input_sonar.test.jira</td>
<td>Default,jira1,jira2</td>
</tr>
<tr>
<td>click</td>
<td>xpath=(//a[contains(text(),'Delete')])[2]</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>3</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 2
- 7
it/it-tests/src/test/resources/settings/SettingsTest/encrypt-text.html View File

@@ -15,7 +15,7 @@
</tr>
<tr>
<td>open</td>
<td>/settings?category=security&subcategory=encryption</td>
<td>/encryption_configuration</td>
<td></td>
</tr>
<tr>
@@ -35,12 +35,7 @@
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>selectFrame</td>
<td>settings_iframe</td>
<td>css=#submit_encrypt</td>
<td></td>
</tr>
<tr>

+ 0
- 59
it/it-tests/src/test/resources/settings/SettingsTest/general-settings.html View File

@@ -1,59 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>settings_on_core_plugins</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">settings_on_core_plugins</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/sessions/logout</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings/index</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>plugins</td>
<td>*General*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 2
- 7
it/it-tests/src/test/resources/settings/SettingsTest/generate-secret-key.html View File

@@ -15,7 +15,7 @@
</tr>
<tr>
<td>open</td>
<td>/settings?category=security&subcategory=encryption</td>
<td>/encryption_configuration</td>
<td></td>
</tr>
<tr>
@@ -35,12 +35,7 @@
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>selectFrame</td>
<td>settings_iframe</td>
<td>css=#submit_encrypt</td>
<td></td>
</tr>
<tr>

+ 0
- 55
it/it-tests/src/test/resources/settings/SettingsTest/global-extension-property.html View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>global-extension-property</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">global-extension-property</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=Settings</td>
<td></td>
</tr>
<tr>
<td>assertNotText</td>
<td>plugins</td>
<td>glob:*Hidden*</td>
</tr>

</tbody>
</table>
</body>
</html>

+ 0
- 55
it/it-tests/src/test/resources/settings/SettingsTest/hidden-extension-property.html View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>hidden-extension-property</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">hidden-extension-property</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=Settings</td>
<td></td>
</tr>
<tr>
<td>assertNotText</td>
<td>plugins</td>
<td>glob:*Hidden*</td>
</tr>

</tbody>
</table>
</body>
</html>

+ 0
- 54
it/it-tests/src/test/resources/settings/SettingsTest/hide-passwords.html View File

@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>hide-passwords</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/logout</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=Settings</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>name=commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>block_password</td>
<td>*Default*</td>
</tr>
<tr>
<td>assertNotText</td>
<td>block_password</td>
<td>*Default*sonar*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 54
it/it-tests/src/test/resources/settings/SettingsTest/property_relocation.html View File

@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>property-relocation</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/logout</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings/index?category=general</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>name=commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>properties</td>
<td>*sonar.newKey*</td>
</tr>
<tr>
<td>assertNotText</td>
<td>properties</td>
<td>*sonar.deprecatedKey*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 79
it/it-tests/src/test/resources/settings/SettingsTest/validate-property-type.html View File

@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>validate-property-type</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/logout</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=Settings</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>name=commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=input_float</td>
<td>abc</td>
</tr>
<tr>
<td>type</td>
<td>id=input_integer</td>
<td>123</td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>properties</td>
<td>*Not a floating point number*</td>
</tr>
<tr>
<td>assertValue</td>
<td>input_integer</td>
<td>123</td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=Settings</td>
<td></td>
</tr>
<tr>
<td>assertValue</td>
<td>input_float</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 60
it/it-tests/src/test/resources/settings/subcategories/global-subcategories-no-default.html View File

@@ -1,60 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>global-subcategories</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=Category 2</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>1</td>
</tr>
<!-- First subcategory should be selected by default -->
<tr>
<td>assertElementPresent</td>
<td>id=input_prop2_1</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop2_2</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 156
it/it-tests/src/test/resources/settings/subcategories/global-subcategories.html View File

@@ -1,156 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>global-subcategories</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/settings?category=Category 1</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>1</td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop1</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop2</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop3</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>id=input_prop4</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Sub category 1</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>id=input_prop1</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>id=input_prop2</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop3</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop4</td>
<td></td>
</tr>
<!-- Verify index attribute is taken into account -->
<tr>
<td>assertElementPresent</td>
<td>xpath=//.[@id='input_prop2']/following::input[@id='input_prop1']</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Sub category 2</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=input_prop3</td>
<td>myValue</td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>2</td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop1</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop2</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>id=input_prop3</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop4</td>
<td></td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_prop3</td>
<td>myValue</td>
</tr>
<!-- SONAR-4473 -->
<tr>
<td>clickAndWait</td>
<td>link=Sub category 1</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>id=input_prop1</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 60
it/it-tests/src/test/resources/settings/subcategories/project-subcategories-no-default.html View File

@@ -1,60 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>create</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/project/settings/sample?category=Category 2</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>1</td>
</tr>
<!-- First subcategory should be selected by default -->
<tr>
<td>assertElementPresent</td>
<td>id=input_prop2_1</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop2_2</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 150
it/it-tests/src/test/resources/settings/subcategories/project-subcategories.html View File

@@ -1,150 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>create</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/project/settings/sample?category=Category 1</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>1</td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop1</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop2</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop3</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>id=input_prop4</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Sub category 1</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>id=input_prop1</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>id=input_prop2</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop3</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop4</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Sub category 2</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=input_prop3</td>
<td>myValue2</td>
</tr>
<tr>
<td>click</td>
<td>id=submit_settings</td>
<td></td>
</tr>
<tr>
<td>waitForValue</td>
<td>name=page_version</td>
<td>2</td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop1</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop2</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>id=input_prop3</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=input_prop4</td>
<td></td>
</tr>
<tr>
<td>assertValue</td>
<td>id=input_prop3</td>
<td>myValue2</td>
</tr>
<!-- SONAR-4473 -->
<tr>
<td>clickAndWait</td>
<td>link=Sub category 1</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>id=input_prop1</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>

+ 1
- 6
it/it-tests/src/test/resources/updateCenter/installed-plugins.html View File

@@ -15,7 +15,7 @@
</tr>
<tr>
<td>open</td>
<td>/settings</td>
<td>/updatecenter</td>
<td></td>
</tr>
<tr>
@@ -38,11 +38,6 @@
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/updatecenter</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>

+ 2
- 7
it/it-tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html View File

@@ -14,7 +14,7 @@
</tr>
<tr>
<td>open</td>
<td>/settings/index</td>
<td>/settings</td>
<td></td>
</tr>
<tr>
@@ -54,12 +54,7 @@
</tr>
<tr>
<td>assertLocation</td>
<td>*/settings/index</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>plugins</td>
<td>*/settings</td>
<td></td>
</tr>
</tbody>

+ 1
- 0
server/sonar-web/config/webpack/webpack.config.base.js View File

@@ -45,6 +45,7 @@ module.exports = {
'projects': './src/main/js/apps/projects/app.js',
'quality-gates': './src/main/js/apps/quality-gates/app.js',
'quality-profiles': './src/main/js/apps/quality-profiles/app.js',
'settings': './src/main/js/apps/settings/app.js',
'source-viewer': './src/main/js/apps/source-viewer/app.js',
'system': './src/main/js/apps/system/app.js',
'update-center': './src/main/js/apps/update-center/app.js',

+ 1
- 5
server/sonar-web/scripts/start.js View File

@@ -121,14 +121,10 @@ function setupCompiler () {
function runDevServer (port) {
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
quiet: true,
publicPath: config.output.publicPath
}));

app.use(require('webpack-hot-middleware')(compiler, {
noInfo: true,
quiet: true
}));
app.use(require('webpack-hot-middleware')(compiler));

app.all('*', proxy(API_HOST, {
forwardPath: function (req) {

+ 69
- 0
server/sonar-web/src/main/js/api/settings.js View File

@@ -0,0 +1,69 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 omitBy from 'lodash/omitBy';
import { getJSON, post } from '../helpers/request';
import { TYPE_PROPERTY_SET } from '../apps/settings/constants';

export function getDefinitions (componentKey) {
const url = '/api/settings/list_definitions';
const data = componentKey ? { componentKey } : {};
return getJSON(url, data).then(r => r.definitions);
}

export function getValues (keys, componentKey) {
const url = '/api/settings/values';
const data = { keys };
if (componentKey) {
data.componentKey = componentKey;
}
return getJSON(url, data).then(r => r.settings);
}

export function setSettingValue (definition, value, componentKey) {
const url = '/api/settings/set';

const { key } = definition;
const data = { key };

if (definition.multiValues) {
data.values = value;
} else if (definition.type === TYPE_PROPERTY_SET) {
data.fieldValues = value
.map(fields => omitBy(fields, value => value == null))
.map(JSON.stringify);
} else {
data.value = value;
}

if (componentKey) {
data.componentKey = componentKey;
}

return post(url, data);
}

export function resetSettingValue (key, componentKey) {
const url = '/api/settings/reset';
const data = { key };
if (componentKey) {
data.componentKey = componentKey;
}
return post(url, data);
}

+ 6
- 0
server/sonar-web/src/main/js/api/users.js View File

@@ -39,3 +39,9 @@ export function getIdentityProviders () {
const url = '/api/users/identity_providers';
return getJSON(url);
}

export function searchUsers (query) {
const url = '/api/users/search';
const data = { q: query };
return getJSON(url, data);
}

+ 46
- 0
server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.js View File

@@ -0,0 +1,46 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 { expect } from 'chai';
import { getEmptyValue } from '../utils';
import { TYPE_PROPERTY_SET, TYPE_STRING, TYPE_SINGLE_SELECT_LIST, TYPE_BOOLEAN } from '../constants';

const fields = [
{ key: 'foo', type: TYPE_STRING },
{ key: 'bar', type: TYPE_SINGLE_SELECT_LIST }
];

describe('Settings :: Utils', () => {
describe('#getEmptyValue()', () => {
it('should work for property sets', () => {
const setting = { type: TYPE_PROPERTY_SET, fields };
expect(getEmptyValue(setting)).to.deep.equal([{ foo: '', bar: null }]);
});

it('should work for multi values string', () => {
const setting = { type: TYPE_STRING, multiValues: true };
expect(getEmptyValue(setting)).to.deep.equal(['']);
});

it('should work for multi values boolean', () => {
const setting = { type: TYPE_BOOLEAN, multiValues: true };
expect(getEmptyValue(setting)).to.deep.equal([null]);
});
});
});

+ 50
- 0
server/sonar-web/src/main/js/apps/settings/app.js View File

@@ -0,0 +1,50 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Router, Route, Redirect, useRouterHistory } from 'react-router';
import { createHistory } from 'history';
import App from './components/App';
import rootReducer from './store/rootReducer';
import configureStore from '../../components/store/configureStore';

window.sonarqube.appStarted.then(options => {
const el = document.querySelector(options.el);

const controller = options.component ? '/project/settings' : '/settings';
const history = useRouterHistory(createHistory)({
basename: window.baseUrl + controller
});

const store = configureStore(rootReducer);

const withComponent = ComposedComponent => props =>
<ComposedComponent {...props} component={options.component}/>;

render((
<Provider store={store}>
<Router history={history}>
<Redirect from="/index" to="/"/>
<Route path="/" component={withComponent(App)}/>
</Router>
</Provider>
), el);
});

+ 37
- 0
server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js View File

@@ -0,0 +1,37 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { connect } from 'react-redux';
import CategoriesList from './CategoriesList';
import { getAllCategories } from '../store/rootReducer';

class AllCategoriesList extends React.Component {
render () {
return <CategoriesList {...this.props}/>;
}
}

const mapStateToProps = state => ({
categories: getAllCategories(state)
});

export default connect(
mapStateToProps
)(AllCategoriesList);

+ 101
- 0
server/sonar-web/src/main/js/apps/settings/components/App.js View File

@@ -0,0 +1,101 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import shallowCompare from 'react-addons-shallow-compare';
import { connect } from 'react-redux';
import PageHeader from './PageHeader';
import CategoryDefinitionsList from './CategoryDefinitionsList';
import AllCategoriesList from './AllCategoriesList';
import GlobalMessagesContainer from './GlobalMessagesContainer';
import { fetchSettings } from '../store/actions';
import { getDefaultCategory } from '../store/rootReducer';
import '../styles.css';

class App extends React.Component {
static propTypes = {
component: React.PropTypes.object,
fetchSettings: React.PropTypes.func.isRequired,
defaultCategory: React.PropTypes.string
};

state = { loaded: false };

componentDidMount () {
document.querySelector('html').classList.add('dashboard-page');
const componentKey = this.props.component ? this.props.component.key : null;
this.props.fetchSettings(componentKey).then(() => {
this.setState({ loaded: true });
});
}

shouldComponentUpdate (nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}

componentDidUpdate (prevProps) {
if (prevProps.component !== this.props.component) {
const componentKey = this.props.component ? this.props.component.key : null;
this.props.fetchSettings(componentKey);
}
}

componentWillUnmount () {
document.querySelector('html').classList.remove('dashboard-page');
}

render () {
if (!this.state.loaded) {
return null;
}

const { query } = this.props.location;
const selectedCategory = query.category || this.props.defaultCategory;

return (
<div id="settings-page" className="page page-limited">
<PageHeader component={this.props.component}/>
<GlobalMessagesContainer/>
<div className="settings-layout">
<div className="settings-side">
<AllCategoriesList
component={this.props.component}
selectedCategory={selectedCategory}
defaultCategory={this.props.defaultCategory}/>
</div>
<div className="settings-main">
<CategoryDefinitionsList
component={this.props.component}
category={selectedCategory}/>
</div>
</div>
</div>
);
}
}

const mapStateToProps = state => ({
defaultCategory: getDefaultCategory(state)
});

export default connect(
mapStateToProps,
{ fetchSettings }
)(App);


+ 71
- 0
server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js View File

@@ -0,0 +1,71 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import shallowCompare from 'react-addons-shallow-compare';
import sortBy from 'lodash/sortBy';
import { IndexLink } from 'react-router';
import { getCategoryName } from '../utils';

export default class CategoriesList extends React.Component {
static propTypes = {
categories: React.PropTypes.array.isRequired,
selectedCategory: React.PropTypes.string.isRequired,
defaultCategory: React.PropTypes.string.isRequired
};

shouldComponentUpdate (nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}

renderLink (category) {
const query = {};

if (category.key !== this.props.defaultCategory) {
query.category = category.key.toLowerCase();
}

if (this.props.component) {
query.id = this.props.component.key;
}

const className = category.key.toLowerCase() === this.props.selectedCategory.toLowerCase() ? 'active' : '';

return (
<IndexLink to={{ pathname: '/', query }} className={className} title={category.name}>
{category.name}
</IndexLink>
);
}

render () {
const categoriesWithName = this.props.categories.map(key => ({ key, name: getCategoryName(key) }));
const sortedCategories = sortBy(categoriesWithName, category => category.name.toLowerCase());

return (
<ul className="settings-menu">
{sortedCategories.map(category => (
<li key={category.key}>
{this.renderLink(category)}
</li>
))}
</ul>
);
}
}

+ 37
- 0
server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js View File

@@ -0,0 +1,37 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { connect } from 'react-redux';
import SubCategoryDefinitionsList from './SubCategoryDefinitionsList';
import { getSettingsForCategory } from '../store/rootReducer';

class CategoryDefinitionsList extends React.Component {
render () {
return <SubCategoryDefinitionsList {...this.props}/>;
}
}

const mapStateToProps = (state, ownProps) => ({
settings: getSettingsForCategory(state, ownProps.category)
});

export default connect(
mapStateToProps
)(CategoryDefinitionsList);

+ 195
- 0
server/sonar-web/src/main/js/apps/settings/components/Definition.js View File

@@ -0,0 +1,195 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { connect } from 'react-redux';
import shallowCompare from 'react-addons-shallow-compare';
import classNames from 'classnames';
import Input from './inputs/Input';
import DefinitionDefaults from './DefinitionDefaults';
import DefinitionChanges from './DefinitionChanges';
import { getPropertyName, getPropertyDescription, isEmptyValue, getSettingValue, isDefaultOrInherited } from '../utils';
import { translateWithParameters, translate } from '../../../helpers/l10n';
import { resetValue, saveValue } from '../store/actions';
import { isLoading, getValidationMessage, getChangedValue } from '../store/rootReducer';
import { failValidation, passValidation } from '../store/settingsPage/validationMessages/actions';
import { cancelChange, changeValue } from '../store/settingsPage/changedValues/actions';

class Definition extends React.Component {
static propTypes = {
component: React.PropTypes.object,
setting: React.PropTypes.object.isRequired,
changedValue: React.PropTypes.any,
loading: React.PropTypes.bool.isRequired,
validationMessage: React.PropTypes.string,

changeValue: React.PropTypes.func.isRequired,
cancelChange: React.PropTypes.func.isRequired,
saveValue: React.PropTypes.func.isRequired,
resetValue: React.PropTypes.func.isRequired,
failValidation: React.PropTypes.func.isRequired,
passValidation: React.PropTypes.func.isRequired
};

state = {
success: false
};

componentDidMount () {
this.mounted = true;
}

shouldComponentUpdate (nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}

componentWillUnmount () {
this.mounted = false;
}

safeSetState (changes) {
if (this.mounted) {
this.setState(changes);
}
}

handleChange (value) {
clearTimeout(this.timeout);
return this.props.changeValue(this.props.setting.definition.key, value);
}

handleReset () {
const componentKey = this.props.component ? this.props.component.key : null;
const { definition } = this.props.setting;
return this.props.resetValue(definition.key, componentKey).then(() => {
this.safeSetState({ success: true });
this.timeout = setTimeout(() => this.safeSetState({ success: false }), 3000);
}).catch(() => { /* do nothing */ });
}

handleCancel () {
this.props.cancelChange(this.props.setting.definition.key);
this.props.passValidation(this.props.setting.definition.key);
}

handleSave () {
this.safeSetState({ success: false });
const { definition } = this.props.setting;
if (isEmptyValue(definition, this.props.changedValue)) {
this.props.failValidation(definition.key, translate('settings.state.value_cant_be_empty'));
return;
}

const componentKey = this.props.component ? this.props.component.key : null;
this.props.saveValue(this.props.setting.definition.key, componentKey).then(() => {
this.safeSetState({ success: true });
this.timeout = setTimeout(() => this.safeSetState({ success: false }), 3000);
}).catch(() => { /* do nothing */ });
}

render () {
const { setting, changedValue, loading } = this.props;
const { definition } = setting;
const propertyName = getPropertyName(definition);

const hasValueChanged = changedValue != null;

const className = classNames('settings-definition', {
'settings-definition-changed': hasValueChanged
});

const effectiveValue = hasValueChanged ? changedValue : getSettingValue(setting);

const isDefault = isDefaultOrInherited(setting) && !hasValueChanged;

return (
<div className={className} data-key={definition.key}>
<div className="settings-definition-left">
<h3 className="settings-definition-name" title={propertyName}>
{propertyName}
</h3>

<div className="settings-definition-description markdown small spacer-top"
dangerouslySetInnerHTML={{ __html: getPropertyDescription(definition) }}/>

<div className="settings-definition-key note little-spacer-top">
{translateWithParameters('settings.key_x', definition.key)}
</div>
</div>

<div className="settings-definition-right">
<Input setting={setting} value={effectiveValue} onChange={this.handleChange.bind(this)}/>

{!hasValueChanged && (
<DefinitionDefaults
setting={setting}
isDefault={isDefault}
onReset={() => this.handleReset()}/>
)}

{hasValueChanged && (
<DefinitionChanges
onSave={this.handleSave.bind(this)}
onCancel={this.handleCancel.bind(this)}/>
)}

<div className="settings-definition-state">
{loading && (
<span className="text-info">
<span className="settings-definition-state-icon">
<i className="spinner"/>
</span>
{translate('settings.state.saving')}
</span>
)}

{!loading && (this.props.validationMessage != null) && (
<span className="text-danger">
<span className="settings-definition-state-icon">
<i className="icon-alert-error"/>
</span>
{translateWithParameters('settings.state.validation_failed', this.props.validationMessage)}
</span>
)}

{!loading && this.state.success && (
<span className="text-success">
<span className="settings-definition-state-icon">
<i className="icon-check"/>
</span>
{translate('settings.state.saved')}
</span>
)}
</div>
</div>
</div>
);
}
}

const mapStateToProps = (state, ownProps) => ({
changedValue: getChangedValue(state, ownProps.setting.definition.key),
loading: isLoading(state, ownProps.setting.definition.key),
validationMessage: getValidationMessage(state, ownProps.setting.definition.key)
});

export default connect(
mapStateToProps,
{ changeValue, saveValue, resetValue, failValidation, passValidation, cancelChange }
)(Definition);

+ 59
- 0
server/sonar-web/src/main/js/apps/settings/components/DefinitionChanges.js View File

@@ -0,0 +1,59 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import shallowCompare from 'react-addons-shallow-compare';
import { translate } from '../../../helpers/l10n';

export default class DefinitionChanges extends React.Component {
static propTypes = {
onSave: React.PropTypes.func.isRequired,
onCancel: React.PropTypes.func.isRequired
};

shouldComponentUpdate (nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}

handleSaveClick (e) {
e.preventDefault();
e.target.blur();
this.props.onSave();
}

handleCancelChange (e) {
e.preventDefault();
e.target.blur();
this.props.onCancel();
}

render () {
return (
<div className="settings-definition-changes">
<button className="js-save-changes button-success" onClick={e => this.handleSaveClick(e)}>
{translate('save')}
</button>

<button className="js-cancel-changes big-spacer-left button-link" onClick={e => this.handleCancelChange(e)}>
{translate('cancel')}
</button>
</div>
);
}
}

+ 60
- 0
server/sonar-web/src/main/js/apps/settings/components/DefinitionDefaults.js View File

@@ -0,0 +1,60 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { getSettingValue, isEmptyValue, getDefaultValue } from '../utils';
import { translate } from '../../../helpers/l10n';

export default class DefinitionDefaults extends React.Component {
static propTypes = {
setting: React.PropTypes.object.isRequired,
isDefault: React.PropTypes.bool.isRequired,
onReset: React.PropTypes.func.isRequired
};

handleReset (e) {
e.preventDefault();
e.target.blur();
this.props.onReset();
}

render () {
const { setting, isDefault } = this.props;
const { definition } = setting;

const isExplicitlySet = !isDefault && !isEmptyValue(definition, getSettingValue(setting));

return (
<div>
{isDefault && (
<div className="spacer-top note" style={{ lineHeight: '24px' }}>
{translate('settings._default')}
</div>
)}

{isExplicitlySet && (
<div className="spacer-top nowrap">
<button onClick={e => this.handleReset(e)}>{translate('reset_verb')}</button>
<span className="spacer-left note">{translate('default')}{': '}{getDefaultValue(setting)}</span>
</div>
)}
</div>
);
}
}

+ 45
- 0
server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js View File

@@ -0,0 +1,45 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import shallowCompare from 'react-addons-shallow-compare';
import Definition from './Definition';

export default class DefinitionsList extends React.Component {
static propTypes = {
component: React.PropTypes.object,
settings: React.PropTypes.array.isRequired
};

shouldComponentUpdate (nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}

render () {
return (
<ul className="settings-definitions-list">
{this.props.settings.map(setting => (
<li key={setting.definition.key}>
<Definition component={this.props.component} setting={setting}/>
</li>
))}
</ul>
);
}
}

+ 28
- 0
server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js View File

@@ -0,0 +1,28 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 GlobalMessages from '../../../components/controls/GlobalMessages';
import { getGlobalMessages } from '../store/rootReducer';

const mapStateToProps = state => ({
messages: getGlobalMessages(state)
});

export default connect(mapStateToProps)(GlobalMessages);

+ 44
- 0
server/sonar-web/src/main/js/apps/settings/components/PageHeader.js View File

@@ -0,0 +1,44 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { translate } from '../../../helpers/l10n';

export default class PageHeader extends React.Component {
static propTypes = {
component: React.PropTypes.object
};

render () {
const title = this.props.component != null ?
translate('project_settings.page') :
translate('settings.page');

const description = this.props.component != null ?
translate('project_settings.page.description') :
translate('settings.page.description');

return (
<header className="page-header">
<h1 className="page-title">{title}</h1>
<div className="page-description">{description}</div>
</header>
);
}
}

+ 62
- 0
server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js View File

@@ -0,0 +1,62 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import shallowCompare from 'react-addons-shallow-compare';
import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';
import DefinitionsList from './DefinitionsList';
import { getSubCategoryName, getSubCategoryDescription } from '../utils';

export default class SubCategoryDefinitionsList extends React.Component {
static propTypes = {
component: React.PropTypes.object,
settings: React.PropTypes.array.isRequired
};

shouldComponentUpdate (nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}

render () {
const bySubCategory = groupBy(this.props.settings, setting => setting.definition.subCategory);
const subCategories = Object.keys(bySubCategory).map(key => ({
key,
name: getSubCategoryName(bySubCategory[key][0].definition.category, key),
description: getSubCategoryDescription(bySubCategory[key][0].definition.category, key)
}));
const sortedSubCategories = sortBy(subCategories, subCategory => subCategory.name.toLowerCase());

return (
<ul className="settings-sub-categories-list">
{sortedSubCategories.map(subCategory => (
<li key={subCategory.key}>
<h2 className="settings-sub-category-name">{subCategory.name}</h2>
{subCategory.description != null && (
<div className="settings-sub-category-description markdown">
{subCategory.description}
</div>
)}
<DefinitionsList settings={bySubCategory[subCategory.key]} component={this.props.component}/>
</li>
))}
</ul>
);
}
}

+ 51
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js View File

@@ -0,0 +1,51 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import shallowCompare from 'react-addons-shallow-compare';
import PropertySetInput from './PropertySetInput';
import MultiValueInput from './MultiValueInput';
import PrimitiveInput from './PrimitiveInput';
import { TYPE_PROPERTY_SET } from '../../constants';

export default class Input extends React.Component {
static propTypes = {
setting: React.PropTypes.object.isRequired,
value: React.PropTypes.any,
onChange: React.PropTypes.func.isRequired
};

shouldComponentUpdate (nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}

render () {
const { definition } = this.props.setting;

if (definition.multiValues) {
return <MultiValueInput {...this.props}/>;
}

if (definition.type === TYPE_PROPERTY_SET) {
return <PropertySetInput {...this.props}/>;
}

return <PrimitiveInput {...this.props}/>;
}
}

+ 48
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.js View File

@@ -0,0 +1,48 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import Toggle from '../../../../components/controls/Toggle';
import { defaultInputPropTypes } from '../../propTypes';
import { translate } from '../../../../helpers/l10n';

export default class InputForBoolean extends React.Component {
static propTypes = {
...defaultInputPropTypes,
value: React.PropTypes.oneOfType([React.PropTypes.bool, React.PropTypes.string])
};

render () {
const hasValue = this.props.value != null;
const displayedValue = hasValue ? this.props.value : false;

return (
<div className="display-inline-block text-top">
<Toggle
name={this.props.name}
value={displayedValue}
onChange={this.props.onChange}/>

{!hasValue && (
<span className="spacer-left note">{translate('settings.not_set')}</span>
)}
</div>
);
}
}

+ 29
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.js View File

@@ -0,0 +1,29 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import SimpleInput from './SimpleInput';

export default class InputForNumber extends React.Component {
render () {
return (
<SimpleInput {...this.props} className="input-small" type="text"/>
);
}
}

+ 89
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js View File

@@ -0,0 +1,89 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { translate } from '../../../../helpers/l10n';
import { defaultInputPropTypes } from '../../propTypes';

export default class InputForPassword extends React.Component {
static propTypes = defaultInputPropTypes;

state = {
changing: false
};

handleChangeClick (e) {
e.preventDefault();
e.target.blur();
this.setState({ changing: true });
}

handleCancelChangeClick (e) {
e.preventDefault();
e.target.blur();
this.setState({ changing: false });
}

handleFormSubmit (e) {
e.preventDefault();
this.props.onChange(this.refs.input.value);
this.setState({ changing: false });
}

renderInput () {
return (
<div>
<form onSubmit={e => this.handleFormSubmit(e)}>
<input className="hidden" type="password"/>
<input
ref="input"
name={this.props.name}
className="input-large text-top"
type="password"
autoFocus={true}
autoComplete={false}/>
<button className="spacer-left">{translate('set')}</button>
<a className="spacer-left" href="#" onClick={e => this.handleCancelChangeClick(e)}>
{translate('cancel')}
</a>
</form>
</div>
);
}

render () {
if (this.state.changing) {
return this.renderInput();
}

const hasValue = !!this.props.value;

return (
<div>
{hasValue && (
<i className="big-spacer-right icon-lock icon-gray"/>
)}

<button onClick={e => this.handleChangeClick(e)}>
{hasValue ? translate('change_verb') : translate('set')}
</button>
</div>
);
}
}

+ 50
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.js View File

@@ -0,0 +1,50 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import Select from 'react-select';
import { defaultInputPropTypes } from '../../propTypes';

export default class InputForSingleSelectList extends React.Component {
static propTypes = {
...defaultInputPropTypes,
options: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
};

handleInputChange (option) {
this.props.onChange(option.value);
}

render () {
const options = this.props.options.map(option => ({
label: option,
value: option
}));

return (
<Select
name={this.props.name}
className="input-large"
options={options}
clearable={false}
value={this.props.value}
onChange={option => this.handleInputChange(option)}/>
);
}
}

+ 29
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.js View File

@@ -0,0 +1,29 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import SimpleInput from './SimpleInput';

export default class InputForString extends React.Component {
render () {
return (
<SimpleInput {...this.props} className="input-large" type="text"/>
);
}
}

+ 40
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.js View File

@@ -0,0 +1,40 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { defaultInputPropTypes } from '../../propTypes';

export default class InputForText extends React.Component {
static propTypes = defaultInputPropTypes;

handleInputChange (e) {
this.props.onChange(e.target.value);
}

render () {
return (
<textarea
name={this.props.name}
className="input-super-large text-top"
rows="5"
value={this.props.value}
onChange={e => this.handleInputChange(e)}/>
);
}
}

+ 90
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js View File

@@ -0,0 +1,90 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import PrimitiveInput from './PrimitiveInput';
import { getEmptyValue } from '../../utils';

export default class MultiValueInput extends React.Component {
static propTypes = {
setting: React.PropTypes.object.isRequired,
value: React.PropTypes.array,
onChange: React.PropTypes.func.isRequired
};

ensureValue () {
return this.props.value || [];
}

handleSingleInputChange (index, value) {
const newValue = [...this.ensureValue()];
newValue.splice(index, 1, value);
this.props.onChange(newValue);
}

handleDeleteValue (e, index) {
e.preventDefault();
e.target.blur();

const newValue = [...this.ensureValue()];
newValue.splice(index, 1);
this.props.onChange(newValue);
}

prepareSetting () {
const { setting } = this.props;
const newDefinition = { ...setting.definition, multiValues: false };
return {
...setting,
definition: newDefinition,
values: undefined
};
}

renderInput (value, index, isLast) {
return (
<li key={index} className="spacer-bottom">
<PrimitiveInput
setting={this.prepareSetting()}
value={value}
onChange={this.handleSingleInputChange.bind(this, index)}/>

{!isLast && (
<div className="display-inline-block spacer-left">
<button className="js-remove-value button-clean" onClick={e => this.handleDeleteValue(e, index)}>
<i className="icon-delete"/>
</button>
</div>
)}
</li>
);
}

render () {
const displayedValue = [...this.ensureValue(), ...getEmptyValue(this.props.setting.definition)];

return (
<div>
<ul>
{displayedValue.map((value, index) => this.renderInput(value, index, index === displayedValue.length - 1))}
</ul>
</div>
);
}
}

+ 75
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.js View File

@@ -0,0 +1,75 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import InputForString from './InputForString';
import InputForText from './InputForText';
import InputForPassword from './InputForPassword';
import InputForBoolean from './InputForBoolean';
import InputForNumber from './InputForNumber';
import InputForSingleSelectList from './InputForSingleSelectList';
import { getUniqueName, isDefaultOrInherited } from '../../utils';
import * as types from '../../constants';

const typeMapping = {
[types.TYPE_STRING]: InputForString,
[types.TYPE_TEXT]: InputForText,
[types.TYPE_PASSWORD]: InputForPassword,
[types.TYPE_BOOLEAN]: InputForBoolean,
[types.TYPE_INTEGER]: InputForNumber,
[types.TYPE_LONG]: InputForNumber,
[types.TYPE_FLOAT]: InputForNumber
};

export default class PrimitiveInput extends React.Component {
static propTypes = {
setting: React.PropTypes.object.isRequired,
value: React.PropTypes.any,
onChange: React.PropTypes.func.isRequired
};

render () {
const { setting, value, onChange, ...other } = this.props;
const { definition } = setting;

const name = getUniqueName(definition);

if (definition.type === types.TYPE_SINGLE_SELECT_LIST) {
return (
<InputForSingleSelectList
name={name}
value={value}
isDefault={isDefaultOrInherited(setting)}
options={definition.options}
onChange={onChange}
{...other}/>
);
}

const InputComponent = typeMapping[definition.type] || InputForString;
return (
<InputComponent
name={name}
value={value}
isDefault={isDefaultOrInherited(setting)}
onChange={onChange}
{...other}/>
);
}
}

+ 110
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js View File

@@ -0,0 +1,110 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import PrimitiveInput from './PrimitiveInput';
import { getEmptyValue, getUniqueName } from '../../utils';

export default class PropertySetInput extends React.Component {
static propTypes = {
setting: React.PropTypes.object.isRequired,
value: React.PropTypes.array,
onChange: React.PropTypes.func.isRequired
};

ensureValue () {
return this.props.value || [];
}

getFieldName (field) {
return getUniqueName(this.props.setting.definition, field.key);
}

handleDeleteValue (e, index) {
e.preventDefault();
e.target.blur();

const newValue = [...this.ensureValue()];
newValue.splice(index, 1);
this.props.onChange(newValue);
}

handleInputChange (index, fieldKey, value) {
const emptyValue = getEmptyValue(this.props.setting.definition)[0];
const newValue = [...this.ensureValue()];
const newFields = { ...emptyValue, ...newValue[index], [fieldKey]: value };
newValue.splice(index, 1, newFields);
return this.props.onChange(newValue);
}

renderFields (fieldValues, index, isLast) {
const { setting } = this.props;

return (
<tr key={index}>
{setting.definition.fields.map(field => (
<td key={field.key}>
<PrimitiveInput
name={this.getFieldName(field)}
setting={{ definition: field, value: fieldValues[field.key] }}
value={fieldValues[field.key]}
onChange={this.handleInputChange.bind(this, index, field.key)}/>
</td>
))}
<td className="thin nowrap">
{!isLast && (
<button className="js-remove-value button-link" onClick={e => this.handleDeleteValue(e, index)}>
<i className="icon-delete"/>
</button>
)}
</td>
</tr>
);
}

render () {
const { setting } = this.props;

const displayedValue = [...this.ensureValue(), ...getEmptyValue(this.props.setting.definition)];

return (
<div>
<table className="data zebra-hover no-outer-padding" style={{ width: 'auto', minWidth: 480, marginTop: -12 }}>
<thead>
<tr>
{setting.definition.fields.map(field => (
<th key={field.key}>
{field.name}
{field.description != null && (
<span className="spacer-top small">{field.description}</span>
)}
</th>
))}
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{displayedValue.map((fieldValues, index) =>
this.renderFields(fieldValues, index, index === displayedValue.length - 1))}
</tbody>
</table>
</div>
);
}
}

+ 45
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.js View File

@@ -0,0 +1,45 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { defaultInputPropTypes } from '../../propTypes';

export default class SimpleInput extends React.Component {
static propTypes = {
...defaultInputPropTypes,
value: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]),
type: React.PropTypes.string.isRequired,
className: React.PropTypes.string.isRequired
};

handleInputChange (e) {
this.props.onChange(e.target.value);
}

render () {
return (
<input
name={this.props.name}
className={this.props.className + ' text-top'}
type={this.props.type}
value={this.props.value}
onChange={e => this.handleInputChange(e)}/>
);
}
}

+ 60
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.js View File

@@ -0,0 +1,60 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import Input from '../Input';
import PrimitiveInput from '../PrimitiveInput';
import MultiValueInput from '../MultiValueInput';
import PropertySetInput from '../PropertySetInput';
import { TYPE_STRING, TYPE_PROPERTY_SET } from '../../../constants';

describe('Settings :: Inputs :: Input', () => {
it('should render PrimitiveInput', () => {
const setting = { definition: { key: 'example', type: TYPE_STRING } };
const onChange = sinon.spy();
const input = shallow(<Input setting={setting} value="foo" onChange={onChange}/>).find(PrimitiveInput);
expect(input).to.have.length(1);
expect(input.prop('setting')).to.equal(setting);
expect(input.prop('value')).to.equal('foo');
expect(input.prop('onChange')).to.equal(onChange);
});

it('should render MultiValueInput', () => {
const setting = { definition: { key: 'example', type: TYPE_STRING, multiValues: true } };
const onChange = sinon.spy();
const input = shallow(<Input setting={setting} value="foo" onChange={onChange}/>).find(MultiValueInput);
expect(input).to.have.length(1);
expect(input.prop('setting')).to.equal(setting);
expect(input.prop('value')).to.equal('foo');
expect(input.prop('onChange')).to.equal(onChange);
});

it('should render PropertySetInput', () => {
const setting = { definition: { key: 'example', type: TYPE_PROPERTY_SET, fields: [] } };
const onChange = sinon.spy();
const input = shallow(<Input setting={setting} value="foo" onChange={onChange}/>).find(PropertySetInput);
expect(input).to.have.length(1);
expect(input.prop('setting')).to.equal(setting);
expect(input.prop('value')).to.equal('foo');
expect(input.prop('onChange')).to.equal(onChange);
});
});

+ 77
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.js View File

@@ -0,0 +1,77 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import InputForBoolean from '../InputForBoolean';
import Toggle from '../../../../../components/controls/Toggle';

describe('Settings :: Inputs :: InputForBoolean', () => {
it('should render Toggle', () => {
const onChange = sinon.spy();
const toggle = shallow(
<InputForBoolean
name="foo"
value={true}
isDefault={false}
onChange={onChange}/>
).find(Toggle);
expect(toggle).to.have.length(1);
expect(toggle.prop('name')).to.equal('foo');
expect(toggle.prop('value')).to.equal(true);
expect(toggle.prop('onChange')).to.be.a('function');
});

it('should render Toggle without value', () => {
const onChange = sinon.spy();
const input = shallow(
<InputForBoolean
name="foo"
isDefault={false}
onChange={onChange}/>
);
const toggle = input.find(Toggle);
expect(toggle).to.have.length(1);
expect(toggle.prop('name')).to.equal('foo');
expect(toggle.prop('value')).to.equal(false);
expect(toggle.prop('onChange')).to.be.a('function');
expect(input.find('.note')).to.have.length(1);
});

it('should call onChange', () => {
const onChange = sinon.spy();
const input = shallow(
<InputForBoolean
name="foo"
value={true}
isDefault={false}
onChange={onChange}/>
);
const toggle = input.find(Toggle);
expect(toggle).to.have.length(1);
expect(toggle.prop('onChange')).to.be.a('function');

toggle.prop('onChange')(false);

expect(onChange.called).to.equal(true);
expect(onChange.lastCall.args).to.deep.equal([false]);
});
});

+ 43
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.js View File

@@ -0,0 +1,43 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import InputForNumber from '../InputForNumber';
import SimpleInput from '../SimpleInput';

describe('Settings :: Inputs :: InputForNumber', () => {
it('should render SimpleInput', () => {
const onChange = sinon.spy();
const simpleInput = shallow(
<InputForNumber
name="foo"
value={17}
isDefault={false}
onChange={onChange}/>
).find(SimpleInput);
expect(simpleInput).to.have.length(1);
expect(simpleInput.prop('name')).to.equal('foo');
expect(simpleInput.prop('value')).to.equal(17);
expect(simpleInput.prop('type')).to.equal('text');
expect(simpleInput.prop('onChange')).to.be.a('function');
});
});

+ 97
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.js View File

@@ -0,0 +1,97 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { expect } from 'chai';
import { shallow, mount } from 'enzyme';
import sinon from 'sinon';
import InputForPassword from '../InputForPassword';
import { click, submit } from '../../../../../../../../tests/utils';

describe('Settings :: Inputs :: InputForPassword', () => {
it('should render lock icon, but no form', () => {
const onChange = sinon.spy();
const input = shallow(
<InputForPassword
name="foo"
value="bar"
isDefault={false}
onChange={onChange}/>
);
expect(input.find('.icon-lock')).to.have.length(1);
expect(input.find('form')).to.have.length(0);
});

it('should open form', () => {
const onChange = sinon.spy();
const input = shallow(
<InputForPassword
name="foo"
value="bar"
isDefault={false}
onChange={onChange}/>
);
const button = input.find('button');
expect(button).to.have.length(1);

click(button);
expect(input.find('form')).to.have.length(1);
});

it('should close form', () => {
const onChange = sinon.spy();
const input = shallow(
<InputForPassword
name="foo"
value="bar"
isDefault={false}
onChange={onChange}/>
);
const button = input.find('button');
expect(button).to.have.length(1);

click(button);
expect(input.find('form')).to.have.length(1);

click(input.find('form').find('a'));
expect(input.find('form')).to.have.length(0);
});

it('should set value', () => {
const onChange = sinon.stub().returns(Promise.resolve());
const input = mount(
<InputForPassword
name="foo"
value="bar"
isDefault={false}
onChange={onChange}/>
);
const button = input.find('button');
expect(button).to.have.length(1);

click(button);
const form = input.find('form');
expect(form).to.have.length(1);

input.ref('input').value = 'secret';
submit(form);

expect(onChange.called).to.equal(true);
});
});

+ 66
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.js View File

@@ -0,0 +1,66 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import Select from 'react-select';
import InputForSingleSelectList from '../InputForSingleSelectList';

describe('Settings :: Inputs :: InputForSingleSelectList', () => {
it('should render Select', () => {
const onChange = sinon.spy();
const select = shallow(
<InputForSingleSelectList
name="foo"
value="bar"
options={['foo', 'bar', 'baz']}
isDefault={false}
onChange={onChange}/>
).find(Select);
expect(select).to.have.length(1);
expect(select.prop('name')).to.equal('foo');
expect(select.prop('value')).to.equal('bar');
expect(select.prop('options')).to.deep.equal([
{ value: 'foo', label: 'foo' },
{ value: 'bar', label: 'bar' },
{ value: 'baz', label: 'baz' }
]);
expect(select.prop('onChange')).to.be.a('function');
});

it('should call onChange', () => {
const onChange = sinon.spy();
const select = shallow(
<InputForSingleSelectList
name="foo"
value="bar"
options={['foo', 'bar', 'baz']}
isDefault={false}
onChange={onChange}/>
).find(Select);
expect(select).to.have.length(1);
expect(select.prop('onChange')).to.be.a('function');

select.prop('onChange')({ value: 'baz', label: 'baz' });
expect(onChange.called).to.equal(true);
expect(onChange.lastCall.args).to.deep.equal(['baz']);
});
});

+ 43
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.js View File

@@ -0,0 +1,43 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import InputForString from '../InputForString';
import SimpleInput from '../SimpleInput';

describe('Settings :: Inputs :: InputForString', () => {
it('should render SimpleInput', () => {
const onChange = sinon.spy();
const simpleInput = shallow(
<InputForString
name="foo"
value="bar"
isDefault={false}
onChange={onChange}/>
).find(SimpleInput);
expect(simpleInput).to.have.length(1);
expect(simpleInput.prop('name')).to.equal('foo');
expect(simpleInput.prop('value')).to.equal('bar');
expect(simpleInput.prop('type')).to.equal('text');
expect(simpleInput.prop('onChange')).to.be.a('function');
});
});

+ 60
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.js View File

@@ -0,0 +1,60 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import InputForText from '../InputForText';
import { change } from '../../../../../../../../tests/utils';

describe('Settings :: Inputs :: InputForText', () => {
it('should render textarea', () => {
const onChange = sinon.spy();
const textarea = shallow(
<InputForText
name="foo"
value="bar"
isDefault={false}
onChange={onChange}/>
).find('textarea');
expect(textarea).to.have.length(1);
expect(textarea.prop('name')).to.equal('foo');
expect(textarea.prop('value')).to.equal('bar');
expect(textarea.prop('onChange')).to.be.a('function');
});

it('should call onChange', () => {
const onChange = sinon.spy();
const textarea = shallow(
<InputForText
name="foo"
value="bar"
isDefault={false}
onChange={onChange}/>
).find('textarea');
expect(textarea).to.have.length(1);
expect(textarea.prop('onChange')).to.be.a('function');

change(textarea, 'qux');

expect(onChange.called).to.equal(true);
expect(onChange.lastCall.args).to.deep.equal(['qux']);
});
});

+ 103
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.js View File

@@ -0,0 +1,103 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { expect } from 'chai';
import { shallow, mount } from 'enzyme';
import sinon from 'sinon';
import MultiValueInput from '../MultiValueInput';
import InputForString from '../InputForString';
import { click, change } from '../../../../../../../../tests/utils';

const definition = { multiValues: true };

const assertValues = (inputs, values) => {
values.forEach((value, index) => {
const input = inputs.at(index);
expect(input.prop('value')).to.equal(value);
});
};

describe('Settings :: Inputs :: MultiValueInput', () => {
it('should render one value', () => {
const multiValueInput = mount(
<MultiValueInput
setting={{ definition }}
value={['foo']}
onChange={sinon.stub()}/>
);
const stringInputs = multiValueInput.find(InputForString);
expect(stringInputs).to.have.length(1 + 1);
assertValues(stringInputs, ['foo', '']);
});

it('should render several values', () => {
const multiValueInput = mount(
<MultiValueInput
setting={{ definition }}
value={['foo', 'bar', 'baz']}
onChange={sinon.stub()}/>
);
const stringInputs = multiValueInput.find(InputForString);
expect(stringInputs).to.have.length(3 + 1);
assertValues(stringInputs, ['foo', 'bar', 'baz', '']);
});

it('should remove value', () => {
const onChange = sinon.spy();
const multiValueInput = mount(
<MultiValueInput
setting={{ definition }}
value={['foo', 'bar', 'baz']}
onChange={onChange}/>
);

click(multiValueInput.find('.js-remove-value').at(1));
expect(onChange.called).to.equal(true);
expect(onChange.lastCall.args).to.deep.equal([['foo', 'baz']]);
});

it('should change existing value', () => {
const onChange = sinon.spy();
const multiValueInput = mount(
<MultiValueInput
setting={{ definition }}
value={['foo', 'bar', 'baz']}
onChange={onChange}/>
);

change(multiValueInput.find(InputForString).at(1).find('input'), 'qux');
expect(onChange.called).to.equal(true);
expect(onChange.lastCall.args).to.deep.equal([['foo', 'qux', 'baz']]);
});

it('should add new value', () => {
const onChange = sinon.spy();
const multiValueInput = mount(
<MultiValueInput
setting={{ definition }}
value={['foo']}
onChange={onChange}/>
);

change(multiValueInput.find(InputForString).at(1).find('input'), 'bar');
expect(onChange.called).to.equal(true);
expect(onChange.lastCall.args).to.deep.equal([['foo', 'bar']]);
});
});

+ 66
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.js View File

@@ -0,0 +1,66 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import SimpleInput from '../SimpleInput';
import { change } from '../../../../../../../../tests/utils';

describe('Settings :: Inputs :: SimpleInput', () => {
it('should render input', () => {
const onChange = sinon.spy();
const input = shallow(
<SimpleInput
type="text"
className="input-large"
name="foo"
value="bar"
isDefault={false}
onChange={onChange}/>
).find('input');
expect(input).to.have.length(1);
expect(input.prop('type')).to.equal('text');
expect(input.prop('className')).to.include('input-large');
expect(input.prop('name')).to.equal('foo');
expect(input.prop('value')).to.equal('bar');
expect(input.prop('onChange')).to.be.a('function');
});

it('should call onChange', () => {
const onChange = sinon.spy();
const input = shallow(
<SimpleInput
type="text"
className="input-large"
name="foo"
value="bar"
isDefault={false}
onChange={onChange}/>
).find('input');
expect(input).to.have.length(1);
expect(input.prop('onChange')).to.be.a('function');

change(input, 'qux');

expect(onChange.called).to.equal(true);
expect(onChange.lastCall.args).to.deep.equal(['qux']);
});
});

+ 28
- 0
server/sonar-web/src/main/js/apps/settings/constants.js View File

@@ -0,0 +1,28 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
export const TYPE_STRING = 'STRING';
export const TYPE_TEXT = 'TEXT';
export const TYPE_PASSWORD = 'PASSWORD';
export const TYPE_BOOLEAN = 'BOOLEAN';
export const TYPE_FLOAT = 'FLOAT';
export const TYPE_INTEGER = 'INTEGER';
export const TYPE_LONG = 'LONG';
export const TYPE_SINGLE_SELECT_LIST = 'SINGLE_SELECT_LIST';
export const TYPE_PROPERTY_SET = 'PROPERTY_SET';

+ 27
- 0
server/sonar-web/src/main/js/apps/settings/propTypes.js View File

@@ -0,0 +1,27 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 { PropTypes } from 'react';

export const defaultInputPropTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.any,
isDefault: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired
};

+ 85
- 0
server/sonar-web/src/main/js/apps/settings/store/actions.js View File

@@ -0,0 +1,85 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 { getDefinitions, getValues, setSettingValue, resetSettingValue } from '../../../api/settings';
import { receiveValues } from './values/actions';
import { receiveDefinitions } from './definitions/actions';
import { startLoading, stopLoading } from './settingsPage/loading/actions';
import { parseError } from '../../code/utils';
import { addGlobalErrorMessage, closeAllGlobalMessages } from '../../../components/store/globalMessages';
import { passValidation, failValidation } from './settingsPage/validationMessages/actions';
import { cancelChange } from './settingsPage/changedValues/actions';
import { getDefinition, getChangedValue } from './rootReducer';

export const fetchSettings = componentKey => dispatch => {
return getDefinitions(componentKey)
.then(definitions => {
dispatch(receiveDefinitions(definitions));
const keys = definitions.map(definition => definition.key).join();
return getValues(keys, componentKey);
})
.then(settings => {
dispatch(receiveValues(settings));
dispatch(closeAllGlobalMessages());
})
.catch(e => parseError(e).then(message => dispatch(addGlobalErrorMessage(message))));
};

export const saveValue = (key, componentKey) => (dispatch, getState) => {
dispatch(startLoading(key));

const state = getState();
const definition = getDefinition(state, key);
const value = getChangedValue(state, key);

return setSettingValue(definition, value, componentKey)
.then(() => getValues(key, componentKey))
.then(values => {
dispatch(receiveValues(values));
dispatch(cancelChange(key));
dispatch(passValidation(key));
dispatch(stopLoading(key));
})
.catch(e => {
dispatch(stopLoading(key));
parseError(e).then(message => dispatch(failValidation(key, message)));
return Promise.reject();
});
};

export const resetValue = (key, componentKey) => dispatch => {
dispatch(startLoading(key));

return resetSettingValue(key, componentKey)
.then(() => getValues(key, componentKey))
.then(values => {
if (values.length > 0) {
dispatch(receiveValues(values));
} else {
dispatch(receiveValues([{ key }]));
}
dispatch(passValidation(key));
dispatch(stopLoading(key));
})
.catch(e => {
dispatch(stopLoading(key));
parseError(e).then(message => dispatch(failValidation(key, message)));
return Promise.reject();
});
};

+ 30
- 0
server/sonar-web/src/main/js/apps/settings/store/definitions/actions.js View File

@@ -0,0 +1,30 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
export const RECEIVE_DEFINITIONS = 'RECEIVE_DEFINITIONS';

/**
* Receive definitions action creator
* @param {Array} definitions
* @returns {Object}
*/
export const receiveDefinitions = definitions => ({
type: RECEIVE_DEFINITIONS,
definitions
});

+ 56
- 0
server/sonar-web/src/main/js/apps/settings/store/definitions/reducer.js View File

@@ -0,0 +1,56 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 from 'lodash/keyBy';
import sortBy from 'lodash/sortBy';
import uniqBy from 'lodash/uniqBy';
import { RECEIVE_DEFINITIONS } from './actions';
import { DEFAULT_CATEGORY, getCategoryName } from '../../utils';

const reducer = (state = {}, action = {}) => {
if (action.type === RECEIVE_DEFINITIONS) {
const definitionsByKey = keyBy(action.definitions, 'key');
return { ...state, ...definitionsByKey };
}

return state;
};

export default reducer;

export const getDefinition = (state, key) => state[key];

export const getAllDefinitions = state => Object.values(state);

export const getDefinitionsForCategory = (state, category) =>
getAllDefinitions(state).filter(definition => definition.category.toLowerCase() === category.toLowerCase());

export const getAllCategories = state => uniqBy(
getAllDefinitions(state).map(definition => definition.category),
category => category.toLowerCase());

export const getDefaultCategory = state => {
const categories = getAllCategories(state);
if (categories.includes(DEFAULT_CATEGORY)) {
return DEFAULT_CATEGORY;
} else {
const sortedCategories = sortBy(categories, category => getCategoryName(category).toLowerCase());
return sortedCategories[0];
}
};

+ 55
- 0
server/sonar-web/src/main/js/apps/settings/store/rootReducer.js View File

@@ -0,0 +1,55 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 definitions, * as fromDefinitions from './definitions/reducer';
import values, * as fromValues from './values/reducer';
import settingsPage, * as fromSettingsPage from './settingsPage/reducer';
import globalMessages, * as fromGlobalMessages from '../../../components/store/globalMessages';

const rootReducer = combineReducers({
definitions,
values,
settingsPage,
globalMessages
});

export default rootReducer;

export const getDefinition = (state, key) => fromDefinitions.getDefinition(state.definitions, key);

export const getAllCategories = state => fromDefinitions.getAllCategories(state.definitions);

export const getDefaultCategory = state => fromDefinitions.getDefaultCategory(state.definitions);

export const getValue = (state, key) => fromValues.getValue(state.values, key);

export const getSettingsForCategory = (state, category) =>
fromDefinitions.getDefinitionsForCategory(state.definitions, category).map(definition => ({
...getValue(state, definition.key),
definition
}));

export const getChangedValue = (state, key) => fromSettingsPage.getChangedValue(state.settingsPage, key);

export const isLoading = (state, key) => fromSettingsPage.isLoading(state.settingsPage, key);

export const getValidationMessage = (state, key) => fromSettingsPage.getValidationMessage(state.settingsPage, key);

export const getGlobalMessages = state => fromGlobalMessages.getGlobalMessages(state.globalMessages);

+ 33
- 0
server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/actions.js View File

@@ -0,0 +1,33 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
export const CHANGE_VALUE = 'settingsPage/CHANGE_VALUE';

export const changeValue = (key, value) => ({
type: CHANGE_VALUE,
key,
value
});

export const CANCEL_CHANGE = 'settingsPage/CANCEL_CHANGE';

export const cancelChange = key => ({
type: CANCEL_CHANGE,
key
});

+ 37
- 0
server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/reducer.js View File

@@ -0,0 +1,37 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 omit from 'lodash/omit';
import { CHANGE_VALUE, CANCEL_CHANGE } from './actions';

const reducer = (state = {}, action = {}) => {
if (action.type === CHANGE_VALUE) {
return { ...state, [action.key]: action.value };
}

if (action.type === CANCEL_CHANGE) {
return omit(state, action.key);
}

return state;
};

export default reducer;

export const getChangedValue = (state, key) => state[key];

+ 31
- 0
server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/actions.js View File

@@ -0,0 +1,31 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
export const START_LOADING = 'settingsPage/START_LOADING';

export const startLoading = key => ({
type: START_LOADING,
key
});
export const STOP_LOADING = 'settingsPage/STOP_LOADING';

export const stopLoading = key => ({
type: STOP_LOADING,
key
});

+ 36
- 0
server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/reducer.js View File

@@ -0,0 +1,36 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 { START_LOADING, STOP_LOADING } from './actions';

const reducer = (state = {}, action = {}) => {
if (action.type === START_LOADING) {
return { ...state, [action.key]: true };
}

if (action.type === STOP_LOADING) {
return { ...state, [action.key]: false };
}

return state;
};

export default reducer;

export const isLoading = (state, key) => !!state[key];

+ 38
- 0
server/sonar-web/src/main/js/apps/settings/store/settingsPage/reducer.js View File

@@ -0,0 +1,38 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 changedValues, * as fromChangedValues from './changedValues/reducer';
import validationMessages, * as fromValidationMessages from './validationMessages/reducer';
import loading, * as fromLoading from './loading/reducer';

export default combineReducers({
changedValues,
validationMessages,
loading
});

export const getChangedValue = (state, key) =>
fromChangedValues.getChangedValue(state.changedValues, key);

export const getValidationMessage = (state, key) =>
fromValidationMessages.getValidationMessage(state.validationMessages, key);

export const isLoading = (state, key) =>
fromLoading.isLoading(state.loading, key);

+ 33
- 0
server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/actions.js View File

@@ -0,0 +1,33 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
export const FAIL_VALIDATION = 'settingsPage/FAIL_VALIDATION';

export const failValidation = (key, message) => ({
type: FAIL_VALIDATION,
key,
message
});

export const PASS_VALIDATION = 'settingsPage/PASS_VALIDATION';

export const passValidation = key => ({
type: PASS_VALIDATION,
key
});

+ 36
- 0
server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/reducer.js View File

@@ -0,0 +1,36 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 { FAIL_VALIDATION, PASS_VALIDATION } from './actions';

const reducer = (state = {}, action = {}) => {
if (action.type === FAIL_VALIDATION) {
return { ...state, [action.key]: action.message };
}

if (action.type === PASS_VALIDATION) {
return { ...state, [action.key]: null };
}

return state;
};

export default reducer;

export const getValidationMessage = (state, key) => state[key];

+ 30
- 0
server/sonar-web/src/main/js/apps/settings/store/values/actions.js View File

@@ -0,0 +1,30 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
export const RECEIVE_VALUES = 'RECEIVE_VALUES';

/**
* Receive settings action creator
* @param {Array} settings
* @returns {Object}
*/
export const receiveValues = settings => ({
type: RECEIVE_VALUES,
settings
});

+ 34
- 0
server/sonar-web/src/main/js/apps/settings/store/values/reducer.js View File

@@ -0,0 +1,34 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 from 'lodash/keyBy';
import { RECEIVE_VALUES } from './actions';

const reducer = (state = {}, action = {}) => {
if (action.type === RECEIVE_VALUES) {
const settingsByKey = keyBy(action.settings, 'key');
return { ...state, ...settingsByKey };
}

return state;
};

export default reducer;

export const getValue = (state, key) => state[key];

+ 157
- 0
server/sonar-web/src/main/js/apps/settings/styles.css View File

@@ -0,0 +1,157 @@
.settings-layout {
display: flex;
justify-content: space-between;
align-items: stretch;
margin-bottom: 60px;
}

.settings-main {
position: relative;
z-index: 2;
flex-grow: 1;
padding: 15px 20px;
border: 1px solid #e6e6e6;
box-sizing: border-box;
background-color: #fff;
}

.settings-side {
position: relative;
z-index: 3;
width: 160px;
flex-shrink: 0;
padding: 10px 0;
box-sizing: border-box;
transform: translateX(1px);
}

.settings-menu {
}

.settings-menu > li {
margin-bottom: 4px;
}

.settings-menu > li > a {
display: block;
padding: 10px 10px;
line-height: 1.5;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
border: 1px solid #e6e6e6;
border-right: none;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.3s ease, background-color 0.3s ease;
}

.settings-menu > li > a:hover,
.settings-menu > li > a:focus,
.settings-menu > li > a.active {
background-color: #fff;
}

.settings-menu > li > a.active {
color: #444;
cursor: default;
}

.settings-definitions-list > li + li {
margin-top: 30px;
}

.settings-definition {
display: flex;
align-items: stretch;
}

.settings-definition-changed {
margin: -10px -20px;
padding: 9px 20px;
border-top: 1px solid #faebcc;
border-bottom: 1px solid #faebcc;
background-color: #fcf8e3;
}

.settings-definition-left {
width: 330px;
padding-right: 30px;
box-sizing: border-box;
}

.settings-definition-right {
position: relative;
width: calc(100% - 330px);
padding-top: 35px;
box-sizing: border-box;
}

.settings-definition-name {
text-overflow: ellipsis;
}

.settings-definition-key {
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.settings-definition-key:hover {
overflow: visible;
}

.settings-definition-state {
position: absolute;
top: 0;
left: 0;
right: 0;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.settings-definition-state-icon {
display: inline-block;
vertical-align: middle;
width: 24px;
height: 24px;
margin-right: 8px;
text-align: center;
}

.settings-definition-state-icon > .icon-alert-error,
.settings-definition-state-icon > .spinner {
position: relative;
top: -2px;
}

.settings-definition-changes {
margin-top: 20px;
padding-top: 20px;
border-top: 1px dotted #e6e6e6;
}

.settings-sub-categories-list {
}

.settings-sub-categories-list > li {
}

.settings-sub-categories-list > li + li {
margin: 30px -20px 0;
padding: 30px 20px;
border-top: 1px solid #e6e6e6;
}

.settings-sub-category-name {
margin-bottom: 20px;
font-size: 16px;
}

.settings-sub-category-description {
margin-top: -15px;
margin-bottom: 20px;
color: #777;
}

+ 140
- 0
server/sonar-web/src/main/js/apps/settings/utils.js View File

@@ -0,0 +1,140 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 { translate, hasMessage } from '../../helpers/l10n';
import { TYPE_PROPERTY_SET, TYPE_BOOLEAN, TYPE_SINGLE_SELECT_LIST, TYPE_PASSWORD } from './constants';

export const DEFAULT_CATEGORY = 'general';

export function getPropertyName (definition) {
const key = `property.${definition.key}.name`;
return hasMessage(key) ? translate(key) : definition.name;
}

export function getPropertyDescription (definition) {
const key = `property.${definition.key}.description`;
return hasMessage(key) ? translate(key) : definition.description;
}

export function getCategoryName (category) {
const key = `property.category.${category}`;
return hasMessage(key) ? translate(key) : category;
}

export function getSubCategoryName (category, subCategory) {
const key = `property.category.${category}.${subCategory}`;
return hasMessage(key) ? translate(key) : getCategoryName(subCategory);
}

export function getSubCategoryDescription (category, subCategory) {
const key = `property.category.${category}.${subCategory}.description`;
return hasMessage(key) ? translate(key) : null;
}

export function getUniqueName (definition, index = null) {
const indexSuffix = index != null ? `[${index}]` : '';
return `settings[${definition.key}]${indexSuffix}`;
}

export function getSettingValue (setting) {
if (setting.definition.multiValues) {
return setting.values;
} else if (setting.definition.type === TYPE_PROPERTY_SET) {
return setting.fieldValues;
} else {
return setting.value;
}
}

export function isEmptyValue (definition, value) {
if (value == null) {
return true;
} else if (definition.type === TYPE_BOOLEAN) {
return false;
} else {
return value.length === 0;
}
}

export function getEmptyValue (definition) {
if (definition.multiValues) {
return [getEmptyValue({ ...definition, multiValues: false })];
}

if (definition.type === TYPE_PROPERTY_SET) {
const value = {};
definition.fields.forEach(field => value[field.key] = getEmptyValue(field));
return [value];
}

if (definition.type === TYPE_BOOLEAN || definition.type === TYPE_SINGLE_SELECT_LIST) {
return null;
}

return '';
}

export function isDefaultOrInherited (setting) {
return !!setting.default || !!setting.inherited;
}

function getParentValue (setting) {
if (setting.definition.multiValues) {
return setting.parentValues;
} else if (setting.definition.type === TYPE_PROPERTY_SET) {
return setting.parentFieldValues;
} else {
return setting.parentValue;
}
}

/**
* Get and format the default value
* @param setting
* @returns {string}
*/
export function getDefaultValue (setting) {
const parentValue = getParentValue(setting);

if (parentValue == null) {
return translate('settings.default.no_value');
}

if (setting.definition.multiValues) {
return parentValue.length > 0 ?
parentValue.join(', ') :
translate('settings.default.no_value');
}

if (setting.definition.type === TYPE_PROPERTY_SET) {
return parentValue.length > 0 ?
translate('settings.default.complex_value') :
translate('settings.default.no_value');
}

if (setting.definition.type === TYPE_PASSWORD) {
return translate('settings.default.password');
}

if (setting.definition.type === TYPE_BOOLEAN) {
return parentValue ? translate('settings.boolean.true') : translate('settings.boolean.false');
}

return parentValue;
}

+ 51
- 0
server/sonar-web/src/main/js/components/controls/Toggle.js View File

@@ -0,0 +1,51 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 React from 'react';
import classNames from 'classnames';
import './styles.css';

export default class Toggle extends React.Component {
static propTypes = {
value: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.bool]).isRequired,
name: React.PropTypes.string,
onChange: React.PropTypes.func
};

handleClick (e, value) {
e.preventDefault();
e.currentTarget.blur();
if (this.props.onChange) {
this.props.onChange(!value);
}
}

render () {
const { value } = this.props;
const booleanValue = typeof value === 'string' ? value === 'true' : value;

const className = classNames('boolean-toggle', { 'boolean-toggle-on': booleanValue });

return (
<button className={className} name={this.props.name} onClick={e => this.handleClick(e, booleanValue)}>
<div className="boolean-toggle-handle"/>
</button>
);
}
}

+ 53
- 0
server/sonar-web/src/main/js/components/controls/__tests__/Toggle-test.js View File

@@ -0,0 +1,53 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 chai, { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import React from 'react';
import Toggle from '../Toggle';

chai.use(sinonChai);

function getSample (props) {
return (
<Toggle value={true} onChange={() => true} {...props}/>);
}

function click (element) {
return element.simulate('click', {
currentTarget: { blur () {} },
preventDefault () {}
});
}

describe('Components :: Controls :: Toggle', () => {
it('should render', () => {
const Toggle = shallow(getSample());
expect(Toggle.is('button')).to.equal(true);
});

it('should call onChange', () => {
const onChange = sinon.spy();
const Toggle = shallow(getSample({ onChange }));
click(Toggle);
expect(onChange).to.have.been.calledWith(false);
});
});

+ 51
- 0
server/sonar-web/src/main/js/components/controls/styles.css View File

@@ -24,3 +24,54 @@
.date-input-control-input:focus + .date-input-control-icon path {
fill: #4b9fd5;
}

.boolean-toggle {
display: inline-block;
vertical-align: middle;
width: 48px;
height: 24px;
padding: 1px;
border: 1px solid #cdcdcd;
border-radius: 24px;
box-sizing: border-box;
background-color: #fff;
cursor: pointer;
transition: all 0.3s ease;
}

.boolean-toggle:hover {
background-color: #fff;
}

.boolean-toggle:focus {
border-color: #4b9fd5;
background-color: #f6f6f6;
}

.boolean-toggle-handle {
width: 20px;
height: 20px;
border: 1px solid #cdcdcd;
border-radius: 22px;
box-sizing: border-box;
background-color: #f6f6f6;
transition: transform 0.3s cubic-bezier(.87,-.41,.19,1.44), border 0.3s ease;
}

.boolean-toggle-on {
border-color: #236a97;
background-color: #236a97;
}

.boolean-toggle-on:hover {
background-color: #236a97;
}

.boolean-toggle-on:focus {
background-color: #236a97;
}

.boolean-toggle-on .boolean-toggle-handle {
border-color: #f6f6f6;
transform: translateX(24px);
}

+ 5
- 0
server/sonar-web/src/main/js/helpers/l10n.js View File

@@ -36,6 +36,11 @@ export function translateWithParameters (messageKey, ...parameters) {
}
}

export function hasMessage (...keys) {
const messageKey = keys.join('.');
return messages[messageKey] != null;
}

function getCurrentLocale () {
return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language;
}

+ 1
- 1
server/sonar-web/src/main/js/helpers/request.js View File

@@ -201,5 +201,5 @@ export function requestDelete (url, data) {
* @returns {Promise}
*/
export function delay (response) {
return new Promise(resolve => setTimeout(() => resolve(response), 3000));
return new Promise(resolve => setTimeout(() => resolve(response), 500));
}

+ 1
- 1
server/sonar-web/src/main/less/components/react-select.less View File

@@ -92,7 +92,7 @@
left: 0;
line-height: @formControlHeight;
padding-left: 8px;
padding-right: 8px;
padding-right: 24px;
position: absolute;
right: 0;
top: 0;

+ 0
- 0
server/sonar-web/src/main/less/init/forms.less View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save