瀏覽代碼

SONAR-5856 Rewrite Settings page (#1163)

tags/6.1-RC1
Stas Vilchik 7 年之前
父節點
當前提交
d2da7f30d5
共有 100 個文件被更改,包括 3484 次插入1948 次删除
  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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件


部分文件因文件數量過多而無法顯示

Loading…
取消
儲存