Переглянути джерело

SONAR-7919 Rewrite "Update Key" project page (#1140)

tags/6.1-RC1
Stas Vilchik 7 роки тому
джерело
коміт
c12f3d56cb
32 змінених файлів з 1263 додано та 776 видалено
  1. 0
    33
      it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java
  2. 184
    0
      it/it-tests/src/test/java/it/projectAdministration/ProjectKeyPageTest.java
  3. 6
    0
      it/it-tests/src/test/java/pageobjects/Navigation.java
  4. 103
    0
      it/it-tests/src/test/java/pageobjects/ProjectKeyPage.java
  5. 0
    104
      it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-duplicate-keys.html
  6. 0
    104
      it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-no-match.html
  7. 0
    114
      it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-success.html
  8. 0
    84
      it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-impossible.html
  9. 0
    89
      it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-success.html
  10. 26
    0
      server/sonar-web/src/main/js/api/components.js
  11. 4
    0
      server/sonar-web/src/main/js/apps/project-admin/app.js
  12. 127
    0
      server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js
  13. 75
    0
      server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js
  14. 111
    0
      server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js
  15. 53
    0
      server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js
  16. 36
    0
      server/sonar-web/src/main/js/apps/project-admin/key/Header.js
  17. 134
    0
      server/sonar-web/src/main/js/apps/project-admin/key/Key.js
  18. 76
    0
      server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js
  19. 92
    0
      server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js
  20. 30
    0
      server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs
  21. 48
    0
      server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js
  22. 28
    0
      server/sonar-web/src/main/js/apps/project-admin/store/actions.js
  23. 47
    0
      server/sonar-web/src/main/js/apps/project-admin/store/components.js
  24. 50
    0
      server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js
  25. 19
    0
      server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
  26. 1
    0
      server/sonar-web/src/main/less/style.less
  27. 0
    62
      server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb
  28. 0
    18
      server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb
  29. 0
    19
      server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb
  30. 3
    71
      server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb
  31. 0
    61
      server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb
  32. 10
    17
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 0
- 33
it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java Переглянути файл

@@ -179,39 +179,6 @@ public class ProjectAdministrationTest {
assertThat(orchestrator.getServer().getAdminWsClient().findAll(PropertyQuery.createForResource(null, "sample"))).isNotEmpty();
}

/**
* SONAR-1608
*/
@Test
public void bulk_update_project_keys() {
SonarScanner build = SonarScanner.create(projectDir("shared/xoo-multi-modules-sample"));
orchestrator.executeBuild(build);

Selenese selenese = Selenese.builder()
.setHtmlTestsInClasspath("project-bulk-update-keys",
"/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-duplicate-keys.html",
"/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-no-match.html",
"/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-success.html")
.build();
new SeleneseTest(selenese).runOn(orchestrator);
}

/**
* SONAR-1608
*/
@Test
public void fine_grain_update_project_keys() {
SonarScanner build = SonarScanner.create(projectDir("shared/xoo-multi-modules-sample"));
orchestrator.executeBuild(build);

Selenese selenese = Selenese.builder()
.setHtmlTestsInClasspath("project-fine-grained-update-keys",
"/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-impossible.html",
"/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-success.html")
.build();
new SeleneseTest(selenese).runOn(orchestrator);
}

/**
* SONAR-4060
*/

+ 184
- 0
it/it-tests/src/test/java/it/projectAdministration/ProjectKeyPageTest.java Переглянути файл

@@ -0,0 +1,184 @@
/*
* 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.projectAdministration;

import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.SonarScanner;
import it.Category1Suite;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.sonarqube.ws.client.PostRequest;
import org.sonarqube.ws.client.WsClient;
import pageobjects.Navigation;
import pageobjects.ProjectKeyPage;

import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.WebDriverRunner.url;
import static org.assertj.core.api.Assertions.assertThat;
import static util.ItUtils.newAdminWsClient;
import static util.ItUtils.projectDir;

public class ProjectKeyPageTest {

@ClassRule
public static Orchestrator ORCHESTRATOR = Category1Suite.ORCHESTRATOR;

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

private static WsClient wsClient;

@BeforeClass
public static void setUp() {
wsClient = newAdminWsClient(ORCHESTRATOR);
}

@Before
public void cleanUp() {
ORCHESTRATOR.resetData();
}

@Test
public void change_key_when_no_modules() {
createProject("sample");

ProjectKeyPage page = openPage("sample");
page.assertSimpleUpdate().trySimpleUpdate("another");

assertThat(url()).endsWith("/project/key?id=another");
}

@Test
public void fail_to_change_key_when_no_modules() {
createProject("sample");
createProject("another");

ProjectKeyPage page = openPage("sample");
page.assertSimpleUpdate().trySimpleUpdate("another");

$(".alert.alert-danger").shouldBe(visible);
assertThat(url()).endsWith("/project/key?id=sample");
}

@Test
public void change_key_of_multi_modules_project() {
analyzeProject("shared/xoo-multi-modules-sample", "sample");

ProjectKeyPage page = openPage("sample");
page.openFineGrainedUpdate().tryFineGrainedUpdate("sample", "another");

assertThat(url()).endsWith("/project/key?id=another");
}

@Test
public void fail_to_change_key_of_multi_modules_project() {
analyzeProject("shared/xoo-multi-modules-sample", "sample");
createProject("another");

ProjectKeyPage page = openPage("sample");
page.openFineGrainedUpdate().tryFineGrainedUpdate("sample", "another");

$(".alert.alert-danger").shouldBe(visible);
assertThat(url()).endsWith("/project/key?id=sample");
}

@Test
public void change_key_of_module_of_multi_modules_project() {
analyzeProject("shared/xoo-multi-modules-sample", "sample");

ProjectKeyPage page = openPage("sample");
page.openFineGrainedUpdate().tryFineGrainedUpdate("sample:module_a:module_a1", "another");

$("#update-key-confirmation-form").shouldNotBe(visible);

nav.openProjectKey("another");
assertThat(url()).endsWith("/project/key?id=another");
}

@Test
public void fail_to_change_key_of_module_of_multi_modules_project() {
analyzeProject("shared/xoo-multi-modules-sample", "sample");
createProject("another");

ProjectKeyPage page = openPage("sample");
page.openFineGrainedUpdate().tryFineGrainedUpdate("sample:module_a:module_a1", "another");

$(".alert.alert-danger").shouldBe(visible);
}

@Test
public void bulk_change() {
analyzeProject("shared/xoo-multi-modules-sample", "sample");

ProjectKeyPage page = openPage("sample");
page.assertBulkChange().simulateBulkChange("sample", "another");

$("#bulk-update-results").shouldBe(visible);
page.assertBulkChangeSimulationResult("sample", "another")
.assertBulkChangeSimulationResult("sample:module_a:module_a1", "another:module_a:module_a1");

page.confirmBulkUpdate().assertSuccessfulBulkUpdate();
}

@Test
public void fail_to_bulk_change_because_no_changed_key() {
analyzeProject("shared/xoo-multi-modules-sample", "sample");

ProjectKeyPage page = openPage("sample");
page.assertBulkChange().simulateBulkChange("random", "another");

$("#bulk-update-nothing").shouldBe(visible);
$("#bulk-update-results").shouldNotBe(visible);
}

@Test
public void fail_to_bulk_change_because_of_duplications() {
analyzeProject("shared/xoo-multi-modules-sample", "sample");

ProjectKeyPage page = openPage("sample");
page.assertBulkChange().simulateBulkChange("module_a1", "module_a2");

$("#bulk-update-duplicate").shouldBe(visible);
$("#bulk-update-results").shouldBe(visible);

page.assertBulkChangeSimulationResult("sample:module_a:module_a1", "sample:module_a:module_a2")
.assertDuplicated("sample:module_a:module_a1");
}

private ProjectKeyPage openPage(String projectKey) {
nav.logIn().submitCredentials("admin", "admin");
return nav.openProjectKey(projectKey);
}

private static void createProject(String projectKey) {
wsClient.wsConnector().call(new PostRequest("api/projects/create")
.setParam("key", projectKey)
.setParam("name", projectKey));
}

private static void analyzeProject(String path, String projectKey) {
ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir(path))
.setProjectKey(projectKey));
}
}

+ 6
- 0
it/it-tests/src/test/java/pageobjects/Navigation.java Переглянути файл

@@ -68,6 +68,12 @@ public class Navigation extends ExternalResource {
return open(url, ProjectHistoryPage.class);
}

public ProjectKeyPage openProjectKey(String projectKey) {
// TODO encode projectKey
String url = "/project/key?id=" + projectKey;
return open(url, ProjectKeyPage.class);
}

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

+ 103
- 0
it/it-tests/src/test/java/pageobjects/ProjectKeyPage.java Переглянути файл

@@ -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.
*/
package pageobjects;

import com.codeborne.selenide.SelenideElement;

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

public class ProjectKeyPage {

public ProjectKeyPage() {
$("#project-key").should(exist);
}

public ProjectKeyPage assertSimpleUpdate() {
$("#update-key-new-key").shouldBe(visible);
$("#update-key-submit").shouldBe(visible);
return this;
}

public ProjectKeyPage trySimpleUpdate(String newKey) {
$("#update-key-new-key").val(newKey);
$("#update-key-submit").click();
$("#update-key-confirm").click();
return this;
}

public ProjectKeyPage openFineGrainedUpdate() {
$("#update-key-tab-fine").click();
$("#project-key-fine-grained-update").shouldBe(visible);
return this;
}

public ProjectKeyPage tryFineGrainedUpdate(String key, String newKey) {
SelenideElement form = $(".js-fine-grained-update[data-key=\"" + key + "\"]");
form.shouldBe(visible);

form.$("input").val(newKey);
form.$("button").click();

$("#update-key-confirm").click();
return this;
}

public ProjectKeyPage assertBulkChange() {
$("#bulk-update-replace").shouldBe(visible);
$("#bulk-update-by").shouldBe(visible);
$("#bulk-update-see-results").shouldBe(visible);
return this;
}

public ProjectKeyPage simulateBulkChange(String replace, String by) {
$("#bulk-update-replace").val(replace);
$("#bulk-update-by").val(by);
$("#bulk-update-see-results").click();

$("#bulk-update-simulation").shouldBe(visible);
return this;
}

public ProjectKeyPage assertBulkChangeSimulationResult(String oldKey, String newKey) {
SelenideElement row = $("#bulk-update-results").$("[data-key=\"" + oldKey + "\"]");
row.$(".js-old-key").should(hasText(oldKey));
row.$(".js-new-key").should(hasText(newKey));
return this;
}

public ProjectKeyPage assertDuplicated(String oldKey) {
SelenideElement row = $("#bulk-update-results").$("[data-key=\"" + oldKey + "\"]");
row.$(".js-new-key").$(".badge-danger").shouldBe(visible);
return this;
}

public ProjectKeyPage confirmBulkUpdate() {
$("#bulk-update-confirm").click();
return this;
}

public ProjectKeyPage assertSuccessfulBulkUpdate() {
$("#project-key-bulk-update").$(".alert.alert-success").shouldBe(visible);
return this;
}
}

+ 0
- 104
it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-duplicate-keys.html Переглянути файл

@@ -1,104 +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>bulk-update-impossible-because-duplicate-keys</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>/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>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/dashboard/index/com.sonarsource.it.samples:multi-modules-sample</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#context-navigation .navbar-admin-link</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Update Key</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=string_to_replace</td>
<td>com.sonarsource.it.samples:multi-modules-sample:module_a </td>
</tr>
<tr>
<td>type</td>
<td>id=replacement_string</td>
<td>com.sonarsource.it.samples:multi-modules-sample:module_b </td>
</tr>
<tr>
<td>clickAndWait</td>
<td>id=bulk_update_button</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>css=#content h1</td>
<td>*Bulk update can not be performed*</td>
</tr>
<tr>
<td>assertText</td>
<td>css=#content p</td>
<td>*The replacement of &quot;com.sonarsource.it.samples:multi-modules-sample:module_a&quot; by &quot;com.sonarsource.it.samples:multi-modules-sample:module_b&quot; is impossible as it would result in duplicate keys (in red below):*</td>
</tr>
<tr>
<td>assertText</td>
<td>css=#content .data</td>
<td>*Duplicate key*</td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=bulk_update_button</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>Link=Back</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*Update Key*com.sonarsource.it.samples:multi-modules-sample*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 104
it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-no-match.html Переглянути файл

@@ -1,104 +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>bulk-update-impossible-because-no-match</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>/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>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/dashboard/index/com.sonarsource.it.samples:multi-modules-sample</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#context-navigation .navbar-admin-link</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>link=Update Key</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Update Key</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=string_to_replace</td>
<td>foo</td>
</tr>
<tr>
<td>type</td>
<td>id=replacement_string</td>
<td>org.sonar</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>id=bulk_update_button</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>css=#content h1</td>
<td>*Bulk update can not be performed*</td>
</tr>
<tr>
<td>assertText</td>
<td>css=#content</td>
<td>*Bulk update can not be performed*No key contains the string to replace (&quot;foo&quot;).*</td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>id=bulk_update_button</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>Link=Back</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*Update Key*com.sonarsource.it.samples:multi-modules-sample*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 114
it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-success.html Переглянути файл

@@ -1,114 +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>bulk-update-success</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>/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>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/dashboard/index/com.sonarsource.it.samples:multi-modules-sample</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#context-navigation .navbar-admin-link</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>link=Update Key</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Update Key</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=string_to_replace</td>
<td>com.sonarsource</td>
</tr>
<tr>
<td>type</td>
<td>id=replacement_string</td>
<td>org.sonar</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>id=bulk_update_button</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*Do you really want to perform the bulk update on project keys?*</td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*com.sonarsource.it.samples:multi-modules-sample*org.sonar.it.samples:multi-modules-sample*</td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*com.sonarsource.it.samples:multi-modules-sample:module_a*org.sonar.it.samples:multi-modules-sample:module_a*</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>id=bulk_update_button</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*The key has successfully been updated for all required resources.*</td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*org.sonar.it.samples:multi-modules-sample*</td>
</tr>
<tr>
<td>assertTextNotPresent</td>
<td>content</td>
<td>*com.sonarsource.it.samples:multi-modules-sample*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 84
it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-impossible.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>fine-grained-update-impossible</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>/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>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/dashboard/index/com.sonarsource.it.samples:multi-modules-sample</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#context-navigation .navbar-admin-link</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>link=Update Key</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Update Key</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=key_05</td>
<td>com.sonarsource.it.samples:multi-modules-sample:module_b</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>id=update_key_05</td>
<td></td>
</tr>
<tr>
<td>assertConfirmation</td>
<td>*Are you sure you want to rename &quot;com.sonarsource.it.samples:multi-modules-sample:module_*&quot;, as well as all its modules and resources?*</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>id=error</td>
<td>*&quot;com.sonarsource.it.samples:multi-modules-sample:module_*&quot; can not be renamed because &quot;com.sonarsource.it.samples:multi-modules-sample:module_b&quot; is the key of an existing resource. The update has been canceled.*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 89
it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-success.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>fine-grained-update-success</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>/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>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/dashboard/index/com.sonarsource.it.samples:multi-modules-sample</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#context-navigation .navbar-admin-link</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>link=Update Key</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Update Key</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=key_02</td>
<td>com.sonarsource.it.samples:module_c1</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>id=update_key_02</td>
<td></td>
</tr>
<tr>
<td>assertConfirmation</td>
<td>*Are you sure you want to rename "com.sonarsource.it.samples:multi-modules-sample:module_*", as well as all its modules and resources?*</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*The key has successfully been updated for all required resources.*</td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*com.sonarsource.it.samples:module_c1*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 26
- 0
server/sonar-web/src/main/js/api/components.js Переглянути файл

@@ -109,3 +109,29 @@ export function getMyProjects (data) {
const url = '/api/projects/search_my_projects';
return getJSON(url, data);
}

/**
* Change component's key
* @param {string} key
* @param {string} newKey
* @returns {Promise}
*/
export function changeKey (key, newKey) {
const url = '/api/components/update_key';
const data = { key, newKey };
return post(url, data);
}

/**
* Bulk change component's key
* @param {string} key
* @param {string} from
* @param {string} to
* @param {boolean} dryRun
* @returns {Promise}
*/
export function bulkChangeKey (key, from, to, dryRun = false) {
const url = '/api/components/bulk_update_key';
const data = { key, from, to, dryRun };
return postJSON(url, data);
}

+ 4
- 0
server/sonar-web/src/main/js/apps/project-admin/app.js Переглянути файл

@@ -26,6 +26,7 @@ import Deletion from './deletion/Deletion';
import QualityProfiles from './quality-profiles/QualityProfiles';
import QualityGate from './quality-gate/QualityGate';
import Links from './links/Links';
import Key from './key/Key';
import rootReducer from './store/rootReducer';
import configureStore from '../../components/store/configureStore';

@@ -56,6 +57,9 @@ window.sonarqube.appStarted.then(options => {
<Route
path="/links"
component={withComponent(Links)}/>
<Route
path="/key"
component={withComponent(Key)}/>
</Router>
</Provider>
), el);

+ 127
- 0
server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js Переглянути файл

@@ -0,0 +1,127 @@
/*
* 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 BulkUpdateForm from './BulkUpdateForm';
import BulkUpdateResults from './BulkUpdateResults';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { bulkChangeKey } from '../../../api/components';
import { getComponentUrl } from '../../../helpers/urls';

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

state = {
updating: false,
updated: false,
newComponentKey: null
};

handleSubmit (replace, by) {
this.loadResults(replace, by);
}

handleConfirm () {
this.setState({ updating: true });
const { component } = this.props;
const { replace, by } = this.state;
bulkChangeKey(component.key, replace, by).then(r => {
const result = r.keys.find(result => result.key === component.key);
const newComponentKey = result != null ? result.newKey : component.key;
this.setState({
updating: false,
updated: true,
newComponentKey
});
});
}

loadResults (replace, by) {
const { component } = this.props;
bulkChangeKey(component.key, replace, by, true).then(r => {
this.setState({ results: r.keys, replace, by });
});
}

renderUpdating () {
return (
<div id="project-key-bulk-update">
<i className="spinner"/>
</div>
);
}

renderUpdated () {
return (
<div id="project-key-bulk-update">
<div className="alert alert-success">
{translate('update_key.key_updated')}
{' '}
<a href={getComponentUrl(this.state.newComponentKey)}>
{translate('check_project')}
</a>
</div>
</div>
);
}

render () {
const { component } = this.props;
const { updating, updated } = this.state;
const { results, replace, by } = this.state;

if (updating) {
return this.renderUpdating();
}

if (updated) {
return this.renderUpdated();
}

return (
<div id="project-key-bulk-update">
<header className="big-spacer-bottom">
<div className="spacer-bottom">
{translate('update_key.bulk_change_description')}
</div>
<div>
{translateWithParameters(
'update_key.current_key_for_project_x_is_x',
component.name,
component.key
)}
</div>
</header>

<BulkUpdateForm onSubmit={this.handleSubmit.bind(this)}/>

{results != null && (
<BulkUpdateResults
results={results}
replace={replace}
by={by}
onConfirm={this.handleConfirm.bind(this)}/>
)}
</div>
);
}
}

+ 75
- 0
server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.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 { translate } from '../../../helpers/l10n';

export default class BulkUpdateForm extends React.Component {
static propTypes = {
onSubmit: React.PropTypes.func.isRequired
};

handleSubmit (e) {
e.preventDefault();
this.refs.submit.blur();

const replace = this.refs.replace.value;
const by = this.refs.by.value;

this.props.onSubmit(replace, by);
}

render () {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<div className="modal-field">
<label htmlFor="bulk-update-replace">
{translate('update_key.replace')}
</label>
<input
ref="replace"
id="bulk-update-replace"
name="replace"
type="text"
placeholder={translate('update_key.replace_example')}
required/>
</div>

<div className="modal-field">
<label htmlFor="bulk-update-by">
{translate('update_key.by')}
</label>
<input
ref="by"
id="bulk-update-by"
name="by"
type="text"
placeholder={translate('update_key.by_example')}
required/>
<button
ref="submit"
id="bulk-update-see-results"
className="big-spacer-left">
{translate('update_key.see_results')}
</button>
</div>
</form>
);
}
}

+ 111
- 0
server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js Переглянути файл

@@ -0,0 +1,111 @@
/*
* 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 some from 'lodash/some';
import { translateWithParameters, translate } from '../../../helpers/l10n';

export default class BulkUpdateResults extends React.Component {
static propTypes = {
results: React.PropTypes.array.isRequired,
onConfirm: React.PropTypes.func.isRequired
};

handleConfirm (e) {
e.preventDefault();
e.target.blur();
this.props.onConfirm();
}

render () {
const { results, replace, by } = this.props;
const isEmpty = results.length === 0;
const hasDuplications = some(results, r => r.duplicate);
const canUpdate = !isEmpty && !hasDuplications;

return (
<div id="bulk-update-simulation" className="big-spacer-top">
{isEmpty && (
<div id="bulk-update-nothing" className="alert alert-warning">
{translateWithParameters(
'update_key.no_key_to_update',
replace
)}
</div>
)}

{hasDuplications && (
<div id="bulk-update-duplicate" className="alert alert-danger">
{translateWithParameters(
'update_key.cant_update_because_duplicate_keys',
replace,
by
)}
</div>
)}

{canUpdate && (
<div className="spacer-bottom">
{translate('update_key.keys_will_be_updated_as_follows')}
</div>
)}

{!isEmpty && (
<table
id="bulk-update-results"
className="data zebra zebra-hover">
<thead>
<tr>
<th>{translate('update_key.old_key')}</th>
<th>{translate('update_key.new_key')}</th>
</tr>
</thead>
<tbody>
{results.map(result => (
<tr key={result.key} data-key={result.key}>
<td className="js-old-key">
{result.key}
</td>
<td className="js-new-key">
{result.duplicate && (
<span className="spacer-right badge badge-danger">
{translate('update_key.duplicate_key')}
</span>
)}
{result.newKey}
</td>
</tr>
))}
</tbody>
</table>
)}

<div className="big-spacer-top">
{canUpdate && (
<button
id="bulk-update-confirm"
onClick={this.handleConfirm.bind(this)}>
{translate('update_verb')}
</button>
)}
</div>
</div>
);
}
}

+ 53
- 0
server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.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 React from 'react';
import UpdateKeyForm from './UpdateKeyForm';
import QualifierIcon from '../../../components/shared/qualifier-icon';
import { translate } from '../../../helpers/l10n';

export default class FineGrainedUpdate extends React.Component {
render () {
const { component, modules } = this.props;
const components = [component, ...modules];

return (
<div id="project-key-fine-grained-update">
<table className="data zebra">
<tbody>
{components.map(component => (
<tr key={component.key}>
<td className="width-40">
<QualifierIcon qualifier={component.qualifier}/>
{' '}
{component.name}
</td>
<td>
<UpdateKeyForm
component={component}
onKeyChange={this.props.onKeyChange}/>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
}

+ 36
- 0
server/sonar-web/src/main/js/apps/project-admin/key/Header.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 React from 'react';
import { translate } from '../../../helpers/l10n';

export default class Header extends React.Component {
render () {
return (
<header className="page-header">
<h1 className="page-title">
{translate('update_key.page')}
</h1>
<div className="page-description">
{translate('update_key.page.description')}
</div>
</header>
);
}
}

+ 134
- 0
server/sonar-web/src/main/js/apps/project-admin/key/Key.js Переглянути файл

@@ -0,0 +1,134 @@
/*
* 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 Header from './Header';
import UpdateForm from './UpdateForm';
import BulkUpdate from './BulkUpdate';
import FineGrainedUpdate from './FineGrainedUpdate';
import { getProjectModules } from '../store/rootReducer';
import { fetchProjectModules, changeKey } from '../store/actions';
import { translate } from '../../../helpers/l10n';

class Key extends React.Component {
static propTypes = {
component: React.PropTypes.object.isRequired,
fetchProjectModules: React.PropTypes.func.isRequired,
changeKey: React.PropTypes.func.isRequired
};

state = {
tab: 'bulk'
};

componentDidMount () {
this.props.fetchProjectModules(this.props.component.key);
}

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

handleChangeKey (key, newKey) {
return this.props.changeKey(key, newKey).then(() => {
if (key === this.props.component.key) {
window.location = window.baseUrl +
'/project/key?id=' + encodeURIComponent(newKey);
}
});
}

handleChangeTab (tab, e) {
e.preventDefault();
e.target.blur();
this.setState({ tab });
}

render () {
const { component, modules } = this.props;

const noModules = modules != null && modules.length === 0;
const hasModules = modules != null && modules.length > 0;

const { tab } = this.state;

return (
<div id="project-key" className="page page-limited">
<Header/>

{modules == null && (
<i className="spinner"/>
)}

{noModules && (
<UpdateForm
component={component}
onKeyChange={this.handleChangeKey.bind(this)}/>
)}

{hasModules && (
<div>
<div className="big-spacer-bottom">
<ul className="tabs">
<li>
<a id="update-key-tab-bulk"
className={tab === 'bulk' ? 'selected' : ''}
href="#"
onClick={this.handleChangeTab.bind(this, 'bulk')}>
{translate('update_key.bulk_update')}
</a>
</li>
<li>
<a id="update-key-tab-fine"
className={tab === 'fine' ? 'selected' : ''}
href="#"
onClick={this.handleChangeTab.bind(this, 'fine')}>
{translate('update_key.fine_grained_key_update')}
</a>
</li>
</ul>
</div>

{tab === 'bulk' && (
<BulkUpdate component={component}/>
)}

{tab === 'fine' && (
<FineGrainedUpdate
component={component}
modules={modules}
onKeyChange={this.handleChangeKey.bind(this)}/>
)}
</div>
)}
</div>
);
}
}

const mapStateToProps = (state, ownProps) => ({
modules: getProjectModules(state, ownProps.component.key)
});

export default connect(
mapStateToProps,
{ fetchProjectModules, changeKey }
)(Key);

+ 76
- 0
server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js Переглянути файл

@@ -0,0 +1,76 @@
/*
* 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 UpdateKeyConfirmation from './views/UpdateKeyConfirmation';
import { translate } from '../../../helpers/l10n';

export default class UpdateForm extends React.Component {
static propTypes = {
component: React.PropTypes.object.isRequired,
onKeyChange: React.PropTypes.func.isRequired
};

state = { newKey: null };

handleSubmit (e) {
e.preventDefault();

const newKey = this.refs.newKey.value;

new UpdateKeyConfirmation({
newKey,
component: this.props.component,
onChange: this.props.onKeyChange
}).render();
}

handleChange (e) {
const newKey = e.target.value;
this.setState({ newKey });
}

render () {
const value = this.state.newKey != null ?
this.state.newKey :
this.props.component.key;

const hasChanged = value !== this.props.component.key;

return (
<form onSubmit={this.handleSubmit.bind(this)}>
<input
ref="newKey"
id="update-key-new-key"
className="input-super-large"
value={value}
type="text"
placeholder={translate('update_key.new_key')}
required
onChange={this.handleChange.bind(this)}/>

<div className="spacer-top">
<button id="update-key-submit" disabled={!hasChanged}>
{translate('update_verb')}
</button>
</div>
</form>
);
}
}

+ 92
- 0
server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js Переглянути файл

@@ -0,0 +1,92 @@
/*
* 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 UpdateKeyConfirmation from './views/UpdateKeyConfirmation';
import { translate } from '../../../helpers/l10n';

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

state = {};

componentWillMount () {
this.handleInputChange = this.handleInputChange.bind(this);
this.handleUpdateClick = this.handleUpdateClick.bind(this);
this.handleResetClick = this.handleResetClick.bind(this);
}

handleInputChange (e) {
const key = e.target.value;
this.setState({ key });
}

handleUpdateClick (e) {
e.preventDefault();
e.target.blur();

const newKey = this.refs.newKey.value;

new UpdateKeyConfirmation({
newKey,
component: this.props.component,
onChange: this.props.onKeyChange
}).render();
}

handleResetClick (e) {
e.preventDefault();
e.target.blur();
this.setState({ key: null });
}

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

const value = this.state.key != null ?
this.state.key :
component.key;

const hasChanged = this.state.key != null &&
this.state.key !== component.key;

return (
<div className="js-fine-grained-update" data-key={component.key}>
<input
ref="newKey"
className="input-super-large big-spacer-right"
type="text"
value={value}
onChange={this.handleInputChange}/>

<div className="button-group">
<button disabled={!hasChanged} onClick={this.handleUpdateClick}>
{translate('update_verb')}
</button>

<button disabled={!hasChanged} onClick={this.handleResetClick}>
{translate('reset_verb')}
</button>
</div>
</div>
);
}
}

+ 30
- 0
server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs Переглянути файл

@@ -0,0 +1,30 @@
<form id="update-key-confirmation-form" autocomplete="off">
<div class="modal-head">
<h2>{{t 'update_key.page'}}</h2>
</div>
<div class="modal-body">
<div class="js-modal-messages"></div>
{{tp 'update_key.are_you_sure_to_change_key' component.name}}
<div class="spacer-top">
<div class="display-inline-block text-right" style="width: 80px;">
{{t 'update_key.old_key'}}:
</div>
<div class="display-inline-block">
{{component.key}}
</div>
</div>
<div class="spacer-top">
<div class="display-inline-block text-right" style="width: 80px;">
{{t 'update_key.new_key'}}:
</div>
<div class="display-inline-block">
{{newKey}}
</div>
</div>
</div>
<div class="modal-foot">
<i class="js-modal-spinner spinner spacer-right hidden"></i>
<button id="update-key-confirm">{{t 'update_verb'}}</button>
<a href="#" class="js-modal-close">{{t 'cancel'}}</a>
</div>
</form>

+ 48
- 0
server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.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 ModalForm from '../../../../components/common/modal-form';
import Template from './UpdateKeyConfirmation.hbs';
import { parseError } from '../../../code/utils';

export default ModalForm.extend({
template: Template,

onFormSubmit () {
ModalForm.prototype.onFormSubmit.apply(this, arguments);
this.disableForm();
this.showSpinner();

this.options.onChange(this.options.component.key, this.options.newKey)
.then(() => this.destroy())
.catch(e => {
parseError(e).then(msg => this.showSingleError(msg));
this.hideSpinner();
this.enableForm();
});
},

serializeData () {
return {
component: this.options.component,
newKey: this.options.newKey
};
}
});


+ 28
- 0
server/sonar-web/src/main/js/apps/project-admin/store/actions.js Переглянути файл

@@ -30,6 +30,8 @@ import {
dissociateGateWithProject
} from '../../../api/quality-gates';
import { getProjectLinks, createLink } from '../../../api/projectLinks';
import { getTree } from '../../../api/components';
import { changeKey as changeKeyApi } from '../../../api/components';
import { addGlobalSuccessMessage } from '../../../components/store/globalMessages';
import { translate, translateWithParameters } from '../../../helpers/l10n';

@@ -157,3 +159,29 @@ export const deleteProjectLink = (projectKey, linkId) => ({
projectKey,
linkId
});

export const RECEIVE_PROJECT_MODULES = 'RECEIVE_PROJECT_MODULES';
const receiveProjectModules = (projectKey, modules) => ({
type: RECEIVE_PROJECT_MODULES,
projectKey,
modules
});

export const fetchProjectModules = projectKey => dispatch => {
const options = { qualifiers: 'BRC', s: 'name', ps: 500 };
getTree(projectKey, options).then(r => {
dispatch(receiveProjectModules(projectKey, r.components));
});
};

export const CHANGE_KEY = 'CHANGE_KEY';
const changeKeyAction = (key, newKey) => ({
type: CHANGE_KEY,
key,
newKey
});

export const changeKey = (key, newKey) => dispatch => {
return changeKeyApi(key, newKey)
.then(() => dispatch(changeKeyAction(key, newKey)));
};

+ 47
- 0
server/sonar-web/src/main/js/apps/project-admin/store/components.js Переглянути файл

@@ -0,0 +1,47 @@
/*
* 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 omit from 'lodash/omit';
import { RECEIVE_PROJECT_MODULES, CHANGE_KEY } from './actions';

const components = (state = {}, action = {}) => {
if (action.type === RECEIVE_PROJECT_MODULES) {
const newComponentsByKey = keyBy(action.modules, 'key');
return { ...state, ...newComponentsByKey };
}

if (action.type === CHANGE_KEY) {
const oldComponent = state[action.key];
if (oldComponent != null) {
const newComponent = { ...oldComponent, key: action.newKey };
return {
...omit(state, action.key),
[action.newKey]: newComponent
};
}
}

return state;
};

export default components;

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

+ 50
- 0
server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.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 { RECEIVE_PROJECT_MODULES, CHANGE_KEY } from './actions';

const modulesByProject = (state = {}, action = {}) => {
if (action.type === RECEIVE_PROJECT_MODULES) {
const moduleKeys = action.modules.map(module => module.key);
return { ...state, [action.projectKey]: moduleKeys };
}

if (action.type === CHANGE_KEY) {
const newState = {};
Object.keys(state).forEach(projectKey => {
const moduleKeys = state[projectKey];
const changedKeyIndex = moduleKeys.indexOf(action.key);
if (changedKeyIndex !== -1) {
const newModuleKeys = [...moduleKeys];
newModuleKeys.splice(changedKeyIndex, 1, action.newKey);
newState[projectKey] = newModuleKeys;
} else {
newState[projectKey] = moduleKeys;
}
});
return newState;
}

return state;
};

export default modulesByProject;

export const getProjectModules = (state, projectKey) =>
state[projectKey];

+ 19
- 0
server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js Переглянути файл

@@ -27,6 +27,12 @@ import gates, { getAllGates as nextGetAllGates, getGate } from './gates';
import gateByProject, { getProjectGate as nextGetProjectGate } from './gateByProject';
import links, { getLink } from './links';
import linksByProject, { getLinks } from './linksByProject';
import components, {
getComponentByKey as nextGetComponentByKey
} from './components';
import modulesByProject, {
getProjectModules as nextGetProjectModules
} from './modulesByProject';
import globalMessages, {
getGlobalMessages as nextGetGlobalMessages
} from '../../../components/store/globalMessages';
@@ -38,6 +44,8 @@ const rootReducer = combineReducers({
gateByProject,
links,
linksByProject,
components,
modulesByProject,
globalMessages
});

@@ -69,5 +77,16 @@ export const getProjectLinks = (state, projectKey) =>
getLinks(state.linksByProject, projectKey)
.map(linkId => getLinkById(state, linkId));

export const getComponentByKey = (state, componentKey) =>
nextGetComponentByKey(state.components, componentKey);

export const getProjectModules = (state, projectKey) => {
const moduleKeys = nextGetProjectModules(state.modulesByProject, projectKey);
if (moduleKeys == null) {
return null;
}
return moduleKeys.map(moduleKey => getComponentByKey(state, moduleKey));
};

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

+ 1
- 0
server/sonar-web/src/main/less/style.less Переглянути файл

@@ -394,6 +394,7 @@ ul.bullet li {
margin: 0 1px 0 0;
padding: 1px 5px;
.link-no-underline;
transition: none;
}

.tabs2 li a.selected, .tabs li a.selected, .tabs .ui-tabs-active a {

+ 0
- 62
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb Переглянути файл

@@ -64,68 +64,6 @@ class ProjectController < ApplicationController

def key
@project = get_current_project(params[:id])
@snapshot = @project.last_snapshot
end

def update_key
project = get_current_project(params[:id])

new_key = params[:new_key].strip
if new_key.blank?
flash[:error] = message('update_key.new_key_cant_be_blank_for_x', :params => project.key)
elsif new_key == project.key
flash[:warning] = message('update_key.same_key_for_x', :params => project.key)
elsif Project.by_key(new_key)
flash[:error] = message('update_key.cant_update_x_because_resource_already_exist_with_key_x', :params => [project.key, new_key])
else
call_backend do
Internal.component_api.updateKey(project.key, new_key)
flash[:notice] = message('update_key.key_updated')
end
end

redirect_to :action => 'key', :id => project.root_project.id
end

def prepare_key_bulk_update
@project = get_current_project(params[:id])

@string_to_replace = params[:string_to_replace].strip
@replacement_string = params[:replacement_string].strip
if @string_to_replace.blank? || @replacement_string.blank?
flash[:error] = message('update_key.fieds_cant_be_blank_for_bulk_update')
redirect_to :action => 'key', :id => @project.id
else
call_backend do
@key_check_results = Internal.component_api.checkModuleKeysBeforeRenaming(@project.key, @string_to_replace, @replacement_string)
@can_update = false
@duplicate_key_found = false
@key_check_results.each do |key, value|
if value=="#duplicate_key#"
@duplicate_key_found = true
else
@can_update = true
end
end
@can_update = false if @duplicate_key_found
end
end
end

def perform_key_bulk_update
project = get_current_project(params[:id])

string_to_replace = params[:string_to_replace].strip
replacement_string = params[:replacement_string].strip

unless string_to_replace.blank? || replacement_string.blank?
call_backend do
Internal.component_api.bulkUpdateKey(project.key, string_to_replace, replacement_string)
flash[:notice] = message('update_key.key_updated')
end
end

redirect_to :action => 'key', :id => project.id
end

def history

+ 0
- 18
server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb Переглянути файл

@@ -1,18 +0,0 @@
<tr class="<%= cycle 'even', 'odd', :name => 'modules_tree' -%>">
<td class="thin nowrap" style="padding-left: <%= 3+ module_depth*15 -%>px">
<%= h(current_module.key) -%>
</td>
<td class="thin nowrap">
<% form_tag( {:action => 'update_key', :id => current_module.id }, :onsubmit => "update_launched();$j('#loading_#{id_prefix}').show();") do -%>
<input type="text" value="<%= h(current_module.key) -%>" name="new_key" id="key_<%= id_prefix -%>" size="80" maxlength="400">
<%= submit_tag message('update_key.rename'), :id => 'update_key_' + id_prefix, :class => 'action',
:confirm => message('update_key.are_you_sure_to_rename_x', :params => current_module.key) %>
<a href="#" onclick="$j('#key_<%= id_prefix -%>').val('<%= h(current_module.key) -%>');"><%= message('update_key.reset') -%></a>
<span class="loading" id="loading_<%= id_prefix -%>" style="display: none; padding: 3px 10px;"></span>
<% end %>
</td>
</tr>
<% current_module.modules.each_with_index do |sub_module, index| %>
<%= render :partial => 'key_modules', :locals => {:current_module => sub_module, :module_depth => module_depth+1,
:id_prefix => id_prefix + (index+1).to_s} -%>
<% end %>

+ 0
- 19
server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb Переглянути файл

@@ -1,19 +0,0 @@
<%
future_key = key_check_results[current_module.key]
if future_key=="#duplicate_key#"
duplicate_key = true
future_key = message('update_key.duplicate_key')
end
%>
<tr class="<%= cycle 'even', 'odd', :name => 'modules_tree' -%>">
<td class="thin nowrap" style="padding-left: <%= 3+ module_depth*15 -%>px;">
<%= h(current_module.key) -%>
</td>
<td class="thin nowrap <%= 'error' if duplicate_key -%>" style="<%= 'font-weight: bold;' if future_key -%>">
<%= future_key ? future_key : current_module.key -%>
</td>
</tr>
<% current_module.modules.each_with_index do |sub_module, index| %>
<%= render :partial => 'prepare_keys', :locals => {:current_module => sub_module, :module_depth => module_depth+1,
:key_check_results => key_check_results} -%>
<% end %>

+ 3
- 71
server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb Переглянути файл

@@ -1,71 +1,3 @@
<%
if controller.java_facade.getResourceTypeBooleanProperty(@project.qualifier, 'updatable_key')
has_modules = !@project.modules.empty?
reset_cycle 'modules_tree'
%>

<script type="text/javascript">
function update_launched() {
$j('input.action').each(function(index,input) {
input.disabled=true;
});
}
</script>

<div class="page">
<header class="page-header">
<h1 class="page-title"><%= message('update_key.page') -%></h1>
<p class="page-description"><%= message('update_key.page.description') -%></p>
</header>

<% if has_modules %>
<h2><%= message('update_key.bulk_update') -%></h2>
<br/>
<p>
<%= message('update_key.bulk_change_description') -%>
<br/><br/>
<%= message('update_key.current_key_for_project_x_is_x', :params => [@project.name, @project.key]) -%>
</p>
<br/>
<% form_tag( {:action => 'prepare_key_bulk_update', :id => @project.id }, :onsubmit => "update_launched();$j('#loading_bulk_update').show();") do -%>
<table>
<tr>
<td style="padding-right: 20px"><%= message('update_key.replace') -%>:</td>
<td><input type="text" value="" name="string_to_replace" id="string_to_replace" size="40" maxlength="400"></td>
<td class="form-val-note" style="padding-left: 10px;"><%= message('update_key.replace_example') -%></td>
</tr>
<tr>
<td style="padding-right: 20px"><%= message('update_key.by') -%>:</td>
<td><input type="text" value="" name="replacement_string" id="replacement_string" size="40" maxlength="400"></td>
<td class="form-val-note" style="padding-left: 10px;"><%= message('update_key.by_example') -%></td>
</tr>
<tr>
<td></td>
<td style="padding-top: 5px">
<%= submit_tag message('update_key.rename'), :id => 'bulk_update_button', :class => 'action' -%>
<span class="loading" id="loading_bulk_update" style="display: none; padding: 3px 10px;"></span>
</td>
<td></td>
</tr>
</table>
<% end %>
<br/>
<br/>
<h2><%= message('update_key.fine_grained_key_update') -%></h2>
<br/>
<% end %>

<table class="data" style="width:1%">
<thead>
<tr>
<th class="nowrap"><%= message('update_key.old_key') -%></th>
<th><%= message('update_key.new_key') -%></th>
</tr>
</thead>
<tbody>
<%= render :partial => 'key_modules', :locals => {:current_module => @project, :module_depth => 0, :id_prefix => "0"} -%>
</tbody>
</table>

<% end %>
</div>
<% content_for :extra_script do %>
<script src="<%= ApplicationController.root_context -%>/js/bundles/project-admin.js?v=<%= sonar_version -%>"></script>
<% end %>

+ 0
- 61
server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb Переглянути файл

@@ -1,61 +0,0 @@
<%
if @string_to_replace && @replacement_string
# validation screen for bulk update
reset_cycle 'modules_tree'
%>

<script type="text/javascript">
function update_launched() {
$j('input.action').each(function(index,input) {
input.disabled=true;
});
}
</script>

<div class="page">
<header class="page-header">
<h1 class="page-title">
<%= @can_update ? message('update_key.bulk_update_confirmation_page') : message('update_key.bulk_update_impossible') -%>
</h1>
</header>
<p>
<% if @can_update %>
<%= message('update_key.keys_will_be_updated_as_follows') -%>
<% else %>
<% if @duplicate_key_found %>
<%= message('update_key.cant_update_because_duplicate_keys', :params => [@string_to_replace, @replacement_string]) -%>
<% else %>
<%= message('update_key.no_key_to_update', :params => @string_to_replace) -%>
<% end %>
<% end %>
</p>

<table class="data" style="width:1%; margin-top: 10px">
<thead>
<tr>
<th><%= message('update_key.old_key') -%></th>
<th><%= message('update_key.new_key') -%></th>
</tr>
</thead>
<tbody>
<%= render :partial => 'prepare_keys', :locals => {:current_module => @project, :module_depth => 0, :key_check_results => @key_check_results} -%>
</tbody>
</table>

<% if @can_update %>
<% form_tag( {:action => 'perform_key_bulk_update', :id => @project.id }, :onsubmit => "update_launched();$j('#loading_bulk_update').show();") do -%>
<input type="hidden" value="<%= @project.id -%>" name="id" id="project_id">
<input type="hidden" value="<%= @string_to_replace -%>" name="string_to_replace" id="string_to_replace">
<input type="hidden" value="<%= @replacement_string -%>" name="replacement_string" id="replacement_string">
<br/>
<%= submit_tag message('update_key.rename'), :id => 'bulk_update_button', :class => 'action' -%>
&nbsp;<a href="<%= url_for :action => 'key', :id => @project.key -%>"><%= message('cancel') -%></a>
<span class="loading" id="loading_bulk_update" style="display: none; padding: 3px 10px;"></span>
<% end %>
<% else %>
<br/>
<a href="<%= url_for :action => 'key', :id => @project.key -%>"><%= message('back') -%></a>
<% end %>
</div>

<% end %>

+ 10
- 17
sonar-core/src/main/resources/org/sonar/l10n/core.properties Переглянути файл

@@ -213,6 +213,7 @@ are_you_sure=Are you sure?
assigned_to=Assigned to
bulk_change=Bulk Change
bulleted_point=Bulleted point
check_project=Check project
coding_rules=Rules
click_to_add_to_favorites=Click to add to favorites
click_to_remove_from_favorites=Click to remove from favorites
@@ -1602,29 +1603,21 @@ project_history.event_already_exists=Event "{0}" already exists.
#------------------------------------------------------------------------------
update_key.bulk_update=Bulk Update
update_key.fine_grained_key_update=Fine-grained Update
update_key.old_key=Old key
update_key.new_key=New key
update_key.rename=Rename
update_key.reset=Reset
update_key.new_key_cant_be_blank_for_x=The new key can not be blank for "{0}".
update_key.same_key_for_x=The new key is the same as the original one ("{0}"), nothing has been updated.
update_key.cant_update_x_because_resource_already_exist_with_key_x="{0}" can not be renamed because "{1}" is the key of an existing resource. The update has been canceled.
update_key.error_occured_while_renaming_key_of_x=An error occurred while renaming the key "{0}": {1}
update_key.old_key=Old Key
update_key.new_key=New Key
update_key.key_updated=The key has successfully been updated for all required resources.
update_key.fieds_cant_be_blank_for_bulk_update=The two fields can not be blank for the bulk update.
update_key.bulk_change_description=The bulk update allows to replace a part of the current key(s) by another string on the current project and all its submodules - if applicable.
update_key.current_key_for_project_x_is_x=The key of the "{0}" project is currently "<b>{1}</b>".
update_key.are_you_sure_to_rename_x=Are you sure you want to rename "{0}", as well as all its modules and resources?
update_key.current_key_for_project_x_is_x=The key of the "{0}" project is currently "{1}".
update_key.replace=Replace
update_key.by=By
update_key.replace_example=Ex.: "org.myCompany"
update_key.by_example=Ex.: "com.myNewCompany"
update_key.replace_example=org.myCompany
update_key.by_example=com.myNewCompany
update_key.cant_update_because_duplicate_keys=The replacement of "{0}" by "{1}" is impossible as it would result in duplicate keys (in red below):
update_key.keys_will_be_updated_as_follows=The resources will be updated as follows (updated keys in bold):
update_key.duplicate_key=Duplicate key
update_key.bulk_update_confirmation_page=Do you really want to perform the bulk update on project keys?
update_key.bulk_update_impossible=Bulk update can not be performed
update_key.keys_will_be_updated_as_follows=The resources will be updated as follows:
update_key.duplicate_key=Duplicate Key
update_key.no_key_to_update=No key contains the string to replace ("{0}").
update_key.are_you_sure_to_change_key=Are you sure you want to change key of "{0}", as well as all its modules and resources?
update_key.see_results=See Results →


#------------------------------------------------------------------------------

Завантаження…
Відмінити
Зберегти