aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java33
-rw-r--r--it/it-tests/src/test/java/it/projectAdministration/ProjectKeyPageTest.java184
-rw-r--r--it/it-tests/src/test/java/pageobjects/Navigation.java6
-rw-r--r--it/it-tests/src/test/java/pageobjects/ProjectKeyPage.java103
-rw-r--r--it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-duplicate-keys.html104
-rw-r--r--it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-no-match.html104
-rw-r--r--it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-success.html114
-rw-r--r--it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-impossible.html84
-rw-r--r--it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-success.html89
-rw-r--r--server/sonar-web/src/main/js/api/components.js26
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/app.js4
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js127
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js75
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js111
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js53
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/Header.js36
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/Key.js134
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js76
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js92
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs30
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js48
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/actions.js28
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/components.js47
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js50
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js19
-rw-r--r--server/sonar-web/src/main/less/style.less1
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb62
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb18
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb19
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb74
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb61
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties27
32 files changed, 1263 insertions, 776 deletions
diff --git a/it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java b/it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java
index 001d25538c0..f9863aa85b4 100644
--- a/it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java
+++ b/it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java
@@ -180,39 +180,6 @@ public class ProjectAdministrationTest {
}
/**
- * 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
*/
@Test
diff --git a/it/it-tests/src/test/java/it/projectAdministration/ProjectKeyPageTest.java b/it/it-tests/src/test/java/it/projectAdministration/ProjectKeyPageTest.java
new file mode 100644
index 00000000000..29ea28a15b5
--- /dev/null
+++ b/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));
+ }
+}
diff --git a/it/it-tests/src/test/java/pageobjects/Navigation.java b/it/it-tests/src/test/java/pageobjects/Navigation.java
index d7d4a794614..a4b3f9af62d 100644
--- a/it/it-tests/src/test/java/pageobjects/Navigation.java
+++ b/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);
}
diff --git a/it/it-tests/src/test/java/pageobjects/ProjectKeyPage.java b/it/it-tests/src/test/java/pageobjects/ProjectKeyPage.java
new file mode 100644
index 00000000000..43f85c1ba43
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-duplicate-keys.html b/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-duplicate-keys.html
deleted file mode 100644
index dcffc75a5a2..00000000000
--- a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-duplicate-keys.html
+++ /dev/null
@@ -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>
diff --git a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-no-match.html b/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-no-match.html
deleted file mode 100644
index f4e01d07dba..00000000000
--- a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-impossible-because-no-match.html
+++ /dev/null
@@ -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>
diff --git a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-success.html b/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-success.html
deleted file mode 100644
index e75663989ca..00000000000
--- a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/bulk-update-success.html
+++ /dev/null
@@ -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>
diff --git a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-impossible.html b/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-impossible.html
deleted file mode 100644
index d074667d223..00000000000
--- a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-impossible.html
+++ /dev/null
@@ -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>
diff --git a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-success.html b/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-success.html
deleted file mode 100644
index 4f2d9d597eb..00000000000
--- a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-update-keys/fine-grained-update-success.html
+++ /dev/null
@@ -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>
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js
index f0f49a636b4..6b3238022fb 100644
--- a/server/sonar-web/src/main/js/api/components.js
+++ b/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);
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/app.js b/server/sonar-web/src/main/js/apps/project-admin/app.js
index 7981c936e87..903ed046cb9 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/app.js
+++ b/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);
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js
new file mode 100644
index 00000000000..1624faddc3f
--- /dev/null
+++ b/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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js
new file mode 100644
index 00000000000..db31d98063d
--- /dev/null
+++ b/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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js
new file mode 100644
index 00000000000..56f5531875b
--- /dev/null
+++ b/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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js b/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js
new file mode 100644
index 00000000000..6f7deacf0ed
--- /dev/null
+++ b/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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/Header.js b/server/sonar-web/src/main/js/apps/project-admin/key/Header.js
new file mode 100644
index 00000000000..c730528a8a2
--- /dev/null
+++ b/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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/Key.js b/server/sonar-web/src/main/js/apps/project-admin/key/Key.js
new file mode 100644
index 00000000000..47b61bc3e6e
--- /dev/null
+++ b/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);
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js b/server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js
new file mode 100644
index 00000000000..701d2070957
--- /dev/null
+++ b/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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js b/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js
new file mode 100644
index 00000000000..f23d1f7069e
--- /dev/null
+++ b/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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs b/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs
new file mode 100644
index 00000000000..bec83b7693e
--- /dev/null
+++ b/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>
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js b/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js
new file mode 100644
index 00000000000..9b1c4abd26e
--- /dev/null
+++ b/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
+ };
+ }
+});
+
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js
index 29a627e9d54..ca20001b763 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js
+++ b/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)));
+};
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/components.js b/server/sonar-web/src/main/js/apps/project-admin/store/components.js
new file mode 100644
index 00000000000..bc369003eee
--- /dev/null
+++ b/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];
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js b/server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js
new file mode 100644
index 00000000000..0b55882cf2d
--- /dev/null
+++ b/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];
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
index 513e39dc67d..a5c56087ce9 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
+++ b/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);
diff --git a/server/sonar-web/src/main/less/style.less b/server/sonar-web/src/main/less/style.less
index 26ca85df14c..5743af7c82e 100644
--- a/server/sonar-web/src/main/less/style.less
+++ b/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 {
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb
index d4d40889d91..a3dad4b490a 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb
+++ b/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
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb
deleted file mode 100644
index 471065cbd2e..00000000000
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb
+++ /dev/null
@@ -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 %>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb
deleted file mode 100644
index 1624108b2d8..00000000000
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb
+++ /dev/null
@@ -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 %> \ No newline at end of file
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb
index 0ae01cfeea0..e9dd9ae3410 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb
+++ b/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 %>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb
deleted file mode 100644
index 7c5bcc61074..00000000000
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb
+++ /dev/null
@@ -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 %>
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 7bfc12f147d..81080336c9a 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/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 →
#------------------------------------------------------------------------------