]> source.dussan.org Git - sonarqube.git/commitdiff
refactor quality profiles page (#1056)
authorStas Vilchik <vilchiks@gmail.com>
Mon, 27 Jun 2016 14:08:44 +0000 (16:08 +0200)
committerGitHub <noreply@github.com>
Mon, 27 Jun 2016 14:08:44 +0000 (16:08 +0200)
127 files changed:
it/it-tests/src/test/java/it/Category4Suite.java
it/it-tests/src/test/java/it/qualityProfile/QualityProfilesPageTest.java [new file with mode: 0644]
it/it-tests/src/test/java/it/qualityProfile/ToDoTest.java [deleted file]
it/it-tests/src/test/resources/authorisation/QualityProfileAdminPermissionTest/normal-user.html
it/it-tests/src/test/resources/authorisation/QualityProfileAdminPermissionTest/profile-admin.html
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/not_found.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_compare.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_copy.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_create.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_delete.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_changelog.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_list.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_profile_exporters.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_profile_inheritance.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_profile_projects.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_profile_rules.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_filter_by_language.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_open_from_list.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_rename.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_restore.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_restore_built_in.html [new file with mode: 0644]
it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_set_default.html [new file with mode: 0644]
server/sonar-web/src/main/js/api/quality-profiles.js
server/sonar-web/src/main/js/api/rules.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/__tests__/utils-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/actions-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/app.js
server/sonar-web/src/main/js/apps/quality-profiles/change-profile-parent-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogEmpty.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogSearch.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangesList.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/changelog/ParameterChange.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogSearch-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangesList-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ParameterChange-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonEmpty.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonForm-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/components/App.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/controller.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/copy-profile-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/create-profile-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/delete-profile-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileEvolution.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProgressBar.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/home/Evolution.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/intro-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/layout.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/profile-changelog-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/profile-comparison-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/profile-details-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/profile-header-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/profile-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/profile.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/profiles-empty-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/profiles-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/profiles.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/propTypes.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/rename-profile-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/restore-built-in-profiles-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/restore-profile-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/router.js [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/styles.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-changelog.hbs [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-comparison.hbs [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-actions.hbs [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-projects.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-create-profile.hbs
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-delete-profile.hbs
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-empty.hbs [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-intro.hbs [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-layout.hbs [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-details.hbs [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-header.hbs [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile.hbs [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles-language.hbs [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles.hbs [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-built-in-profiles-success.hbs
server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-built-in-profiles.hbs
server/sonar-web/src/main/js/apps/quality-profiles/utils.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeParentView.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeProjectsView.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/views/CopyProfileView.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/views/CreateProfileView.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/views/DeleteProfileView.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/views/RenameProfileView.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreBuiltInProfilesView.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreProfileView.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/DateInput.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/styles.css [new file with mode: 0644]
server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js
server/sonar-web/src/main/js/components/shared/severity-helper.js
server/sonar-web/src/main/js/helpers/urls.js
server/sonar-web/src/main/less/components/page.less
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/profiles_controller.rb
server/sonar-web/tests/mocha.opts
server/sonar-web/tests/null-compiler.js
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index a12a4f39779c5d2aee3a4e26da57fd9805a7be50..48297240e98d48da31c319748a0a5fc9f999b197 100644 (file)
@@ -33,6 +33,7 @@ import it.duplication.DuplicationsTest;
 import it.http.HttpHeadersTest;
 import it.projectComparison.ProjectComparisonTest;
 import it.projectEvent.EventTest;
+import it.qualityProfile.QualityProfilesPageTest;
 import it.serverSystem.ServerSystemTest;
 import it.ui.UiTest;
 import it.uiExtension.UiExtensionsTest;
@@ -90,7 +91,9 @@ import static util.ItUtils.xooPlugin;
   // ui extensions
   UiExtensionsTest.class,
   WsLocalCallTest.class,
-  WsTest.class
+  WsTest.class,
+  // quality profiles
+  QualityProfilesPageTest.class
 })
 public class Category4Suite {
 
diff --git a/it/it-tests/src/test/java/it/qualityProfile/QualityProfilesPageTest.java b/it/it-tests/src/test/java/it/qualityProfile/QualityProfilesPageTest.java
new file mode 100644 (file)
index 0000000..8a74c2b
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ * 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.qualityProfile;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import com.sonar.orchestrator.selenium.Selenese;
+import it.Category4Suite;
+import org.junit.*;
+import org.junit.experimental.categories.Category;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import util.QaOnly;
+import util.selenium.SeleneseTest;
+
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.projectDir;
+
+@Category(QaOnly.class)
+public class QualityProfilesPageTest {
+
+  @ClassRule
+  public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+  private static WsClient adminWsClient;
+
+  @BeforeClass
+  public static void setUp() {
+    adminWsClient = newAdminWsClient(orchestrator);
+    orchestrator.resetData();
+  }
+
+  @Before
+  public void createSampleProfile() {
+    createProfile("xoo", "sample");
+    inheritProfile("xoo", "sample", "Basic");
+    analyzeProject("shared/xoo-sample");
+    addProfileToProject("xoo", "sample", "sample");
+  }
+
+  @After
+  public void deleteSampleProfile() {
+    setDefault("xoo", "Basic");
+    deleteProfile("xoo", "sample");
+    deleteProfile("xoo", "new name");
+  }
+
+  @Test
+  public void testHomePage() throws Exception {
+    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_home_page",
+      "/qualityProfile/QualityProfilesPageTest/should_display_list.html",
+      "/qualityProfile/QualityProfilesPageTest/should_open_from_list.html",
+      "/qualityProfile/QualityProfilesPageTest/should_filter_by_language.html"
+    ).build();
+    new SeleneseTest(selenese).runOn(orchestrator);
+  }
+
+  @Test
+  public void testProfilePage() throws Exception {
+    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_profile_page",
+      "/qualityProfile/QualityProfilesPageTest/should_display_profile_rules.html",
+      "/qualityProfile/QualityProfilesPageTest/should_display_profile_inheritance.html",
+      "/qualityProfile/QualityProfilesPageTest/should_display_profile_projects.html",
+      "/qualityProfile/QualityProfilesPageTest/should_display_profile_exporters.html"
+    ).build();
+    new SeleneseTest(selenese).runOn(orchestrator);
+  }
+
+  @Test
+  public void testNotFound() {
+    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_not_found",
+      "/qualityProfile/QualityProfilesPageTest/not_found.html").build();
+    orchestrator.executeSelenese(selenese);
+  }
+
+  @Test
+  public void testProfileChangelog() throws Exception {
+    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_profile_changelog",
+      "/qualityProfile/QualityProfilesPageTest/should_display_changelog.html"
+    ).build();
+    new SeleneseTest(selenese).runOn(orchestrator);
+  }
+
+  @Ignore("find a way to know profile key inside selenium tests")
+  @Test
+  public void testComparison() throws Exception {
+    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_comparison",
+      "/qualityProfile/QualityProfilesPageTest/should_compare.html"
+    ).build();
+    new SeleneseTest(selenese).runOn(orchestrator);
+  }
+
+  @Test
+  public void testCreation() throws Exception {
+    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_creation",
+      "/qualityProfile/QualityProfilesPageTest/should_create.html"
+    ).build();
+    new SeleneseTest(selenese).runOn(orchestrator);
+  }
+
+  @Test
+  public void testDeletion() throws Exception {
+    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_deletion",
+      "/qualityProfile/QualityProfilesPageTest/should_delete.html"
+    ).build();
+    new SeleneseTest(selenese).runOn(orchestrator);
+  }
+
+  @Test
+  public void testCopying() throws Exception {
+    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_copying",
+      "/qualityProfile/QualityProfilesPageTest/should_copy.html"
+    ).build();
+    new SeleneseTest(selenese).runOn(orchestrator);
+  }
+
+  @Test
+  public void testRenaming() throws Exception {
+    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_renaming",
+      "/qualityProfile/QualityProfilesPageTest/should_rename.html"
+    ).build();
+    new SeleneseTest(selenese).runOn(orchestrator);
+  }
+
+  @Test
+  public void testSettingDefault() throws Exception {
+    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_setting_default",
+      "/qualityProfile/QualityProfilesPageTest/should_set_default.html"
+    ).build();
+    new SeleneseTest(selenese).runOn(orchestrator);
+  }
+
+  @Test
+  public void testRestoration() throws Exception {
+    deleteProfile("xoo", "empty");
+
+    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_restoration",
+      "/qualityProfile/QualityProfilesPageTest/should_restore.html",
+      "/qualityProfile/QualityProfilesPageTest/should_restore_built_in.html"
+    ).build();
+    new SeleneseTest(selenese).runOn(orchestrator);
+  }
+
+  private static void createProfile(String language, String name) {
+    adminWsClient.wsConnector().call(
+      new PostRequest("api/qualityprofiles/create")
+        .setParam("language", language)
+        .setParam("name", name));
+  }
+
+  private static void inheritProfile(String language, String name, String parentName) {
+    adminWsClient.wsConnector().call(
+      new PostRequest("api/qualityprofiles/change_parent")
+        .setParam("language", language)
+        .setParam("profileName", name)
+        .setParam("parentName", parentName));
+  }
+
+  private static void analyzeProject(String path) {
+    orchestrator.executeBuild(SonarScanner.create(projectDir(path)));
+  }
+
+  private static void addProfileToProject(String language, String profileName, String projectKey) {
+    adminWsClient.wsConnector().call(
+      new PostRequest("api/qualityprofiles/add_project")
+        .setParam("language", language)
+        .setParam("profileName", profileName)
+        .setParam("projectKey", projectKey));
+  }
+
+  private static void deleteProfile(String language, String name) {
+    adminWsClient.wsConnector().call(
+      new PostRequest("api/qualityprofiles/delete")
+        .setParam("language", language)
+        .setParam("profileName", name));
+  }
+
+  private static void setDefault(String language, String name) {
+     adminWsClient.wsConnector().call(
+      new PostRequest("api/qualityprofiles/set_default")
+        .setParam("language", language)
+        .setParam("profileName", name));
+  }
+}
diff --git a/it/it-tests/src/test/java/it/qualityProfile/ToDoTest.java b/it/it-tests/src/test/java/it/qualityProfile/ToDoTest.java
deleted file mode 100644 (file)
index 1823f97..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package it.qualityProfile;
-
-public class ToDoTest {
-}
index ef778363fc39cc9496a67b91be7b4d1daadbbd1f..dee98790990e6ca5e0e9dac1bc933b035fcbab7c 100644 (file)
@@ -9,71 +9,56 @@
 <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/new</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>type</td>
-    <td>id=login</td>
-    <td>not_profileadm</td>
-  </tr>
-  <tr>
-    <td>type</td>
-    <td>id=password</td>
-    <td>userpwd</td>
-  </tr>
-  <tr>
-    <td>clickAndWait</td>
-    <td>name=commit</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>open</td>
-    <td>/profiles</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>css=.quality-profiles-results</td>
-    <td>*Basic*</td>
-  </tr>
-  <tr>
-    <td>assertNotText</td>
-    <td>css=.search-navigator-filters</td>
-    <td>*Create*</td>
-  </tr>
-  <tr>
-    <td>assertNotText</td>
-    <td>css=.search-navigator-filters</td>
-    <td>*Restore Profile*</td>
-  </tr>
-  <tr>
-    <td>assertNotText</td>
-    <td>css=.search-navigator-filters</td>
-    <td>*Restore Built-in Profiles*</td>
-  </tr>
-  <tr>
-    <td>open</td>
-    <td>/project/profile/sample</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>id=content</td>
-    <td>*Log In to SonarQube*</td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>id=login_form</td>
-    <td>glob:*You are not authorized to access this page*</td>
-  </tr>
-  </tbody>
+       <td>open</td>
+       <td>/sessions/logout</td>
+       <td></td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/sessions/new</td>
+       <td></td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>id=login</td>
+       <td>not_profileadm</td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>id=password</td>
+       <td>userpwd</td>
+</tr>
+<tr>
+       <td>clickAndWait</td>
+       <td>name=commit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;Basic&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;Basic&quot;] .quality-profiles-table-name a</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=.quality-profile-header</td>
+       <td>*Basic*</td>
+</tr>
+<tr>
+       <td>assertElementNotPresent</td>
+       <td>css=.js-change-parent</td>
+       <td></td>
+</tr>
+</tbody>
 </table>
 </body>
 </html>
index af19a0a1da2c99879662d59dd4496d78ac81115d..76ec57eadc0a789606001b54718dfc16e1fda4f2 100644 (file)
        <td></td>
 </tr>
 <tr>
-       <td>waitForText</td>
-       <td>css=.quality-profiles-results</td>
-       <td>*Basic*</td>
+  <td>waitForElementPresent</td>
+  <td>css=.quality-profiles-table-row[data-name=&quot;Basic&quot;]</td>
+  <td></td>
 </tr>
 <tr>
-       <td>waitForText</td>
-       <td>css=.search-navigator-filters</td>
-       <td>*Create*</td>
+  <td>click</td>
+  <td>css=.quality-profiles-table-row[data-name=&quot;Basic&quot;] .quality-profiles-table-name a</td>
+  <td></td>
 </tr>
 <tr>
-       <td>waitForText</td>
-       <td>css=.search-navigator-filters</td>
-       <td>*Restore Profile*</td>
+  <td>waitForText</td>
+  <td>css=.quality-profile-header</td>
+  <td>*Basic*</td>
 </tr>
 <tr>
-       <td>waitForText</td>
-       <td>css=.search-navigator-filters</td>
-       <td>*Restore Built-in Profiles*</td>
+  <td>assertElementPresent</td>
+  <td>css=.js-change-parent</td>
+  <td></td>
 </tr>
 <tr>
-       <td>open</td>
-       <td>/project/profile/sample</td>
-       <td></td>
+  <td>open</td>
+  <td>/project/profile/sample</td>
+  <td></td>
 </tr>
 <tr>
-       <td>waitForText</td>
-       <td>id=content</td>
-       <td>*Quality Profiles*</td>
+  <td>waitForText</td>
+  <td>id=content</td>
+  <td>*Quality Profiles*</td>
 </tr>
 <tr>
-       <td>assertValue</td>
-       <td>id=submit-xoo</td>
-       <td>glob:*Update*</td>
+  <td>assertValue</td>
+  <td>id=submit-xoo</td>
+  <td>glob:*Update*</td>
 </tr>
 </tbody>
 </table>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/not_found.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/not_found.html
new file mode 100644 (file)
index 0000000..220d398
--- /dev/null
@@ -0,0 +1,24 @@
+<?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>not_found</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+    <tbody>
+    <tr>
+        <td>open</td>
+        <td>/profiles/show?key=unknown</td>
+        <td></td>
+    </tr>
+    <tr>
+        <td>waitForElementPresent</td>
+        <td>css=.quality-profile-not-found</td>
+        <td></td>
+    </tr>
+    </tbody>
+</table>
+</body>
+</html>
\ No newline at end of file
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_compare.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_compare.html
new file mode 100644 (file)
index 0000000..c2339d6
--- /dev/null
@@ -0,0 +1,60 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_display_changelog</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_changelog</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-header .dropdown-toggle</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profile-header .dropdown-toggle</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=#quality-profile-compare</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.js-profile-comparison .Select</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.js-profile-comparison .Select-control</td>
+       <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_copy.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_copy.html
new file mode 100644 (file)
index 0000000..77bced3
--- /dev/null
@@ -0,0 +1,105 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<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>id=password</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>id=login</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>clickAndWait</td>
+       <td>name=commit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-header .dropdown-toggle</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profile-header .dropdown-toggle</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=#quality-profile-copy</td>
+       <td></td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>css=#copy-profile-name</td>
+       <td>copied</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=#copy-profile-submit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=.quality-profile-header</td>
+       <td>*copied*</td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=.quality-profile-rules</td>
+       <td>*1*Bugs*0*Vulnerabilities*0*Code Smells*1*</td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;copied&quot;]</td>
+       <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_create.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_create.html
new file mode 100644 (file)
index 0000000..2d9a7f5
--- /dev/null
@@ -0,0 +1,105 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<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>id=password</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>id=login</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>clickAndWait</td>
+       <td>name=commit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=#quality-profiles-create</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=#quality-profiles-create</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=#create-profile-name</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertElementPresent</td>
+       <td>css=#create-profile-form-backup-XooProfileImporter</td>
+       <td></td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>css=#create-profile-name</td>
+       <td>test</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=#create-profile-submit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-header</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=.quality-profile-header</td>
+       <td>*test*</td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=.quality-profile-rules</td>
+       <td>*Bugs*0*Vulnerabilities*0*Code Smells*0*</td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;test&quot;]</td>
+       <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_delete.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_delete.html
new file mode 100644 (file)
index 0000000..9ed1f3a
--- /dev/null
@@ -0,0 +1,110 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<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>id=password</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>id=login</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>clickAndWait</td>
+       <td>name=commit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-header .dropdown-toggle</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profile-header .dropdown-toggle</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=#quality-profile-delete</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=#delete-profile-submit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=#delete-profile-submit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;Basic&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementNotPresent</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;Basic&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertElementNotPresent</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;sample&quot;]</td>
+       <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_changelog.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_changelog.html
new file mode 100644 (file)
index 0000000..18851fb
--- /dev/null
@@ -0,0 +1,55 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_display_changelog</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_changelog</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-basic&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-basic&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=a[href^=&quot;/profiles/changelog?key=xoo-basic&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=a[href^=&quot;/profiles/changelog?key=xoo-basic&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.js-profile-changelog-event</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.js-profile-changelog-event</td>
+       <td>*System*Activated*Has Tag*Major*tag*xoo*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_list.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_list.html
new file mode 100644 (file)
index 0000000..9d171b7
--- /dev/null
@@ -0,0 +1,75 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_display_list</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_list</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertElementPresent</td>
+       <td>css=.quality-profiles-table[data-language=&quot;xoo&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertElementPresent</td>
+       <td>css=.quality-profiles-table[data-language=&quot;xoo2&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertElementPresent</td>
+       <td>css=.quality-profiles-table-row[data-key^=&quot;xoo-basic&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.quality-profiles-table-row[data-key^=&quot;xoo-basic&quot;] .quality-profiles-table-name</td>
+       <td>*Basic*</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.quality-profiles-table-row[data-key^=&quot;xoo-basic&quot;] .quality-profiles-table-projects</td>
+       <td>*Default*</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.quality-profiles-table-row[data-key^=&quot;xoo-basic&quot;] .quality-profiles-table-rules</td>
+       <td>*1*</td>
+</tr>
+<tr>
+       <td>assertElementPresent</td>
+       <td>css=.quality-profiles-table-row[data-key^=&quot;xoo-basic&quot;] .quality-profiles-table-rules a[href^=&quot;/coding_rules#qprofile=xoo-basic&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.quality-profiles-table-row[data-key^=&quot;xoo-empty&quot;] .quality-profiles-table-projects</td>
+       <td>*0*</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.quality-profiles-table-row[data-key^=&quot;xoo2-basic&quot;] .quality-profiles-table-name</td>
+       <td>*Basic*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_profile_exporters.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_profile_exporters.html
new file mode 100644 (file)
index 0000000..aaabd19
--- /dev/null
@@ -0,0 +1,50 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_display_profile_projects.html</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_profile_projects.html</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-exporters</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-exporters [data-key=&quot;XooFakeExporter&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-exporters a[href^=&quot;/api/qualityprofiles/export&quot;]</td>
+       <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_profile_inheritance.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_profile_inheritance.html
new file mode 100644 (file)
index 0000000..22477a9
--- /dev/null
@@ -0,0 +1,60 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_display_profile_inheritance.html</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_profile_inheritance.html</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-inheritance</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.js-inheritance-ancestor</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.js-inheritance-ancestor</td>
+       <td>*Basic*1*</td>
+</tr>
+<tr>
+       <td>assertElementPresent</td>
+       <td>css=.js-inheritance-current</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.js-inheritance-current</td>
+       <td>*sample*1*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_profile_projects.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_profile_projects.html
new file mode 100644 (file)
index 0000000..fba5742
--- /dev/null
@@ -0,0 +1,50 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_display_profile_projects.html</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_profile_projects.html</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-projects</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.js-profile-project</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.js-profile-project</td>
+       <td>*Sample*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_profile_rules.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_display_profile_rules.html
new file mode 100644 (file)
index 0000000..9819f42
--- /dev/null
@@ -0,0 +1,45 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_display_profile_rules.html</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_profile_rules.html</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-basic&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-basic&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-rules</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=.quality-profile-rules</td>
+       <td>*Active*1*Bugs*0*Vulnerabilities*0*Code Smells*1*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_filter_by_language.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_filter_by_language.html
new file mode 100644 (file)
index 0000000..192ad66
--- /dev/null
@@ -0,0 +1,90 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_filter_by_language</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_filter_by_language</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table[data-language=&quot;xoo&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertElementPresent</td>
+       <td>css=.quality-profiles-table[data-language=&quot;xoo2&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.js-language-filter</td>
+       <td>*All*</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.js-language-filter</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.js-language-filter-option[data-language=&quot;xoo2&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.js-language-filter-option[data-language=&quot;xoo2&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementNotPresent</td>
+       <td>css=.quality-profiles-table[data-language=&quot;xoo&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertElementPresent</td>
+       <td>css=.quality-profiles-table[data-language=&quot;xoo2&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.js-language-filter</td>
+       <td>*Xoo2*</td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles?language=xoo2</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table[data-language=&quot;xoo2&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertElementNotPresent</td>
+       <td>css=.quality-profiles-table[data-language=&quot;xoo&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.js-language-filter</td>
+       <td>*Xoo2*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_open_from_list.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_open_from_list.html
new file mode 100644 (file)
index 0000000..8a8adb9
--- /dev/null
@@ -0,0 +1,55 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_open_from_list</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_open_from_list</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-row[data-key^=&quot;xoo-basic&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profiles-table-row[data-key^=&quot;xoo-basic&quot;] .quality-profiles-table-name a</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-header</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-rules</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-inheritance</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-projects</td>
+       <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_rename.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_rename.html
new file mode 100644 (file)
index 0000000..059057a
--- /dev/null
@@ -0,0 +1,110 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<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>id=password</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>id=login</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>clickAndWait</td>
+       <td>name=commit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-header .dropdown-toggle</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profile-header .dropdown-toggle</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=#quality-profile-rename</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=#rename-profile-name</td>
+       <td></td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>css=#rename-profile-name</td>
+       <td>new name</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=#rename-profile-submit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=.quality-profile-header</td>
+       <td>*new name*</td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertElementPresent</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;new name&quot;]</td>
+       <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_restore.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_restore.html
new file mode 100644 (file)
index 0000000..d358859
--- /dev/null
@@ -0,0 +1,70 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<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>id=password</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>id=login</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>clickAndWait</td>
+       <td>name=commit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=#quality-profiles-restore</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=#quality-profiles-restore</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=#restore-profile-backup</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=#restore-profile-submit</td>
+       <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_restore_built_in.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_restore_built_in.html
new file mode 100644 (file)
index 0000000..2ece141
--- /dev/null
@@ -0,0 +1,90 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<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>id=password</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>id=login</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>clickAndWait</td>
+       <td>name=commit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.js-restore-built-in[data-language=&quot;xoo&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertElementNotPresent</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;empty&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.js-restore-built-in[data-language=&quot;xoo&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=#restore-built-in-profiles-submit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=#restore-built-in-profiles-submit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=#restore-built-in-profiles-form .alert-success</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.js-modal-close</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;empty&quot;]</td>
+       <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_set_default.html b/it/it-tests/src/test/resources/qualityProfile/QualityProfilesPageTest/should_set_default.html
new file mode 100644 (file)
index 0000000..11c15d4
--- /dev/null
@@ -0,0 +1,95 @@
+<?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"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<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>id=password</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>id=login</td>
+       <td>admin</td>
+</tr>
+<tr>
+       <td>clickAndWait</td>
+       <td>name=commit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profile-header .dropdown-toggle</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.quality-profile-header .dropdown-toggle</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=#quality-profile-set-as-default</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=.quality-profile-projects</td>
+       <td>*Default*</td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/profiles</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;sample&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>css=.quality-profiles-table-row[data-name=&quot;sample&quot;] .quality-profiles-table-projects</td>
+       <td>*Default*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
index 8172dec351746ab12abceb4a89e3ae7f083cbfa4..4ddb455e9e694dedf1f3388aece2c3aacbffefda 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { request, checkStatus, parseJSON } from '../helpers/request';
+import {
+    request,
+    checkStatus,
+    parseJSON,
+    getJSON,
+    post,
+    postJSON
+} from '../helpers/request';
+
+export function getQualityProfiles () {
+  const url = '/api/qualityprofiles/search';
+  return getJSON(url).then(r => r.profiles);
+}
 
 export function createQualityProfile (data) {
   return request('/api/qualityprofiles/create')
@@ -36,3 +48,118 @@ export function restoreQualityProfile (data) {
       .then(checkStatus)
       .then(parseJSON);
 }
+
+export function getProfileProjects (data) {
+  const url = '/api/qualityprofiles/projects';
+  return getJSON(url, data);
+}
+
+export function getProfileInheritance (profileKey) {
+  const url = '/api/qualityprofiles/inheritance';
+  const data = { profileKey };
+  return getJSON(url, data);
+}
+
+export function setDefaultProfile (profileKey) {
+  const url = '/api/qualityprofiles/set_default';
+  const data = { profileKey };
+  return post(url, data);
+}
+
+/**
+ * Rename profile
+ * @param {string} key
+ * @param {string} name
+ * @returns {Promise}
+ */
+export function renameProfile (key, name) {
+  const url = '/api/qualityprofiles/rename';
+  const data = { key, name };
+  return post(url, data);
+}
+
+/**
+ * Copy profile
+ * @param {string} fromKey
+ * @param {string} toName
+ * @returns {Promise}
+ */
+export function copyProfile (fromKey, toName) {
+  const url = '/api/qualityprofiles/copy';
+  const data = { fromKey, toName };
+  return postJSON(url, data);
+}
+
+/**
+ * Delete profile
+ * @param {string} profileKey
+ * @returns {Promise}
+ */
+export function deleteProfile (profileKey) {
+  const url = '/api/qualityprofiles/delete';
+  const data = { profileKey };
+  return post(url, data);
+}
+
+/**
+ * Change profile parent
+ * @param {string} profileKey
+ * @param {string} parentKey
+ * @returns {Promise}
+ */
+export function changeProfileParent (profileKey, parentKey) {
+  const url = '/api/qualityprofiles/change_parent';
+  const data = { profileKey, parentKey };
+  return post(url, data);
+}
+
+/**
+ * Get list of available importers
+ * @returns {Promise}
+ */
+export function getImporters () {
+  const url = '/api/qualityprofiles/importers';
+  return getJSON(url).then(r => r.importers);
+}
+
+/**
+ * Get list of available exporters
+ * @returns {Promise}
+ */
+export function getExporters () {
+  const url = '/api/qualityprofiles/exporters';
+  return getJSON(url).then(r => r.exporters);
+}
+
+/**
+ * Restore built-in profiles
+ * @param {string} languageKey
+ * @returns {Promise}
+ */
+export function restoreBuiltInProfiles (languageKey) {
+  const url = '/api/qualityprofiles/restore_built_in';
+  const data = { language: languageKey };
+  return post(url, data);
+}
+
+/**
+ * Get changelog of a quality profile
+ * @param {Object} data API parameters
+ * @returns {Promise}
+ */
+export function getProfileChangelog (data) {
+  const url = '/api/qualityprofiles/changelog';
+  return getJSON(url, data);
+}
+
+/**
+ * Compare two profiles
+ * @param {string} leftKey
+ * @param {string} rightKey
+ * @returns {Promise}
+ */
+export function compareProfiles (leftKey, rightKey) {
+  const url = '/api/qualityprofiles/compare';
+  const data = { leftKey, rightKey };
+  return getJSON(url, data);
+}
diff --git a/server/sonar-web/src/main/js/api/rules.js b/server/sonar-web/src/main/js/api/rules.js
new file mode 100644 (file)
index 0000000..825e114
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { getJSON } from '../helpers/request';
+
+export function searchRules (data) {
+  const url = '/api/rules/search';
+  return getJSON(url, data);
+}
+
+export function takeFacet (response, property) {
+  const facet = response.facets.find(facet => facet.property === property);
+  return facet ? facet.values : [];
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/utils-test.js
new file mode 100644 (file)
index 0000000..9781199
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { expect } from 'chai';
+import { sortProfiles } from '../utils';
+
+function createProfile (key, parentKey) {
+  return { name: key, key, parentKey };
+}
+
+function checkOrder (list, order) {
+  const listKeys = list.map(item => item.key);
+  expect(listKeys).to.deep.equal(order);
+}
+
+describe('Quality Profiles :: Utils', () => {
+  describe('#sortProfiles', () => {
+    it('should sort when no parents', () => {
+      const profile1 = createProfile('profile1');
+      const profile2 = createProfile('profile2');
+      const profile3 = createProfile('profile3');
+      checkOrder(
+          sortProfiles([profile1, profile2, profile3]),
+          ['profile1', 'profile2', 'profile3']
+      );
+    });
+
+    it('should sort by name', () => {
+      const profile1 = createProfile('profile1');
+      const profile2 = createProfile('profile2');
+      const profile3 = createProfile('profile3');
+      checkOrder(
+          sortProfiles([profile3, profile1, profile2]),
+          ['profile1', 'profile2', 'profile3']
+      );
+    });
+
+    it('should sort with children', () => {
+      const child1 = createProfile('child1', 'parent');
+      const child2 = createProfile('child2', 'parent');
+      const parent = createProfile('parent');
+      checkOrder(
+          sortProfiles([child1, child2, parent]),
+          ['parent', 'child1', 'child2']
+      );
+    });
+
+    it('should sort single branch', () => {
+      const profile1 = createProfile('profile1');
+      const profile2 = createProfile('profile2', 'profile3');
+      const profile3 = createProfile('profile3', 'profile1');
+      checkOrder(
+          sortProfiles([profile3, profile2, profile1]),
+          ['profile1', 'profile3', 'profile2']
+      );
+    });
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/actions-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/actions-view.js
deleted file mode 100644 (file)
index ae5c603..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import CreateProfileView from './create-profile-view';
-import RestoreProfileView from './restore-profile-view';
-import RestoreBuiltInProfilesView from './restore-built-in-profiles-view';
-import Template from './templates/quality-profiles-actions.hbs';
-
-export default Marionette.ItemView.extend({
-  template: Template,
-
-  events: {
-    'click #quality-profiles-create': 'onCreateClick',
-    'click #quality-profiles-restore': 'onRestoreClick',
-    'click #quality-profiles-restore-built-in': 'onRestoreBuiltInClick',
-
-    'click .js-filter-by-language': 'onLanguageClick'
-  },
-
-  onCreateClick (e) {
-    e.preventDefault();
-    this.create();
-  },
-
-  onRestoreClick (e) {
-    e.preventDefault();
-    this.restore();
-  },
-
-  onRestoreBuiltInClick (e) {
-    e.preventDefault();
-    this.restoreBuiltIn();
-  },
-
-  onLanguageClick (e) {
-    e.preventDefault();
-    const language = $(e.currentTarget).data('language');
-    this.filterByLanguage(language);
-  },
-
-  create () {
-    const that = this;
-    this.requestImporters().done(function () {
-      new CreateProfileView({
-        collection: that.collection,
-        languages: that.languages,
-        importers: that.importers
-      }).render();
-    });
-  },
-
-  restore () {
-    new RestoreProfileView({
-      collection: this.collection
-    }).render();
-  },
-
-  restoreBuiltIn () {
-    new RestoreBuiltInProfilesView({
-      collection: this.collection,
-      languages: this.languages
-    }).render();
-  },
-
-  requestLanguages () {
-    const that = this;
-    const url = window.baseUrl + '/api/languages/list';
-    return $.get(url).done(function (r) {
-      that.languages = r.languages;
-    });
-  },
-
-  requestImporters () {
-    const that = this;
-    const url = window.baseUrl + '/api/qualityprofiles/importers';
-    return $.get(url).done(function (r) {
-      that.importers = r.importers;
-    });
-  },
-
-  filterByLanguage (language) {
-    this.selectedLanguage = _.findWhere(this.languages, { key: language });
-    this.render();
-    this.collection.trigger('filter', language);
-  },
-
-  serializeData () {
-    return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
-      canWrite: this.options.canWrite,
-      languages: this.languages,
-      selectedLanguage: this.selectedLanguage
-    });
-  }
-});
-
index 74a8511776c7ae2cb6cc16205919af098268b23d..cb998c8edfce89d6f9293ca80e84b679e13c05f0 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import $ from 'jquery';
-import Backbone from 'backbone';
-import Marionette from 'backbone.marionette';
-import Router from './router';
-import Controller from './controller';
-import Layout from './layout';
-import Profiles from './profiles';
-import ActionsView from './actions-view';
-import ProfilesView from './profiles-view';
-
-const App = new Marionette.Application();
-const requestUser = $.get(window.baseUrl + '/api/users/current').done(function (r) {
-  App.canWrite = r.permissions.global.indexOf('profileadmin') !== -1;
-});
-const requestExporters = $.get(window.baseUrl + '/api/qualityprofiles/exporters').done(function (r) {
-  App.exporters = r.exporters;
-});
-const init = function () {
-  const options = window.sonarqube;
-
-  // Layout
-  this.layout = new Layout({ el: options.el });
-  this.layout.render();
-  $('#footer').addClass('search-navigator-footer');
-
-  // Profiles List
-  this.profiles = new Profiles();
-
-  // Controller
-  this.controller = new Controller({ app: this });
-
-  // Actions View
-  this.actionsView = new ActionsView({
-    collection: this.profiles,
-    canWrite: this.canWrite
-  });
-  this.actionsView.requestLanguages().done(function () {
-    App.layout.actionsRegion.show(App.actionsView);
+import React from 'react';
+import { render } from 'react-dom';
+import {
+    Router,
+    Route,
+    IndexRoute,
+    Redirect,
+    useRouterHistory
+} from 'react-router';
+import { createHistory } from 'history';
+import App from './components/App';
+import ProfileContainer from './components/ProfileContainer';
+import HomeContainer from './home/HomeContainer';
+import ProfileDetails from './details/ProfileDetails';
+import ChangelogContainer from './changelog/ChangelogContainer';
+import ComparisonContainer from './compare/ComparisonContainer';
+
+window.sonarqube.appStarted.then(options => {
+  const el = document.querySelector(options.el);
+
+  const history = useRouterHistory(createHistory)({
+    basename: window.baseUrl + '/profiles'
   });
 
-  // Profiles View
-  this.profilesView = new ProfilesView({
-    collection: this.profiles,
-    canWrite: this.canWrite
-  });
-  this.layout.resultsRegion.show(this.profilesView);
-
-  // Router
-  this.router = new Router({ app: this });
-  Backbone.history.start({
-    pushState: true,
-    root: options.urlRoot
-  });
-};
-
-App.on('start', function () {
-  $.when(requestUser, requestExporters).done(function () {
-    init.call(App);
-  });
+  render((
+      <Router history={history}>
+        <Route path="/" component={App}>
+          <Redirect from="/index" to="/"/>
+
+          <IndexRoute component={HomeContainer}/>
+
+          <Route component={ProfileContainer}>
+            <Route path="show" component={ProfileDetails}/>
+            <Route path="changelog" component={ChangelogContainer}/>
+            <Route path="compare" component={ComparisonContainer}/>
+          </Route>
+        </Route>
+      </Router>
+  ), el);
 });
-
-window.sonarqube.appStarted.then(options => App.start(options));
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/change-profile-parent-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/change-profile-parent-view.js
deleted file mode 100644 (file)
index e86554b..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import ModalFormView from '../../components/common/modal-form';
-import Template from './templates/quality-profiles-change-profile-parent.hbs';
-
-export default ModalFormView.extend({
-  template: Template,
-
-  onRender () {
-    ModalFormView.prototype.onRender.apply(this, arguments);
-    this.$('select').select2({
-      width: '250px',
-      minimumResultsForSearch: 50
-    });
-  },
-
-  onFormSubmit () {
-    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
-    this.disableForm();
-    this.sendRequest();
-  },
-
-  sendRequest () {
-    const that = this;
-    const url = window.baseUrl + '/api/qualityprofiles/change_parent';
-    const parent = this.$('#change-profile-parent').val();
-    const options = {
-      profileKey: this.model.get('key'),
-      parentKey: parent
-    };
-    return $.ajax({
-      url,
-      type: 'POST',
-      data: options,
-      statusCode: {
-        // do not show global error
-        400: null
-      }
-    }).done(function () {
-      that.model.collection.fetch();
-      that.model.trigger('select', that.model);
-      that.destroy();
-    }).fail(function (jqXHR) {
-      that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
-      that.enableForm();
-    });
-  },
-
-  serializeData () {
-    const that = this;
-    const profilesData = this.model.collection.toJSON();
-    const profiles = _.filter(profilesData, function (profile) {
-      return profile.language === that.model.get('language') && profile.key !== that.model.id;
-    });
-    return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
-      profiles
-    });
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.js
new file mode 100644 (file)
index 0000000..b7c8184
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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 moment from 'moment';
+import ChangesList from './ChangesList';
+import { translate } from '../../../helpers/l10n';
+import { getRulesUrl } from '../../../helpers/urls';
+
+export default class Changelog extends React.Component {
+  static propTypes = {
+    events: React.PropTypes.array.isRequired
+  };
+
+  render () {
+    const rows = this.props.events.map((event, index) => (
+        <tr key={index} className="js-profile-changelog-event">
+          <td className="thin nowrap">
+            {moment(event.date).format('LLL')}
+          </td>
+
+          <td className="thin nowrap">
+            {event.authorName ? (
+                <span>{event.authorName}</span>
+            ) : (
+                <span className="note">System</span>
+            )}
+          </td>
+
+          <td className="thin nowrap">
+            {translate('quality_profiles.changelog', event.action)}
+          </td>
+
+          <td style={{ lineHeight: '1.5' }}>
+            <a href={getRulesUrl({ 'rule_key': event.ruleKey })}>
+              {event.ruleName}
+            </a>
+          </td>
+
+          <td className="thin nowrap">
+            <ChangesList changes={event.params}/>
+          </td>
+        </tr>
+    ));
+
+    return (
+        <table className="data zebra zebra-hover">
+          <thead>
+            <tr>
+              <th className="thin nowrap">
+                {translate('date')}
+                {' '}
+                <i className="icon-sort-desc"/>
+              </th>
+              <th className="thin nowrap">{translate('user')}</th>
+              <th className="thin nowrap">{translate('action')}</th>
+              <th>{translate('rule')}</th>
+              <th className="thin nowrap">{translate('parameters')}</th>
+            </tr>
+          </thead>
+          <tbody>{rows}</tbody>
+        </table>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.js
new file mode 100644 (file)
index 0000000..247b3a6
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * 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 Changelog from './Changelog';
+import ChangelogSearch from './ChangelogSearch';
+import ChangelogEmpty from './ChangelogEmpty';
+import { getProfileChangelog } from '../../../api/quality-profiles';
+import { ProfileType } from '../propTypes';
+
+export default class ChangelogContainer extends React.Component {
+  static propTypes = {
+    location: React.PropTypes.object.isRequired,
+    profile: ProfileType
+  };
+
+  static contextTypes = {
+    router: React.PropTypes.object
+  };
+
+  state = {
+    loading: true
+  };
+
+  componentWillMount () {
+    this.handleFromDateChange = this.handleFromDateChange.bind(this);
+    this.handleToDateChange = this.handleToDateChange.bind(this);
+    this.handleReset = this.handleReset.bind(this);
+  }
+
+  componentDidMount () {
+    this.mounted = true;
+    this.loadChangelog();
+  }
+
+  componentDidUpdate (prevProps) {
+    if (prevProps.location !== this.props.location) {
+      this.loadChangelog();
+    }
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  loadChangelog () {
+    this.setState({ loading: true });
+    const { query } = this.props.location;
+    const data = { profileKey: this.props.profile.key };
+    if (query.since) {
+      data.since = query.since;
+    }
+    if (query.to) {
+      data.to = query.to;
+    }
+
+    getProfileChangelog(data).then(r => {
+      if (this.mounted) {
+        this.setState({
+          events: r.events,
+          total: r.total,
+          page: r.p,
+          loading: false
+        });
+      }
+    });
+  }
+
+  handleFromDateChange (fromDate) {
+    const query = { ...this.props.location.query, since: fromDate };
+    this.context.router.push({ pathname: '/changelog', query });
+  }
+
+  handleToDateChange (toDate) {
+    const query = { ...this.props.location.query, to: toDate };
+    this.context.router.push({ pathname: '/changelog', query });
+  }
+
+  handleReset () {
+    const query = { key: this.props.profile.key };
+    this.context.router.push({ pathname: '/changelog', query });
+  }
+
+  render () {
+    const { query } = this.props.location;
+
+    return (
+        <div className="quality-profile-box js-profile-changelog">
+          <header className="spacer-bottom">
+            <ChangelogSearch
+                fromDate={query.since}
+                toDate={query.to}
+                onFromDateChange={this.handleFromDateChange}
+                onToDateChange={this.handleToDateChange}
+                onReset={this.handleReset}/>
+
+            {this.state.loading && (
+                <i className="spinner spacer-left"/>
+            )}
+          </header>
+
+          {this.state.events != null && this.state.events.length === 0 && (
+              <ChangelogEmpty/>
+          )}
+
+          {this.state.events != null && this.state.events.length > 0 && (
+              <Changelog events={this.state.events}/>
+          )}
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogEmpty.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogEmpty.js
new file mode 100644 (file)
index 0000000..797e551
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export default class ChangelogEmpty extends React.Component {
+  render () {
+    return (
+      <div className="big-spacer-top">
+        {translate('no_results')}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogSearch.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogSearch.js
new file mode 100644 (file)
index 0000000..1093f7f
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import DateInput from '../../../components/controls/DateInput';
+import { translate } from '../../../helpers/l10n';
+
+export default class ChangelogSearch extends React.Component {
+  static propTypes = {
+    fromDate: React.PropTypes.string,
+    toDate: React.PropTypes.string,
+    onFromDateChange: React.PropTypes.func.isRequired,
+    onToDateChange: React.PropTypes.func.isRequired,
+    onReset: React.PropTypes.func.isRequired
+  };
+
+  handleResetClick (e) {
+    e.preventDefault();
+    e.target.blur();
+    this.props.onReset();
+  }
+
+  render () {
+    return (
+        <div className="display-inline-block"
+             id="quality-profile-changelog-form">
+          <DateInput
+              name="since"
+              value={this.props.fromDate}
+              placeholder="From"
+              onChange={this.props.onFromDateChange}/>
+          {' â€” '}
+          <DateInput
+              name="to"
+              value={this.props.toDate}
+              placeholder="To"
+              onChange={this.props.onToDateChange}/>
+          <button
+              className="spacer-left"
+              onClick={this.handleResetClick.bind(this)}>
+            {translate('reset_verb')}
+          </button>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangesList.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangesList.js
new file mode 100644 (file)
index 0000000..5a413b0
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import SeverityChange from './SeverityChange';
+import ParameterChange from './ParameterChange';
+
+export default class ChangesList extends React.Component {
+  static propTypes = {
+    changes: React.PropTypes.object.isRequired
+  };
+
+  render () {
+    const { changes } = this.props;
+
+    return (
+        <ul>
+          {Object.keys(changes).map(key => (
+              <li key={key}>
+                {key === 'severity' ? (
+                    <SeverityChange severity={changes[key]}/>
+                ) : (
+                    <ParameterChange name={key} value={changes[key]}/>
+                )}
+              </li>
+          ))}
+        </ul>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ParameterChange.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ParameterChange.js
new file mode 100644 (file)
index 0000000..82e685a
--- /dev/null
@@ -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 { translateWithParameters } from '../../../helpers/l10n';
+
+export default class ParameterChange extends React.Component {
+  static propTypes = {
+    name: React.PropTypes.string.isRequired,
+    value: React.PropTypes.any
+  };
+
+  render () {
+    const { name, value } = this.props;
+
+    if (value == null) {
+      return (
+          <div>
+            {translateWithParameters(
+                'quality_profiles.changelog.parameter_reset_to_default_value',
+                name
+            )}
+          </div>
+      );
+    }
+
+    return (
+        <div>
+          {translateWithParameters(
+              'quality_profiles.parameter_set_to',
+              name,
+              value
+          )}
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js
new file mode 100644 (file)
index 0000000..2b85431
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import SeverityHelper from '../../../components/shared/severity-helper';
+import { translate } from '../../../helpers/l10n';
+
+export default class SeverityChange extends React.Component {
+  static propTypes = {
+    severity: React.PropTypes.string.isRequired
+  };
+
+  render () {
+    return (
+        <div>
+          {translate('quality_profiles.severity_set_to')}
+          {' '}
+          <SeverityHelper severity={this.props.severity}/>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.js
new file mode 100644 (file)
index 0000000..9a36c64
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import Changelog from '../Changelog';
+import ChangesList from '../ChangesList';
+
+function createEvent (overrides) {
+  return {
+    date: '2016-01-01',
+    authorName: 'John',
+    action: 'ACTIVATED',
+    ruleKey: 'squid1234',
+    ruleName: 'Do not do this',
+    params: {},
+    ...overrides
+  };
+}
+
+describe('Quality Profiles :: Changelog', () => {
+  it('should render events', () => {
+    const events = [createEvent(), createEvent()];
+    const changelog = shallow(<Changelog events={events}/>);
+    expect(changelog.find('tbody').find('tr')).to.have.length(2);
+  });
+
+  it('should render event date', () => {
+    const events = [createEvent()];
+    const changelog = shallow(<Changelog events={events}/>);
+    expect(changelog.text()).to.include('2016');
+  });
+
+  it('should render author', () => {
+    const events = [createEvent()];
+    const changelog = shallow(<Changelog events={events}/>);
+    expect(changelog.text()).to.include('John');
+  });
+
+  it('should render system author', () => {
+    const events = [createEvent({ authorName: undefined })];
+    const changelog = shallow(<Changelog events={events}/>);
+    expect(changelog.text()).to.include('System');
+  });
+
+  it('should render action', () => {
+    const events = [createEvent()];
+    const changelog = shallow(<Changelog events={events}/>);
+    expect(changelog.text()).to.include('ACTIVATED');
+  });
+
+  it('should render rule', () => {
+    const events = [createEvent()];
+    const changelog = shallow(<Changelog events={events}/>);
+    expect(changelog.text()).to.include('Do not do this');
+    expect(changelog.find('a').prop('href')).to.include('rule_key=squid1234');
+  });
+
+  it('should render ChangesList', () => {
+    const params = { severity: 'BLOCKER' };
+    const events = [createEvent({ params })];
+    const changelog = shallow(<Changelog events={events}/>);
+    const changesList = changelog.find(ChangesList);
+    expect(changesList).to.have.length(1);
+    expect(changesList.prop('changes')).to.equal(params);
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogSearch-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogSearch-test.js
new file mode 100644 (file)
index 0000000..302273c
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import sinon from 'sinon';
+import React from 'react';
+import ChangelogSearch from '../ChangelogSearch';
+import DateInput from '../../../../components/controls/DateInput';
+
+function click (element) {
+  return element.simulate('click', {
+    target: { blur () {} },
+    preventDefault () {}
+  });
+}
+
+describe('Quality Profiles :: ChangelogSearch', () => {
+  it('should render DateInput', () => {
+    const onFromDateChange = sinon.spy();
+    const onToDateChange = sinon.spy();
+    const output = shallow(
+        <ChangelogSearch
+            fromDate="2016-01-01"
+            toDate="2016-05-05"
+            onFromDateChange={onFromDateChange}
+            onToDateChange={onToDateChange}
+            onReset={sinon.spy()}/>
+    );
+    const dateInputs = output.find(DateInput);
+    expect(dateInputs).to.have.length(2);
+    expect(dateInputs.at(0).prop('value')).to.equal('2016-01-01');
+    expect(dateInputs.at(0).prop('onChange')).to.equal(onFromDateChange);
+    expect(dateInputs.at(1).prop('value')).to.equal('2016-05-05');
+    expect(dateInputs.at(1).prop('onChange')).to.equal(onToDateChange);
+  });
+
+  it('should reset', () => {
+    const onReset = sinon.spy();
+    const output = shallow(
+        <ChangelogSearch
+            fromDate="2016-01-01"
+            toDate="2016-05-05"
+            onFromDateChange={sinon.spy()}
+            onToDateChange={sinon.spy()}
+            onReset={onReset}/>
+    );
+    expect(onReset.called).to.equal(false);
+    click(output.find('button'));
+    expect(onReset.called).to.equal(true);
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangesList-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangesList-test.js
new file mode 100644 (file)
index 0000000..86e194b
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import ChangesList from '../ChangesList';
+import SeverityChange from '../SeverityChange';
+import ParameterChange from '../ParameterChange';
+
+describe('Quality Profiles :: ChangesList', () => {
+  it('should render changes', () => {
+    const changes = { severity: 'BLOCKER', foo: 'bar' };
+    const output = shallow(
+        <ChangesList changes={changes}/>
+    );
+    expect(output.find('li')).to.have.length(2);
+  });
+
+  it('should render severity change', () => {
+    const changes = { severity: 'BLOCKER' };
+    const output = shallow(
+        <ChangesList changes={changes}/>
+    ).find(SeverityChange);
+    expect(output).to.have.length(1);
+    expect(output.prop('severity')).to.equal('BLOCKER');
+  });
+
+  it('should render parameter change', () => {
+    const changes = { foo: 'bar' };
+    const output = shallow(
+        <ChangesList changes={changes}/>
+    ).find(ParameterChange);
+    expect(output).to.have.length(1);
+    expect(output.prop('name')).to.equal('foo');
+    expect(output.prop('value')).to.equal('bar');
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ParameterChange-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ParameterChange-test.js
new file mode 100644 (file)
index 0000000..1845b09
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import ParameterChange from '../ParameterChange';
+
+describe('Quality Profiles :: ParameterChange', () => {
+  it('should render different messages', () => {
+    const first = shallow(<ParameterChange name="foo"/>);
+    const second = shallow(<ParameterChange name="foo" value="bar"/>);
+    expect(first.text()).to.not.be.equal(second.text());
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js
new file mode 100644 (file)
index 0000000..3a7fcb3
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import SeverityChange from '../SeverityChange';
+import SeverityHelper from '../../../../components/shared/severity-helper';
+
+describe('Quality Profiles :: SeverityChange', () => {
+  it('should render SeverityHelper', () => {
+    const output = shallow(
+        <SeverityChange severity="BLOCKER"/>
+    ).find(SeverityHelper);
+    expect(output).to.have.length(1);
+    expect(output.prop('severity')).to.equal('BLOCKER');
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.js
new file mode 100644 (file)
index 0000000..03d5d14
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * 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 ComparisonForm from './ComparisonForm';
+import ComparisonResults from './ComparisonResults';
+import { ProfileType, ProfilesListType } from '../propTypes';
+import { compareProfiles } from '../../../api/quality-profiles';
+
+export default class ComparisonContainer extends React.Component {
+  static propTypes = {
+    profile: ProfileType,
+    profiles: ProfilesListType
+  };
+
+  static contextTypes = {
+    router: React.PropTypes.object
+  };
+
+  state = {
+    loading: false
+  };
+
+  componentWillMount () {
+    this.handleCompare = this.handleCompare.bind(this);
+  }
+
+  componentDidMount () {
+    this.mounted = true;
+    this.loadResults();
+  }
+
+  componentDidUpdate (prevProps) {
+    if (prevProps.profile !== this.props.profile ||
+        prevProps.location !== this.props.location) {
+      this.loadResults();
+    }
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  loadResults () {
+    const { withKey } = this.props.location.query;
+    if (!withKey) {
+      this.setState({ left: null, loading: false });
+      return;
+    }
+
+    this.setState({ loading: true });
+    compareProfiles(this.props.profile.key, withKey).then(r => {
+      if (this.mounted) {
+        this.setState({
+          left: r.left,
+          right: r.right,
+          inLeft: r.inLeft,
+          inRight: r.inRight,
+          modified: r.modified,
+          loading: false
+        });
+      }
+    });
+  }
+
+  handleCompare (withKey) {
+    this.context.router.push({
+      pathname: '/compare',
+      query: {
+        key: this.props.profile.key,
+        withKey
+      }
+    });
+  }
+
+  render () {
+    const { profile, profiles, location } = this.props;
+    const { withKey } = location.query;
+    const { left, right, inLeft, inRight, modified } = this.state;
+
+    return (
+        <div className="quality-profile-box js-profile-comparison">
+          <header className="spacer-bottom">
+            <ComparisonForm
+                withKey={withKey}
+                profile={profile}
+                profiles={profiles}
+                onCompare={this.handleCompare}/>
+
+            {this.state.loading && (
+                <i className="spinner spacer-left"/>
+            )}
+          </header>
+
+          {left != null && (
+              <ComparisonResults
+                  left={left}
+                  right={right}
+                  inLeft={inLeft}
+                  inRight={inRight}
+                  modified={modified}/>
+          )}
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonEmpty.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonEmpty.js
new file mode 100644 (file)
index 0000000..d3e49ae
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export default class ComparisonEmpty extends React.Component {
+  render () {
+    return (
+      <div className="big-spacer-top">
+        {translate('quality_profile.empty_comparison')}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonForm.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonForm.js
new file mode 100644 (file)
index 0000000..149a809
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import Select from 'react-select';
+import { ProfileType, ProfilesListType } from '../propTypes';
+import { translate } from '../../../helpers/l10n';
+
+export default class ComparisonForm extends React.Component {
+  static propTypes = {
+    profile: ProfileType.isRequired,
+    profiles: ProfilesListType.isRequired,
+    onCompare: React.PropTypes.func.isRequired
+  };
+
+  handleChange (option) {
+    this.props.onCompare(option.value);
+  }
+
+  render () {
+    const { profile, profiles, withKey } = this.props;
+    const options = profiles
+        .filter(p => p.language === profile.language && p !== profile)
+        .map(p => ({ value: p.key, label: p.name }));
+
+    return (
+        <div className="display-inline-block">
+          <label className="spacer-right">
+            {translate('quality_profiles.compare_with')}
+          </label>
+          <Select
+              value={withKey}
+              options={options}
+              clearable={false}
+              className="input-large"
+              onChange={this.handleChange.bind(this)}/>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js
new file mode 100644 (file)
index 0000000..31bed46
--- /dev/null
@@ -0,0 +1,176 @@
+/*
+ * 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 ComparisonEmpty from './ComparisonEmpty';
+import SeverityIcon from '../../../components/shared/severity-icon';
+import { translateWithParameters } from '../../../helpers/l10n';
+import { getRulesUrl } from '../../../helpers/urls';
+
+export default class ComparisonResults extends React.Component {
+  static propTypes = {
+    left: React.PropTypes.shape({
+      name: React.PropTypes.string.isRequired
+    }).isRequired,
+    right: React.PropTypes.shape({
+      name: React.PropTypes.string.isRequired
+    }).isRequired,
+    inLeft: React.PropTypes.array.isRequired,
+    inRight: React.PropTypes.array.isRequired,
+    modified: React.PropTypes.array.isRequired
+  };
+
+  renderRule (rule, severity) {
+    return (
+        <div>
+          <SeverityIcon severity={severity}/>
+          {' '}
+          <a href={getRulesUrl({ 'rule_key': rule.key })}>
+            {rule.name}
+          </a>
+        </div>
+    );
+  }
+
+  renderParameters (params) {
+    if (!params) {
+      return null;
+    }
+    return (
+        <ul>
+          {Object.keys(params).map(key => (
+              <li key={key} className="spacer-top">
+                <code>{key}{': '}{params[key]}</code>
+              </li>
+          ))}
+        </ul>
+    );
+  }
+
+  renderLeft () {
+    if (this.props.inLeft.length === 0) {
+      return null;
+    }
+    const header = (
+        <tr key="left-header">
+          <td>
+            <h6>
+              {translateWithParameters(
+                  'quality_profiles.x_rules_only_in',
+                  this.props.inLeft.length
+              )}
+              {' '}
+              {this.props.left.name}
+            </h6>
+          </td>
+          <td>&nbsp;</td>
+        </tr>
+    );
+    const rows = this.props.inLeft.map(rule => (
+        <tr key={`left-${rule.key}`} className="js-comparison-in-left">
+          <td>{this.renderRule(rule, rule.severity)}</td>
+          <td>&nbsp;</td>
+        </tr>
+    ));
+    return [header, ...rows];
+  }
+
+  renderRight () {
+    if (this.props.inRight.length === 0) {
+      return null;
+    }
+    const header = (
+        <tr key="right-header">
+          <td>&nbsp;</td>
+          <td>
+            <h6>
+              {translateWithParameters(
+                  'quality_profiles.x_rules_only_in',
+                  this.props.inRight.length
+              )}
+              {' '}
+              {this.props.right.name}
+            </h6>
+          </td>
+        </tr>
+    );
+    const rows = this.props.inRight.map(rule => (
+        <tr key={`right-${rule.key}`}
+            className="js-comparison-in-right">
+          <td>&nbsp;</td>
+          <td>{this.renderRule(rule, rule.severity)}</td>
+        </tr>
+    ));
+    return [header, ...rows];
+  }
+
+  renderModified () {
+    if (this.props.modified.length === 0) {
+      return null;
+    }
+    const header = (
+        <tr key="modified-header">
+          <td colSpan="2" className="text-center">
+            <h6>
+              {translateWithParameters(
+                  'quality_profiles.x_rules_have_different_configuration',
+                  this.props.modified.length
+              )}
+            </h6>
+          </td>
+        </tr>
+    );
+    const secondHeader = (
+        <tr key="modified-second-header">
+          <td><h6>{this.props.left.name}</h6></td>
+          <td><h6>{this.props.right.name}</h6></td>
+        </tr>
+    );
+    const rows = this.props.modified.map(rule => (
+        <tr key={`modified-${rule.key}`}
+            className="js-comparison-modified">
+          <td>
+            {this.renderRule(rule, rule.left.severity)}
+            {this.renderParameters(rule.left.params)}
+          </td>
+          <td>
+            {this.renderRule(rule, rule.right.severity)}
+            {this.renderParameters(rule.right.params)}
+          </td>
+        </tr>
+    ));
+    return [header, secondHeader, ...rows];
+  }
+
+  render () {
+    if (!this.props.inLeft.length && !this.props.inRight.length && !this.props.modified.length) {
+      return <ComparisonEmpty/>;
+    }
+
+    return (
+        <table className="data zebra quality-profile-comparison-table">
+          <tbody>
+            {this.renderLeft()}
+            {this.renderRight()}
+            {this.renderModified()}
+          </tbody>
+        </table>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonForm-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonForm-test.js
new file mode 100644 (file)
index 0000000..4282233
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import Select from 'react-select';
+import ComparisonForm from '../ComparisonForm';
+import { createFakeProfile } from '../../utils';
+
+describe('Quality Profiles :: ComparisonForm', () => {
+  it('should render Select with right options', () => {
+    const profile = createFakeProfile();
+    const profiles = [
+      profile,
+      createFakeProfile({ key: 'another', name: 'another name' }),
+      createFakeProfile({ key: 'java', name: 'java', language: 'java' })
+    ];
+
+    const output = shallow(
+        <ComparisonForm
+            withKey="another"
+            profile={profile}
+            profiles={profiles}
+            onCompare={() => true}/>
+    ).find(Select);
+    expect(output).to.have.length(1);
+    expect(output.prop('value')).to.equal('another');
+    expect(output.prop('options')).to.deep.equal([
+      { value: 'another', label: 'another name' }
+    ]);
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js
new file mode 100644 (file)
index 0000000..0482bcb
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import ComparisonResults from '../ComparisonResults';
+import ComparisonEmpty from '../ComparisonEmpty';
+import SeverityIcon from '../../../../components/shared/severity-icon';
+
+describe('Quality Profiles :: ComparisonResults', () => {
+  it('should render ComparisonEmpty', () => {
+    const output = shallow(
+        <ComparisonResults
+            left={{ name: 'left' }}
+            right={{ name: 'right' }}
+            inLeft={[]}
+            inRight={[]}
+            modified={[]}/>
+    );
+    expect(output.is(ComparisonEmpty)).to.equal(true);
+  });
+
+  it('should compare', () => {
+    const inLeft = [
+      { key: 'rule1', name: 'rule1', severity: 'BLOCKER' }
+    ];
+    const inRight = [
+      { key: 'rule2', name: 'rule2', severity: 'CRITICAL' },
+      { key: 'rule3', name: 'rule3', severity: 'MAJOR' }
+    ];
+    const modified = [
+      {
+        key: 'rule4',
+        name: 'rule4',
+        left: {
+          severity: 'BLOCKER',
+          params: { foo: 'bar' }
+        },
+        right: {
+          severity: 'INFO',
+          params: { foo: 'qwe' }
+        }
+      }
+    ];
+
+    const output = shallow(
+        <ComparisonResults
+            left={{ name: 'left' }}
+            right={{ name: 'right' }}
+            inLeft={inLeft}
+            inRight={inRight}
+            modified={modified}/>
+    );
+
+    const leftDiffs = output.find('.js-comparison-in-left');
+    expect(leftDiffs).to.have.length(1);
+    expect(leftDiffs.find('a')).to.have.length(1);
+    expect(leftDiffs.find('a').prop('href')).to.include('rule_key=rule1');
+    expect(leftDiffs.find('a').text()).to.include('rule1');
+    expect(leftDiffs.find(SeverityIcon)).to.have.length(1);
+    expect(leftDiffs.find(SeverityIcon).prop('severity')).to.equal('BLOCKER');
+
+    const rightDiffs = output.find('.js-comparison-in-right');
+    expect(rightDiffs).to.have.length(2);
+    expect(rightDiffs.at(0).find('a')).to.have.length(1);
+    expect(rightDiffs.at(0).find('a').prop('href'))
+        .to.include('rule_key=rule2');
+    expect(rightDiffs.at(0).find('a').text()).to.include('rule2');
+    expect(rightDiffs.at(0).find(SeverityIcon)).to.have.length(1);
+    expect(rightDiffs.at(0).find(SeverityIcon).prop('severity'))
+        .to.equal('CRITICAL');
+
+    const modifiedDiffs = output.find('.js-comparison-modified');
+    expect(modifiedDiffs).to.have.length(1);
+    expect(modifiedDiffs.find('a').at(0).prop('href')).to.include('rule_key=rule4');
+    expect(modifiedDiffs.find('a').at(0).text()).to.include('rule4');
+    expect(modifiedDiffs.find(SeverityIcon)).to.have.length(2);
+    expect(modifiedDiffs.text()).to.include('bar');
+    expect(modifiedDiffs.text()).to.include('qwe');
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/App.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/App.js
new file mode 100644 (file)
index 0000000..30d5932
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { getLanguages } from '../../../api/languages';
+import {
+    getQualityProfiles,
+    getExporters
+} from '../../../api/quality-profiles';
+import { getCurrentUser } from '../../../api/users';
+import '../styles.css';
+import { sortProfiles } from '../utils';
+
+export default class App extends React.Component {
+  state = { loading: true };
+
+  componentWillMount () {
+    this.updateProfiles = this.updateProfiles.bind(this);
+  }
+
+  componentDidMount () {
+    this.mounted = true;
+    this.loadData();
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  loadData () {
+    this.setState({ loading: true });
+    Promise.all([
+      getCurrentUser(),
+      getLanguages(),
+      getExporters(),
+      getQualityProfiles()
+    ]).then(responses => {
+      if (this.mounted) {
+        const [user, languages, exporters, profiles] = responses;
+        const canAdmin = user.permissions.global.includes('profileadmin');
+        this.setState({
+          languages,
+          exporters,
+          canAdmin,
+          profiles: sortProfiles(profiles),
+          loading: false
+        });
+      }
+    });
+  }
+
+  updateProfiles () {
+    return getQualityProfiles().then(profiles => {
+      if (this.mounted) {
+        this.setState({ profiles: sortProfiles(profiles) });
+      }
+    });
+  }
+
+  renderChild () {
+    if (this.state.loading) {
+      return <i className="spinner"/>;
+    }
+
+    return React.cloneElement(this.props.children, {
+      profiles: this.state.profiles,
+      languages: this.state.languages,
+      exporters: this.state.exporters,
+      canAdmin: this.state.canAdmin,
+      updateProfiles: this.updateProfiles
+    });
+  }
+
+  render () {
+    return (
+        <div className="page page-limited-small">
+          {this.renderChild()}
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.js
new file mode 100644 (file)
index 0000000..d1fe79c
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * 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 Helmet from 'react-helmet';
+import ProfileNotFound from './ProfileNotFound';
+import ProfileHeader from '../details/ProfileHeader';
+import { translate } from '../../../helpers/l10n';
+import { ProfilesListType } from '../propTypes';
+
+export default class ProfileContainer extends React.Component {
+  static propTypes = {
+    location: React.PropTypes.object,
+    profiles: ProfilesListType,
+    canAdmin: React.PropTypes.bool,
+    updateProfiles: React.PropTypes.func
+  };
+
+  componentWillMount () {
+    document.querySelector('html').classList.add('dashboard-page');
+  }
+
+  componentWillUnmount () {
+    document.querySelector('html').classList.remove('dashboard-page');
+  }
+
+  render () {
+    const { profiles, location, ...other } = this.props;
+    const { key } = location.query;
+    const profile = profiles.find(profile => profile.key === key);
+
+    if (!profile) {
+      return <ProfileNotFound/>;
+    }
+
+    const child = React.cloneElement(
+        this.props.children,
+        { profile, profiles, ...other });
+
+    const title = translate('quality_profiles.page') + ' - ' + profile.name;
+
+    return (
+        <div>
+          <Helmet
+              title={title}
+              titleTemplate="SonarQube - %s"/>
+
+          <ProfileHeader
+              profile={profile}
+              canAdmin={this.props.canAdmin}
+              updateProfiles={this.props.updateProfiles}/>
+          {child}
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.js
new file mode 100644 (file)
index 0000000..974f2d8
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * 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 moment from 'moment';
+import shallowCompare from 'react-addons-shallow-compare';
+import { translate } from '../../../helpers/l10n';
+
+export default class ProfileDate extends React.Component {
+  static propTypes = {
+    date: React.PropTypes.string
+  };
+
+  shouldComponentUpdate (nextProps, nextState) {
+    return shallowCompare(this, nextProps, nextState);
+  }
+
+  render () {
+    const { date } = this.props;
+
+    if (!date) {
+      return (
+          <span className="note">{translate('never')}</span>
+      );
+    }
+
+    return (
+        <span title={moment(date).format('LLL')} data-toggle="tooltip">
+          {moment(date).fromNow()}
+        </span>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.js
new file mode 100644 (file)
index 0000000..eda61f6
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { Link } from 'react-router';
+
+export default class ProfileLink extends React.Component {
+  static propTypes = {
+    profileKey: React.PropTypes.string.isRequired
+  };
+
+  render () {
+    const { profileKey, children, ...other } = this.props;
+    const query = { key: profileKey };
+    return (
+        <Link to={{ pathname: '/show', query }} {...other}>
+          {children}
+        </Link>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.js
new file mode 100644 (file)
index 0000000..9c37677
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { IndexLink } from 'react-router';
+import { translate } from '../../../helpers/l10n';
+
+export default class ProfileNotFound extends React.Component {
+  render () {
+    return (
+        <div className="quality-profile-not-found">
+          <div className="note spacer-bottom">
+            <IndexLink to="/" className="text-muted">
+              {translate('quality_profiles.page')}
+            </IndexLink>
+          </div>
+
+          <div>
+            {translate('quality_profiles.not_found')}
+          </div>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.js
new file mode 100644 (file)
index 0000000..87dacc7
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import sinon from 'sinon';
+import React from 'react';
+import Helmet from 'react-helmet';
+import ProfileContainer from '../ProfileContainer';
+import ProfileNotFound from '../ProfileNotFound';
+import ProfileHeader from '../../details/ProfileHeader';
+import { createFakeProfile } from '../../utils';
+
+describe('Quality Profiles :: ProfileContainer', () => {
+  it('should render ProfileHeader', () => {
+    const targetProfile = createFakeProfile({ key: 'profile1' });
+    const profiles = [
+      targetProfile,
+      createFakeProfile({ key: 'profile2' })
+    ];
+    const updateProfiles = sinon.spy();
+    const output = shallow(
+        <ProfileContainer
+            location={{ query: { key: 'profile1' } }}
+            profiles={profiles}
+            canAdmin={false}
+            updateProfiles={updateProfiles}>
+          <div/>
+        </ProfileContainer>
+    );
+    const header = output.find(ProfileHeader);
+    expect(header).to.have.length(1);
+    expect(header.prop('profile')).to.equal(targetProfile);
+    expect(header.prop('canAdmin')).to.equal(false);
+    expect(header.prop('updateProfiles')).to.equal(updateProfiles);
+  });
+
+  it('should render ProfileNotFound', () => {
+    const profiles = [
+      createFakeProfile({ key: 'profile1' }),
+      createFakeProfile({ key: 'profile2' })
+    ];
+    const output = shallow(
+        <ProfileContainer
+            location={{ query: { key: 'random' } }}
+            profiles={profiles}
+            canAdmin={false}
+            updateProfiles={() => true}>
+          <div/>
+        </ProfileContainer>
+    );
+    expect(output.is(ProfileNotFound)).to.equal(true);
+  });
+
+  it('should render Helmet', () => {
+    const profiles = [
+      createFakeProfile({ key: 'profile1', name: 'First Profile' })
+    ];
+    const updateProfiles = sinon.spy();
+    const output = shallow(
+        <ProfileContainer
+            location={{ query: { key: 'profile1' } }}
+            profiles={profiles}
+            canAdmin={false}
+            updateProfiles={updateProfiles}>
+          <div/>
+        </ProfileContainer>
+    );
+    const helmet = output.find(Helmet);
+    expect(helmet).to.have.length(1);
+    expect(helmet.prop('title')).to.include('First Profile');
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/controller.js b/server/sonar-web/src/main/js/apps/quality-profiles/controller.js
deleted file mode 100644 (file)
index e3303e5..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import ProfileHeaderView from './profile-header-view';
-import ProfileDetailsView from './profile-details-view';
-
-export default Marionette.Controller.extend({
-
-  initialize () {
-    this.listenTo(this.options.app.profiles, 'select', this.onProfileSelect);
-    this.listenTo(this.options.app.profiles, 'setAsDefault', this.onProfileSetAsDefault);
-    this.listenTo(this.options.app.profiles, 'destroy', this.onProfileDestroy);
-  },
-
-  index () {
-    this.fetchProfiles();
-  },
-
-  show (key) {
-    const that = this;
-    this.fetchProfiles().done(function () {
-      const profile = that.options.app.profiles.findWhere({ key });
-      if (profile != null) {
-        profile.trigger('select', profile, { trigger: false });
-      }
-    });
-  },
-
-  changelog (key, since, to) {
-    const that = this;
-    this.anchor = 'changelog';
-    this.fetchProfiles().done(function () {
-      const profile = that.options.app.profiles.findWhere({ key });
-      if (profile != null) {
-        profile.trigger('select', profile, { trigger: false });
-        profile.fetchChangelog({ since, to });
-      }
-    });
-  },
-
-  compare (key, withKey) {
-    const that = this;
-    this.anchor = 'comparison';
-    this.fetchProfiles().done(function () {
-      const profile = that.options.app.profiles.findWhere({ key });
-      if (profile != null) {
-        profile.trigger('select', profile, { trigger: false });
-        profile.compareWith(withKey);
-      }
-    });
-  },
-
-  onProfileSelect (profile, options) {
-    const that = this;
-    const key = profile.get('key');
-    const route = 'show?key=' + encodeURIComponent(key);
-    const opts = _.defaults(options || {}, { trigger: true });
-    if (opts.trigger) {
-      this.options.app.router.navigate(route);
-    }
-    this.options.app.profilesView.highlight(key);
-    this.fetchProfile(profile).done(function () {
-      const profileHeaderView = new ProfileHeaderView({
-        model: profile,
-        canWrite: that.options.app.canWrite
-      });
-      that.options.app.layout.headerRegion.show(profileHeaderView);
-
-      const profileDetailsView = new ProfileDetailsView({
-        model: profile,
-        canWrite: that.options.app.canWrite,
-        exporters: that.options.app.exporters,
-        anchor: that.anchor
-      });
-      that.options.app.layout.detailsRegion.show(profileDetailsView);
-
-      that.anchor = null;
-    });
-  },
-
-  onProfileSetAsDefault (profile) {
-    const that = this;
-    const url = window.baseUrl + '/api/qualityprofiles/set_default';
-    const key = profile.get('key');
-    const options = { profileKey: key };
-    return $.post(url, options).done(function () {
-      profile.set({ isDefault: true });
-      that.fetchProfiles();
-    });
-  },
-
-  onProfileDestroy () {
-    this.options.app.router.navigate('');
-    this.options.app.layout.headerRegion.reset();
-    this.options.app.layout.detailsRegion.reset();
-    this.options.app.layout.renderIntro();
-    this.options.app.profilesView.highlight(null);
-  },
-
-  fetchProfiles () {
-    return this.options.app.profiles.fetch({ reset: true });
-  },
-
-  fetchProfile (profile) {
-    return profile.fetch();
-  }
-
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/copy-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/copy-profile-view.js
deleted file mode 100644 (file)
index e391f8b..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import ModalFormView from '../../components/common/modal-form';
-import Profile from './profile';
-import Template from './templates/quality-profiles-copy-profile.hbs';
-
-export default ModalFormView.extend({
-  template: Template,
-
-  onFormSubmit () {
-    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
-    this.disableForm();
-    this.sendRequest();
-  },
-
-  sendRequest () {
-    const that = this;
-    const url = window.baseUrl + '/api/qualityprofiles/copy';
-    const name = this.$('#copy-profile-name').val();
-    const options = {
-      fromKey: this.model.get('key'),
-      toName: name
-    };
-    return $.ajax({
-      url,
-      type: 'POST',
-      data: options,
-      statusCode: {
-        // do not show global error
-        400: null
-      }
-    }).done(function (r) {
-      that.addProfile(r);
-      that.destroy();
-    }).fail(function (jqXHR) {
-      that.enableForm();
-      that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
-    });
-  },
-
-  addProfile (profileData) {
-    const profile = new Profile(profileData);
-    this.model.collection.add([profile]);
-    profile.trigger('select', profile);
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/create-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/create-profile-view.js
deleted file mode 100644 (file)
index 68aa252..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import ModalFormView from '../../components/common/modal-form';
-import Profile from './profile';
-import Template from './templates/quality-profiles-create-profile.hbs';
-import { createQualityProfile } from '../../api/quality-profiles';
-
-export default ModalFormView.extend({
-  template: Template,
-
-  events () {
-    return _.extend(ModalFormView.prototype.events.apply(this, arguments), {
-      'change #create-profile-language': 'onLanguageChange'
-    });
-  },
-
-  onFormSubmit () {
-    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
-
-    const form = this.$('form')[0];
-    const data = new FormData(form);
-
-    createQualityProfile(data)
-        .then(r => {
-          this.addProfile(r.profile);
-          this.destroy();
-        })
-        .catch(e => {
-          e.response.json().then(r => this.showErrors(r.errors, r.warnings));
-        });
-  },
-
-  onRender () {
-    ModalFormView.prototype.onRender.apply(this, arguments);
-    this.$('select').select2({
-      width: '250px',
-      minimumResultsForSearch: 50
-    });
-    this.onLanguageChange();
-  },
-
-  onLanguageChange () {
-    const that = this;
-    const language = this.$('#create-profile-language').val();
-    const importers = this.getImportersForLanguages(language);
-    this.$('.js-importer').each(function () {
-      that.emptyInput($(this));
-      $(this).addClass('hidden');
-    });
-    importers.forEach(function (importer) {
-      that.$(`.js-importer[data-key="${importer.key}"]`).removeClass('hidden');
-    });
-  },
-
-  emptyInput (e) {
-    e.wrap('<form>').closest('form').get(0).reset();
-    e.unwrap();
-  },
-
-  addProfile (profileData) {
-    const profile = new Profile(profileData);
-    this.collection.add([profile]);
-    profile.trigger('select', profile);
-  },
-
-  getImportersForLanguages (language) {
-    if (language != null) {
-      return this.options.importers.filter(function (importer) {
-        return importer.languages.indexOf(language) !== -1;
-      });
-    } else {
-      return [];
-    }
-  },
-
-  serializeData () {
-    return _.extend(ModalFormView.prototype.serializeData.apply(this, arguments), {
-      languages: this.options.languages,
-      importers: this.options.importers
-    });
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/delete-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/delete-profile-view.js
deleted file mode 100644 (file)
index b75d930..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import ModalFormView from '../../components/common/modal-form';
-import Template from './templates/quality-profiles-delete-profile.hbs';
-
-export default ModalFormView.extend({
-  template: Template,
-
-  modelEvents: {
-    'destroy': 'destroy'
-  },
-
-  onFormSubmit () {
-    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
-    this.disableForm();
-    this.sendRequest();
-  },
-
-  sendRequest () {
-    const that = this;
-    const url = window.baseUrl + '/api/qualityprofiles/delete';
-    const options = { profileKey: this.model.get('key') };
-    return $.ajax({
-      url,
-      type: 'POST',
-      data: options,
-      statusCode: {
-        // do not show global error
-        400: null
-      }
-    }).done(function () {
-      that.model.collection.fetch();
-      that.model.trigger('destroy', that.model, that.model.collection);
-    }).fail(function (jqXHR) {
-      that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
-      that.enableForm();
-    });
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js
new file mode 100644 (file)
index 0000000..f08eb86
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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 ProfileRules from './ProfileRules';
+import ProfileProjects from './ProfileProjects';
+import ProfileInheritance from './ProfileInheritance';
+import ProfileEvolution from './ProfileEvolution';
+import ProfileExporters from './ProfileExporters';
+import { ProfileType } from '../propTypes';
+
+export default class ProfileDetails extends React.Component {
+  static propTypes = {
+    profile: ProfileType,
+    canAdmin: React.PropTypes.bool,
+    updateProfiles: React.PropTypes.func
+  };
+
+  render () {
+    return (
+        <div>
+          <div className="quality-profile-grid">
+            <div className="quality-profile-grid-left">
+              <ProfileRules {...this.props}/>
+              <ProfileExporters {...this.props}/>
+              <ProfileEvolution {...this.props}/>
+            </div>
+            <div className="quality-profile-grid-right">
+              <ProfileInheritance {...this.props}/>
+              <ProfileProjects {...this.props}/>
+            </div>
+          </div>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileEvolution.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileEvolution.js
new file mode 100644 (file)
index 0000000..aa643f6
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * 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 moment from 'moment';
+import { translate } from '../../../helpers/l10n';
+
+export default class ProfileEvolution extends React.Component {
+  render () {
+    const { profile } = this.props;
+
+    return (
+        <div className="quality-profile-evolution">
+          <div>
+            <h6 className="little-spacer-bottom">
+              {translate('quality_profiles.list.updated')}
+            </h6>
+            {profile.userUpdatedAt ? (
+                <div>
+                  {moment(profile.userUpdatedAt).format('LL')}
+                </div>
+            ) : (
+                <div className="note">
+                  {translate('never')}
+                </div>
+            )}
+          </div>
+          <div>
+            <h6 className="little-spacer-bottom">
+              {translate('quality_profiles.list.used')}
+            </h6>
+            {profile.lastUsed ? (
+                <div>
+                  {moment(profile.lastUsed).format('LL')}
+                </div>
+            ) : (
+                <div className="note">
+                  {translate('never')}
+                </div>
+            )}
+          </div>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.js
new file mode 100644 (file)
index 0000000..bb18ba7
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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 ProfileExporters extends React.Component {
+  static propTypes = {
+    exporters: React.PropTypes.array.isRequired
+  };
+
+  getExportUrl (exporter) {
+    return window.baseUrl + '/api/qualityprofiles/export' +
+        '?exporterKey=' + encodeURIComponent(exporter.key) +
+        '&language=' + encodeURIComponent(this.props.profile.language) +
+        '&name=' + encodeURIComponent(this.props.profile.name);
+  }
+
+  render () {
+    const { exporters, profile } = this.props;
+    const exportersForLanguage = exporters.filter(e => (
+        e.languages.includes(profile.language)
+    ));
+
+    if (exportersForLanguage.length === 0) {
+      return null;
+    }
+
+    return (
+        <div className="quality-profile-box quality-profile-exporters">
+          <header className="big-spacer-bottom">
+            <h2>{translate('quality_profiles.exporters')}</h2>
+          </header>
+          <ul>
+            {exportersForLanguage.map(exporter => (
+                <li key={exporter.key}
+                    data-key={exporter.key}
+                    className="spacer-top">
+                  <a href={this.getExportUrl(exporter)} target="_blank">
+                    {exporter.name}
+                  </a>
+                </li>
+            ))}
+          </ul>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.js
new file mode 100644 (file)
index 0000000..1062f4c
--- /dev/null
@@ -0,0 +1,183 @@
+/*
+ * 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 { Link, IndexLink } from 'react-router';
+import ProfileLink from '../components/ProfileLink';
+import RenameProfileView from '../views/RenameProfileView';
+import CopyProfileView from '../views/CopyProfileView';
+import DeleteProfileView from '../views/DeleteProfileView';
+import { ProfileType } from '../propTypes';
+import { translate } from '../../../helpers/l10n';
+import { setDefaultProfile } from '../../../api/quality-profiles';
+
+export default class ProfileHeader extends React.Component {
+  static propTypes = {
+    profile: ProfileType.isRequired,
+    canAdmin: React.PropTypes.bool.isRequired,
+    updateProfiles: React.PropTypes.func.isRequired
+  };
+
+  static contextTypes = {
+    router: React.PropTypes.object
+  };
+
+  handleRenameClick (e) {
+    e.preventDefault();
+    new RenameProfileView({
+      profile: this.props.profile
+    }).on('done', () => {
+      this.props.updateProfiles();
+    }).render();
+  }
+
+  handleCopyClick (e) {
+    e.preventDefault();
+    new CopyProfileView({
+      profile: this.props.profile
+    }).on('done', profile => {
+      this.props.updateProfiles().then(() => {
+        this.context.router.push({
+          pathname: '/show',
+          query: { key: profile.key }
+        });
+      });
+    }).render();
+  }
+
+  handleSetDefaultClick (e) {
+    e.preventDefault();
+    setDefaultProfile(this.props.profile.key)
+        .then(this.props.updateProfiles);
+  }
+
+  handleDeleteClick (e) {
+    e.preventDefault();
+    new DeleteProfileView({
+      profile: this.props.profile
+    }).on('done', () => {
+      this.context.router.replace('/');
+      this.props.updateProfiles();
+    }).render();
+  }
+
+  render () {
+    const { profile, canAdmin } = this.props;
+
+    const backupUrl = window.baseUrl +
+        '/api/qualityprofiles/backup?profileKey=' +
+        encodeURIComponent(profile.key);
+
+    // TODO fix inline styles
+
+    return (
+        <header className="page-header quality-profile-header">
+          <div className="note spacer-bottom">
+            <IndexLink to="/" className="text-muted">
+              {translate('quality_profiles.page')}
+            </IndexLink>
+          </div>
+
+          <h1 className="page-title">
+            <ProfileLink
+                profileKey={this.props.profile.key}
+                className="link-base-color">
+              {profile.name}
+            </ProfileLink>
+            <span className="spacer-left small text-muted">
+              {this.props.profile.languageName}
+            </span>
+          </h1>
+
+          <div className="pull-right">
+            <ul className="list-inline" style={{ lineHeight: '24px' }}>
+              <li>
+                <Link
+                    to={{ pathname: '/changelog', query: { key: this.props.profile.key } }}
+                    className="small text-muted"
+                    activeClassName="link-active">
+                  {translate('changelog')}
+                </Link>
+              </li>
+              <li>
+                <div className="pull-left dropdown">
+                  <button className="dropdown-toggle"
+                          data-toggle="dropdown">
+                    {translate('actions')}
+                    {' '}
+                    <i className="icon-dropdown"/>
+                  </button>
+                  <ul className="dropdown-menu dropdown-menu-right">
+                    <li>
+                      <Link
+                          to={{ pathname: '/compare', query: { key: profile.key } }}
+                          id="quality-profile-compare">
+                        {translate('compare')}
+                      </Link>
+                    </li>
+                    <li>
+                      <a id="quality-profile-backup" href={backupUrl}>
+                        {translate('backup_verb')}
+                      </a>
+                    </li>
+                    {canAdmin && (
+                        <li>
+                          <a id="quality-profile-rename"
+                             href="#"
+                             onClick={this.handleRenameClick.bind(this)}>
+                            {translate('rename')}
+                          </a>
+                        </li>
+                    )}
+                    {canAdmin && (
+                        <li>
+                          <a id="quality-profile-copy"
+                             href="#"
+                             onClick={this.handleCopyClick.bind(this)}>
+                            {translate('copy')}
+                          </a>
+                        </li>
+                    )}
+                    {canAdmin && !profile.isDefault && (
+                        <li>
+                          <a id="quality-profile-set-as-default"
+                             href="#"
+                             onClick={this.handleSetDefaultClick.bind(this)}>
+                            {translate('set_as_default')}
+                          </a>
+                        </li>
+                    )}
+                    {canAdmin && !profile.isDefault && (
+                        <li>
+                          <a id="quality-profile-delete"
+                             href="#"
+                             onClick={this.handleDeleteClick.bind(this)}>
+                            {translate('delete')}
+                          </a>
+                        </li>
+                    )}
+                  </ul>
+                </div>
+              </li>
+            </ul>
+          </div>
+        </header>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.js
new file mode 100644 (file)
index 0000000..1d97203
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * 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 ProfileInheritanceBox from './ProfileInheritanceBox';
+import ChangeParentView from '../views/ChangeParentView';
+import { ProfileType, ProfilesListType } from '../propTypes';
+import { translate } from '../../../helpers/l10n';
+import { getProfileInheritance } from '../../../api/quality-profiles';
+
+export default class ProfileInheritance extends React.Component {
+  static propTypes = {
+    profile: ProfileType.isRequired,
+    canAdmin: React.PropTypes.bool.isRequired
+  };
+
+  state = {
+    loading: true
+  };
+
+  componentWillMount () {
+    this.handleChangeParent = this.handleChangeParent.bind(this);
+  }
+
+  componentDidMount () {
+    this.mounted = true;
+    this.loadData();
+  }
+
+  componentDidUpdate (prevProps) {
+    if (prevProps.profile !== this.props.profile) {
+      this.loadData();
+    }
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  loadData () {
+    getProfileInheritance(this.props.profile.key).then(r => {
+      if (this.mounted) {
+        const { ancestors, children } = r;
+        this.setState({
+          children,
+          ancestors: ancestors.reverse(),
+          profile: r.profile,
+          loading: false
+        });
+      }
+    });
+  }
+
+  handleChangeParent (e) {
+    e.preventDefault();
+    new ChangeParentView({
+      profile: this.props.profile,
+      profiles: this.props.profiles
+    }).on('done', () => {
+      this.props.updateProfiles();
+    }).render();
+  }
+
+  render () {
+    return (
+        <div className="quality-profile-inheritance">
+          <header className="big-spacer-bottom clearfix">
+            <h2 className="pull-left">
+              {translate('quality_profiles.profile_inheritance')}
+            </h2>
+            {this.props.canAdmin && (
+                <button
+                    className="pull-right js-change-parent"
+                    onClick={this.handleChangeParent}>
+                  {translate('quality_profiles.change_parent')}
+                </button>
+            )}
+          </header>
+
+          {!this.state.loading && (
+              <table className="data condensed zebra">
+                <tbody>
+                  {this.state.ancestors.map((ancestor, index) => (
+                      <ProfileInheritanceBox
+                          key={ancestor.key}
+                          profile={ancestor}
+                          depth={index}
+                          className="js-inheritance-ancestor"/>
+                  ))}
+
+                  <ProfileInheritanceBox
+                      profile={this.state.profile}
+                      depth={this.state.ancestors.length}
+                      displayLink={false}
+                      className="js-inheritance-current"/>
+
+                  {this.state.children.map(child => (
+                      <ProfileInheritanceBox
+                          key={child.key}
+                          profile={child}
+                          depth={this.state.ancestors.length + 1}
+                          className="js-inheritance-child"/>
+                  ))}
+                </tbody>
+              </table>
+          )}
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.js
new file mode 100644 (file)
index 0000000..5cf7ee9
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import ProfileLink from '../components/ProfileLink';
+import { translateWithParameters } from '../../../helpers/l10n';
+
+export default class ProfileInheritanceBox extends React.Component {
+  static propTypes = {
+    profile: React.PropTypes.shape({
+      key: React.PropTypes.string.isRequired,
+      name: React.PropTypes.string.isRequired,
+      activeRuleCount: React.PropTypes.number.isRequired,
+      overridingRuleCount: React.PropTypes.number
+    }).isRequired,
+    depth: React.PropTypes.number.isRequired,
+    displayLink: React.PropTypes.bool.isRequired,
+    className: React.PropTypes.string
+  };
+
+  static defaultProps = {
+    displayLink: true
+  };
+
+  render () {
+    const { profile, className } = this.props;
+    const offset = 25 * this.props.depth;
+
+    return (
+        <tr className={className}>
+          <td>
+            <h6 style={{ paddingLeft: offset }}>
+              {this.props.displayLink ? (
+                  <ProfileLink profileKey={profile.key}>
+                    {profile.name}
+                  </ProfileLink>
+              ) : profile.name}
+            </h6>
+          </td>
+
+          <td>
+            {translateWithParameters(
+                'quality_profile.x_active_rules',
+                profile.activeRuleCount
+            )}
+          </td>
+
+          <td>
+            {profile.overridingRuleCount != null && (
+                <p>
+                  {translateWithParameters(
+                      'quality_profiles.x_overridden_rules',
+                      profile.overridingRuleCount
+                  )}
+                </p>
+            )}
+          </td>
+        </tr>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js
new file mode 100644 (file)
index 0000000..99b75fe
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * 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 ChangeProjectsView from '../views/ChangeProjectsView';
+import { ProfileType } from '../propTypes';
+import { getProfileProjects } from '../../../api/quality-profiles';
+import { translate } from '../../../helpers/l10n';
+
+export default class ProfileProjects extends React.Component {
+  static propTypes = {
+    profile: ProfileType,
+    canAdmin: React.PropTypes.bool.isRequired
+  };
+
+  state = {
+    projects: null
+  };
+
+  componentWillMount () {
+    this.loadProjects = this.loadProjects.bind(this);
+  }
+
+  componentDidMount () {
+    this.mounted = true;
+    this.loadProjects();
+  }
+
+  componentDidUpdate (prevProps) {
+    if (prevProps.profile !== this.props.profile) {
+      this.loadProjects();
+    }
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  loadProjects () {
+    if (this.props.profile.isDefault) {
+      return;
+    }
+
+    const data = { key: this.props.profile.key };
+    getProfileProjects(data).then(r => {
+      if (this.mounted) {
+        this.setState({
+          projects: r.results,
+          more: r.more,
+          loading: false
+        });
+      }
+    });
+  }
+
+  handleChange (e) {
+    e.preventDefault();
+    e.target.blur();
+    new ChangeProjectsView({
+      profile: this.props.profile,
+      loadProjects: this.loadProjects
+    }).render();
+  }
+
+  renderDefault () {
+    return (
+        <div>
+          <span className="badge spacer-right">
+            {translate('default')}
+          </span>
+          {translate('quality_profiles.projects_for_default')}
+        </div>
+    );
+  }
+
+  renderProjects () {
+    const { projects } = this.state;
+
+    if (projects == null) {
+      return null;
+    }
+
+    if (projects.length === 0) {
+      return (
+          <div>
+            {translate('quality_profiles.no_projects_associated_to_profile')}
+          </div>
+      );
+    }
+
+    return (
+        <ul>
+          {projects.map(project => (
+              <li key={project.uuid}
+                  className="spacer-top js-profile-project"
+                  data-key={project.key}>
+                <i className="icon-checkbox icon-checkbox-checked"/>
+                {' '}
+                {project.name}
+              </li>
+          ))}
+        </ul>
+    );
+  }
+
+  render () {
+    return (
+        <div className="quality-profile-projects">
+          <header className="page-header">
+            <h2 className="page-title">
+              {translate('projects')}
+            </h2>
+
+            {this.props.canAdmin && !this.props.profile.isDefault && (
+                <div className="pull-right">
+                  <button
+                      className="js-change-projects"
+                      onClick={this.handleChange.bind(this)}>
+                    {translate('quality_profiles.change_projects')}
+                  </button>
+                </div>
+            )}
+          </header>
+
+          {this.props.profile.isDefault ?
+              this.renderDefault() :
+              this.renderProjects()
+          }
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.js
new file mode 100644 (file)
index 0000000..539878d
--- /dev/null
@@ -0,0 +1,237 @@
+/*
+ * 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 keyBy from 'lodash/keyBy';
+import ProfileRulesRow from './ProfileRulesRow';
+import { ProfileType } from '../propTypes';
+import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
+import { searchRules, takeFacet } from '../../../api/rules';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+import {
+    getRulesUrl,
+    getDeprecatedActiveRulesUrl
+} from '../../../helpers/urls';
+
+const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
+
+export default class ProfileRules extends React.Component {
+  static propTypes = {
+    profile: ProfileType.isRequired,
+    canAdmin: React.PropTypes.bool.isRequired
+  };
+
+  state = {
+    total: null,
+    activatedTotal: null,
+    allByType: keyBy(TYPES.map(t => ({ val: t, count: null })), 'val'),
+    activatedByType: keyBy(TYPES.map(t => ({ val: t, count: null })), 'val')
+  };
+
+  componentDidMount () {
+    this.mounted = true;
+    this.loadRules();
+  }
+
+  componentDidUpdate (prevProps) {
+    if (prevProps.profile !== this.props.profile) {
+      this.loadRules();
+    }
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  loadAllRules () {
+    return searchRules({
+      languages: this.props.profile.language,
+      ps: 1,
+      facets: 'types'
+    });
+  }
+
+  loadActivatedRules () {
+    return searchRules({
+      qprofile: this.props.profile.key,
+      activation: 'true',
+      ps: 1,
+      facets: 'types'
+    });
+  }
+
+  loadRules () {
+    Promise.all([
+      this.loadAllRules(),
+      this.loadActivatedRules()
+    ]).then(responses => {
+      if (this.mounted) {
+        const [allRules, activatedRules] = responses;
+        this.setState({
+          total: allRules.total,
+          activatedTotal: activatedRules.total,
+          allByType: keyBy(takeFacet(allRules, 'types'), 'val'),
+          activatedByType: keyBy(takeFacet(activatedRules, 'types'), 'val')
+        });
+      }
+    });
+  }
+
+  getTooltip (count, total) {
+    if (count == null || total == null) {
+      return '';
+    }
+
+    return translateWithParameters(
+        'quality_profiles.x_activated_out_of_y',
+        formatMeasure(count, 'INT'),
+        formatMeasure(total, 'INT'));
+  }
+
+  renderActiveTitle () {
+    return (
+        <strong>
+          {translate('quality_profile.total_active_rules')}
+        </strong>
+    );
+  }
+
+  renderActiveCount () {
+    const rulesUrl = getRulesUrl({
+      qprofile: this.props.profile.key,
+      activation: 'true'
+    });
+
+    if (this.state.activatedTotal == null) {
+      return null;
+    }
+
+    return (
+        <a href={rulesUrl}>
+          <strong>
+            {formatMeasure(this.state.activatedTotal, 'SHORT_INT')}
+          </strong>
+        </a>
+    );
+  }
+
+  getTooltipForType (type) {
+    const { count } = this.state.activatedByType[type];
+    const total = this.state.allByType[type].count;
+    return this.getTooltip(count, total);
+  }
+
+  renderTitleForType (type) {
+    return <span>{translate('issue.type', type, 'plural')}</span>;
+  }
+
+  renderCountForType (type) {
+    const rulesUrl = getRulesUrl({
+      qprofile: this.props.profile.key,
+      activation: 'true',
+      types: type
+    });
+
+    const { count } = this.state.activatedByType[type];
+
+    if (count == null) {
+      return null;
+    }
+
+    return (
+        <a href={rulesUrl}>
+          {formatMeasure(count, 'SHORT_INT')}
+        </a>
+    );
+  }
+
+  renderDeprecated () {
+    const { profile } = this.props;
+
+    if (profile.activeDeprecatedRuleCount === 0) {
+      return null;
+    }
+
+    const url = getDeprecatedActiveRulesUrl({ qprofile: profile.key });
+
+    return (
+        <div className="quality-profile-rules-deprecated clearfix">
+          <div className="pull-left">
+            {translate('quality_profiles.deprecated_rules')}
+          </div>
+          <div className="pull-right">
+            <a href={url}>
+              {profile.activeDeprecatedRuleCount}
+            </a>
+          </div>
+        </div>
+    );
+  }
+
+  render () {
+    const { total, activatedTotal, allByType, activatedByType } = this.state;
+
+    const activateMoreUrl = getRulesUrl({
+      qprofile: this.props.profile.key,
+      activation: 'false'
+    });
+
+    return (
+        <div className="quality-profile-rules">
+          <header className="clearfix">
+            <h2 className="pull-left">{translate('rules')}</h2>
+
+            {this.props.canAdmin && (
+                <a href={activateMoreUrl}
+                   className="button pull-right js-activate-rules">
+                  {translate('quality_profiles.activate_more')}
+                </a>
+            )}
+          </header>
+
+          {this.renderDeprecated()}
+
+          <TooltipsContainer options={{ delay: { show: 250, hide: 0 } }}>
+            <ul className="quality-profile-rules-distribution">
+              <li key="all" className="big-spacer-bottom">
+                <ProfileRulesRow
+                    count={activatedTotal}
+                    total={total}
+                    tooltip={this.getTooltip(activatedTotal, total)}
+                    renderTitle={this.renderActiveTitle.bind(this)}
+                    renderCount={this.renderActiveCount.bind(this)}/>
+              </li>
+
+              {TYPES.map(type => (
+                  <li key={type} className="spacer-top">
+                    <ProfileRulesRow
+                        count={activatedByType[type].count}
+                        total={allByType[type].count}
+                        tooltip={this.getTooltipForType(type)}
+                        renderTitle={this.renderTitleForType.bind(this, type)}
+                        renderCount={this.renderCountForType.bind(this, type)}/>
+                  </li>
+              ))}
+            </ul>
+          </TooltipsContainer>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.js
new file mode 100644 (file)
index 0000000..6baf9ca
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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 ProgressBar from './ProgressBar';
+
+export default class ProfileRulesRow extends React.Component {
+  static propTypes = {
+    count: React.PropTypes.number,
+    total: React.PropTypes.number,
+    tooltip: React.PropTypes.string,
+    renderTitle: React.PropTypes.func.isRequired,
+    renderCount: React.PropTypes.func.isRequired
+  };
+
+  render () {
+    const { total, count, tooltip, renderTitle, renderCount } = this.props;
+
+    return (
+        <div title={tooltip} data-toggle="tooltip">
+          <div className="clearfix">
+            <div className="pull-left">
+              {renderTitle()}
+            </div>
+            <div className="pull-right">
+              {renderCount()}
+            </div>
+          </div>
+          <div className="little-spacer-top" style={{ height: 2 }}>
+            <ProgressBar
+                count={count || 0}
+                total={total || 0}
+                width={300}/>
+          </div>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProgressBar.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProgressBar.js
new file mode 100644 (file)
index 0000000..5efc85f
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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';
+
+export default class ProgressBar extends React.Component {
+  static propTypes = {
+    width: React.PropTypes.number.isRequired,
+    height: React.PropTypes.number,
+    count: React.PropTypes.number.isRequired,
+    total: React.PropTypes.number.isRequired
+  };
+
+  static defaultProps = {
+    height: 2
+  };
+
+  render () {
+    const { width, height } = this.props;
+    const p = this.props.total > 0 ? this.props.count / this.props.total : 0;
+    const fillWidth = this.props.width * p;
+
+    const commonProps = { x: 0, y: 0, rx: 2, height };
+
+    return (
+        <svg width={width} height={height}>
+          <rect
+              {...commonProps}
+              width={width}
+              fill="#e6e6e6"/>
+          <rect
+              {...commonProps}
+              width={fillWidth}
+              className="bar-chart-bar quality-profile-progress-bar"/>
+        </svg>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/Evolution.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/Evolution.js
new file mode 100644 (file)
index 0000000..82b6f60
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import EvolutionDeprecated from './EvolutionDeprecated';
+import EvolutionStagnant from './EvolutionStagnant';
+import EvolutionRules from './EvolutionRules';
+import { ProfilesListType } from '../propTypes';
+
+export default class Evolution extends React.Component {
+  static propTypes = {
+    profiles: ProfilesListType.isRequired
+  };
+
+  render () {
+    return (
+        <div className="quality-profiles-evolution">
+          <EvolutionDeprecated profiles={this.props.profiles}/>
+          <EvolutionStagnant profiles={this.props.profiles}/>
+          <EvolutionRules/>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.js
new file mode 100644 (file)
index 0000000..fb0fd93
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * 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 ProfileLink from '../components/ProfileLink';
+import { getDeprecatedActiveRulesUrl } from '../../../helpers/urls';
+import { ProfilesListType } from '../propTypes';
+import { translateWithParameters, translate } from '../../../helpers/l10n';
+
+export default class EvolutionDeprecated extends React.Component {
+  static propTypes = {
+    profiles: ProfilesListType.isRequired
+  };
+
+  render () {
+    const profilesWithDeprecations = this.props.profiles
+        .filter(profile => profile.activeDeprecatedRuleCount > 0);
+
+    if (profilesWithDeprecations.length === 0) {
+      return null;
+    }
+
+    const totalRules = profilesWithDeprecations
+        .map(p => p.activeDeprecatedRuleCount)
+        .reduce((p, c) => p + c, 0);
+
+    return (
+        <div className="quality-profiles-evolution-deprecated">
+          <div className="spacer-bottom">
+            <strong>{translate('quality_profiles.deprecated_rules')}</strong>
+          </div>
+          <div className="spacer-bottom">
+            {translateWithParameters(
+                'quality_profiles.x_deprecated_rules_are_still_activated',
+                totalRules,
+                profilesWithDeprecations.length
+            )}
+          </div>
+          <ul>
+            {profilesWithDeprecations.map(profile => (
+                <li key={profile.key} className="spacer-top">
+                  <div className="text-ellipsis">
+                    <ProfileLink
+                        profileKey={profile.key}
+                        className="link-no-underline">
+                      {profile.name}
+                    </ProfileLink>
+                  </div>
+                  <div className="note">
+                    {profile.languageName}
+                    {', '}
+                    <a className="text-muted"
+                       href={getDeprecatedActiveRulesUrl({ qprofile: profile.key })}>
+                      {translateWithParameters(
+                          'quality_profile.x_rules',
+                          profile.activeDeprecatedRuleCount
+                      )}
+                    </a>
+                  </div>
+                </li>
+            ))}
+          </ul>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.js
new file mode 100644 (file)
index 0000000..e2956f2
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * 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 moment from 'moment';
+import sortBy from 'lodash/sortBy';
+import { searchRules } from '../../../api/rules';
+import { translateWithParameters, translate } from '../../../helpers/l10n';
+import { getRulesUrl } from '../../../helpers/urls';
+
+const RULES_LIMIT = 3;
+
+const PERIOD_START_MOMENT = moment().subtract(1, 'month');
+
+function parseRules (r) {
+  const { rules, actives } = r;
+  return rules.map(rule => {
+    const activations = actives[rule.key];
+    return { ...rule, activations: activations ? activations.length : 0 };
+  });
+}
+
+export default class EvolutionRules extends React.Component {
+  state = {};
+
+  componentDidMount () {
+    this.mounted = true;
+    this.loadLatestRules();
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  loadLatestRules () {
+    const data = {
+      'available_since': PERIOD_START_MOMENT.format('YYYY-MM-DD'),
+      s: 'createdAt',
+      asc: false,
+      ps: RULES_LIMIT,
+      f: 'name,langName,actives'
+    };
+
+    searchRules(data).then(r => {
+      if (this.mounted) {
+        this.setState({
+          latestRules: sortBy(parseRules(r), 'langName'),
+          latestRulesTotal: r.total
+        });
+      }
+    });
+  }
+
+  render () {
+    if (!this.state.latestRulesTotal) {
+      return null;
+    }
+
+    const newRulesUrl = getRulesUrl({
+      'available_since': PERIOD_START_MOMENT.format('YYYY-MM-DD')
+    });
+
+    return (
+        <div className="quality-profiles-evolution-rules">
+          <div className="clearfix">
+            <strong className="pull-left">
+              {translate('quality_profiles.latest_new_rules')}
+            </strong>
+
+            {this.state.latestRulesTotal > RULES_LIMIT && (
+                <a className="pull-right small text-muted"
+                   href={newRulesUrl}>
+                  {translate('see_all')}
+                </a>
+            )}
+          </div>
+          <ul>
+            {this.state.latestRules.map(rule => (
+                <li key={rule.key} className="spacer-top">
+                  <div className="text-ellipsis">
+                    <a className="link-no-underline"
+                       href={getRulesUrl({ 'rule_key': rule.key })}>
+                      {' '}
+                      {rule.name}
+                    </a>
+                    <div className="note">
+                      {rule.activations ? (
+                          translateWithParameters(
+                              'quality_profiles.latest_new_rules.activated',
+                              rule.langName,
+                              rule.activations
+                          )
+                      ) : (
+                          translateWithParameters(
+                              'quality_profiles.latest_new_rules.not_activated',
+                              rule.langName
+                          )
+                      )}
+                    </div>
+                  </div>
+                </li>
+            ))}
+          </ul>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.js
new file mode 100644 (file)
index 0000000..8b5b1e3
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import moment from 'moment';
+import ProfileLink from '../components/ProfileLink';
+import { ProfilesListType } from '../propTypes';
+import { translate } from '../../../helpers/l10n';
+
+export default class EvolutionStagnant extends React.Component {
+  static propTypes = {
+    profiles: ProfilesListType.isRequired
+  };
+
+  render () {
+    // TODO filter built-in out
+
+    const outdated = this.props.profiles.filter(profile => (
+        moment().diff(moment(profile.rulesUpdatedAt), 'years') >= 1
+    ));
+
+    if (outdated.length === 0) {
+      return null;
+    }
+
+    return (
+        <div className="quality-profiles-evolution-stagnant">
+          <div className="spacer-bottom">
+            <strong>{translate('quality_profiles.stagnant_profiles')}</strong>
+          </div>
+          <div className="spacer-bottom">
+            {translate('quality_profiles.not_updated_more_than_year')}
+          </div>
+          <ul>
+            {outdated.map(profile => (
+                <li key={profile.key} className="spacer-top">
+                  <div className="text-ellipsis">
+                    <ProfileLink
+                        profileKey={profile.key}
+                        className="link-no-underline">
+                      {profile.name}
+                    </ProfileLink>
+                  </div>
+                  <div className="note">
+                    {profile.languageName}
+                    {', '}
+                    updated on {moment(profile.rulesUpdatedAt).format('LL')}
+                  </div>
+                </li>
+            ))}
+          </ul>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.js
new file mode 100644 (file)
index 0000000..94db2d7
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * 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 Helmet from 'react-helmet';
+import PageHeader from './PageHeader';
+import Evolution from './Evolution';
+import ProfilesList from './ProfilesList';
+import { translate } from '../../../helpers/l10n';
+
+export default class HomeContainer extends React.Component {
+  render () {
+    return (
+        <div>
+          <Helmet
+              title={translate('quality_profiles.page')}
+              titleTemplate="SonarQube - %s"/>
+
+          <PageHeader {...this.props}/>
+          <Evolution {...this.props}/>
+          <ProfilesList {...this.props}/>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.js
new file mode 100644 (file)
index 0000000..1fcd7aa
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * 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 CreateProfileView from '../views/CreateProfileView';
+import RestoreProfileView from '../views/RestoreProfileView';
+import { translate } from '../../../helpers/l10n';
+import { getImporters } from '../../../api/quality-profiles';
+
+export default class PageHeader extends React.Component {
+  static propTypes = {
+    canAdmin: React.PropTypes.bool.isRequired
+  };
+
+  static contextTypes = {
+    router: React.PropTypes.object
+  };
+
+  state = {};
+
+  componentDidMount () {
+    this.mounted = true;
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  retrieveImporters () {
+    if (this.state.importers) {
+      return Promise.resolve(this.state.importers);
+    } else {
+      return getImporters().then(importers => {
+        this.setState({ importers });
+        return importers;
+      });
+    }
+  }
+
+  handleCreateClick (e) {
+    e.preventDefault();
+    e.target.blur();
+    this.retrieveImporters().then(importers => {
+      new CreateProfileView({
+        languages: this.props.languages,
+        importers
+      }).on('done', profile => {
+        this.props.updateProfiles().then(() => {
+          this.context.router.push({
+            pathname: '/show',
+            query: { key: profile.key }
+          });
+        });
+      }).render();
+    });
+  }
+
+  handleRestoreClick (e) {
+    e.preventDefault();
+    e.target.blur();
+    new RestoreProfileView()
+        .on('done', this.props.updateProfiles)
+        .render();
+  }
+
+  render () {
+    return (
+        <header className="page-header">
+          <h1 className="page-title">
+            {translate('quality_profiles.page')}
+          </h1>
+
+          {this.props.canAdmin && (
+              <div className="page-actions button-group">
+                <button
+                    id="quality-profiles-create"
+                    onClick={this.handleCreateClick.bind(this)}>
+                  {translate('create')}
+                </button>
+
+                <button
+                    id="quality-profiles-restore"
+                    className="spacer-left"
+                    onClick={this.handleRestoreClick.bind(this)}>
+                  {translate('quality_profiles.restore_profile')}
+                </button>
+              </div>
+          )}
+
+          <div className="page-description markdown">
+            {translate('quality_profiles.intro1')}
+            <br/>
+            {translate('quality_profiles.intro2')}
+          </div>
+        </header>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.js
new file mode 100644 (file)
index 0000000..b4b10f4
--- /dev/null
@@ -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 { PropTypes as RouterPropTypes } from 'react-router';
+import groupBy from 'lodash/groupBy';
+import pick from 'lodash/pick';
+import ProfilesListRow from './ProfilesListRow';
+import ProfilesListHeader from './ProfilesListHeader';
+import RestoreBuiltInProfilesView from '../views/RestoreBuiltInProfilesView';
+import { ProfilesListType, LanguagesListType } from '../propTypes';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
+
+export default class ProfilesList extends React.Component {
+  static propTypes = {
+    profiles: ProfilesListType,
+    languages: LanguagesListType,
+    location: RouterPropTypes.location
+  };
+
+  handleRestoreBuiltIn (languageKey, e) {
+    e.preventDefault();
+    const language = this.props.languages.find(l => l.key === languageKey);
+    new RestoreBuiltInProfilesView({ language })
+        .on('done', this.props.updateProfiles)
+        .render();
+  }
+
+  renderProfiles (profiles) {
+    return profiles.map(profile => (
+        <ProfilesListRow key={profile.key} profile={profile}/>
+    ));
+  }
+
+  renderHeader (languageKey, profilesCount) {
+    const language = this.props.languages.find(l => l.key === languageKey);
+    return (
+        <thead>
+          <tr>
+            <th>
+              {language.name}
+              {' ('}
+              {translateWithParameters(
+                  'quality_profiles.x_profiles',
+                  profilesCount
+              )}
+              {')'}
+              {this.props.canAdmin && (
+                  <button
+                      className="spacer-left js-restore-built-in"
+                      data-language={languageKey}
+                      onClick={this.handleRestoreBuiltIn.bind(this, languageKey)}>
+                    {translate('quality_profiles.restore_built_in_profiles')}
+                  </button>
+              )}
+            </th>
+            <th className="text-right nowrap">
+              {translate('quality_profiles.list.projects')}
+            </th>
+            <th className="text-right nowrap">
+              {translate('quality_profiles.list.rules')}
+            </th>
+            <th className="text-right nowrap">
+              {translate('quality_profiles.list.updated')}
+            </th>
+            <th className="text-right nowrap">
+              {translate('quality_profiles.list.used')}
+            </th>
+          </tr>
+        </thead>
+    );
+  }
+
+  render () {
+    const { profiles, languages } = this.props;
+    const { language } = this.props.location.query;
+
+    const profilesIndex = groupBy(profiles, profile => profile.language);
+    const profilesToShow = language ?
+        pick(profilesIndex, language) :
+        profilesIndex;
+
+    return (
+        <div>
+          <ProfilesListHeader
+              languages={languages}
+              currentFilter={language}/>
+
+          {Object.keys(profilesToShow).length === 0 && (
+              <div className="alert alert-warning">
+                {translate('no_results')}
+              </div>
+          )}
+
+          {Object.keys(profilesToShow).map(languageKey => (
+              <table
+                  key={languageKey}
+                  data-language={languageKey}
+                  className="data zebra zebra-hover quality-profiles-table">
+
+                {this.renderHeader(
+                    languageKey,
+                    profilesToShow[languageKey].length)}
+
+                <TooltipsContainer>
+                  <tbody>
+                    {this.renderProfiles(profilesToShow[languageKey])}
+                  </tbody>
+                </TooltipsContainer>
+
+              </table>
+          ))}
+
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.js
new file mode 100644 (file)
index 0000000..532d76e
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 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 { IndexLink } from 'react-router';
+import { LanguagesListType } from '../propTypes';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+export default class ProfilesListHeader extends React.Component {
+  static propTypes = {
+    languages: LanguagesListType.isRequired,
+    currentFilter: React.PropTypes.string
+  };
+
+  renderFilterToggle () {
+    const { languages, currentFilter } = this.props;
+    const currentLanguage = currentFilter &&
+        languages.find(l => l.key === currentFilter);
+
+    const label = currentFilter ?
+        translateWithParameters(
+            'quality_profiles.x_profiles',
+            currentLanguage.name) :
+        translate('quality_profiles.all_profiles');
+
+    return (
+        <a className="dropdown-toggle link-no-underline js-language-filter"
+           href="#"
+           data-toggle="dropdown">
+          {label} <i className="icon-dropdown"/>
+        </a>
+    );
+  }
+
+  renderFilterMenu () {
+    return (
+        <ul className="dropdown-menu">
+          <li>
+            <IndexLink to="/">
+              {translate('quality_profiles.all_profiles')}
+            </IndexLink>
+          </li>
+          {this.props.languages.map(language => (
+              <li key={language.key}>
+                <IndexLink
+                    to={{ pathname: '/', query: { language: language.key } }}
+                    className="js-language-filter-option"
+                    data-language={language.key}>
+                  {language.name}
+                </IndexLink>
+              </li>
+          ))}
+        </ul>
+    );
+  }
+
+  render () {
+    if (this.props.languages.length < 2) {
+      return null;
+    }
+
+    const { languages, currentFilter } = this.props;
+    const currentLanguage = currentFilter &&
+        languages.find(l => l.key === currentFilter);
+
+    // if unknown language, then
+    if (currentFilter && !currentLanguage) {
+      return null;
+    }
+
+    return (
+        <header className="quality-profiles-list-header clearfix">
+          <div className="dropdown">
+            {this.renderFilterToggle()}
+            {this.renderFilterMenu()}
+          </div>
+        </header>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js
new file mode 100644 (file)
index 0000000..8a326ad
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * 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 ProfileLink from '../components/ProfileLink';
+import ProfileDate from '../components/ProfileDate';
+import { ProfileType } from '../propTypes';
+import { translate } from '../../../helpers/l10n';
+import { getRulesUrl } from '../../../helpers/urls';
+
+export default class ProfilesListRow extends React.Component {
+  static propTypes = {
+    profile: ProfileType.isRequired
+  };
+
+  shouldComponentUpdate (nextProps, nextState) {
+    return shallowCompare(this, nextProps, nextState);
+  }
+
+  renderName () {
+    const { profile } = this.props;
+    const offset = 25 * (profile.depth - 1);
+    return (
+        <h4 style={{ paddingLeft: offset }}>
+          <ProfileLink profileKey={profile.key}>
+            {profile.name}
+          </ProfileLink>
+        </h4>
+    );
+  }
+
+  renderProjects () {
+    const { profile } = this.props;
+
+    if (profile.isDefault) {
+      return (
+          <span className="badge">
+            {translate('default')}
+          </span>
+      );
+    }
+
+    return (
+        <span>
+          {profile.projectCount}
+        </span>
+    );
+  }
+
+  renderRules () {
+    const { profile } = this.props;
+
+    const activeRulesUrl = getRulesUrl({
+      qprofile: profile.key,
+      activation: 'true'
+    });
+
+    const deprecatedRulesUrl = getRulesUrl({
+      qprofile: profile.key,
+      activation: 'true',
+      statuses: 'DEPRECATED'
+    });
+
+    return (
+        <div>
+          {profile.activeDeprecatedRuleCount > 0 && (
+              <span className="spacer-right">
+                <a className="badge badge-warning"
+                   href={deprecatedRulesUrl}
+                   title={translate('quality_profiles.deprecated_rules')}
+                   data-toggle="tooltip">
+                  {profile.activeDeprecatedRuleCount}
+                </a>
+              </span>
+          )}
+
+          <a href={activeRulesUrl}>
+            {profile.activeRuleCount}
+          </a>
+        </div>
+    );
+  }
+
+  renderUpdateDate () {
+    return <ProfileDate date={this.props.profile.userUpdatedAt}/>;
+  }
+
+  renderUsageDate () {
+    return <ProfileDate date={this.props.profile.lastUsed}/>;
+  }
+
+  render () {
+    return (
+        <tr className="quality-profiles-table-row"
+            data-key={this.props.profile.key}
+            data-name={this.props.profile.name}>
+          <td className="quality-profiles-table-name">
+            {this.renderName()}
+          </td>
+          <td className="quality-profiles-table-projects thin nowrap text-right">
+            {this.renderProjects()}
+          </td>
+          <td className="quality-profiles-table-rules thin nowrap text-right">
+            {this.renderRules()}
+          </td>
+          <td className="quality-profiles-table-date thin nowrap text-right">
+            {this.renderUpdateDate()}
+          </td>
+          <td className="quality-profiles-table-date thin nowrap text-right">
+            {this.renderUsageDate()}
+          </td>
+        </tr>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/intro-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/intro-view.js
deleted file mode 100644 (file)
index 924a99e..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import Marionette from 'backbone.marionette';
-import Template from './templates/quality-profiles-intro.hbs';
-
-export default Marionette.ItemView.extend({
-  template: Template
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/layout.js b/server/sonar-web/src/main/js/apps/quality-profiles/layout.js
deleted file mode 100644 (file)
index 5102782..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import Marionette from 'backbone.marionette';
-import IntroView from './intro-view';
-import Template from './templates/quality-profiles-layout.hbs';
-
-export default Marionette.LayoutView.extend({
-  template: Template,
-
-  regions: {
-    headerRegion: '.search-navigator-workspace-header',
-    actionsRegion: '.search-navigator-filters',
-    resultsRegion: '.quality-profiles-results',
-    detailsRegion: '.search-navigator-workspace-details'
-  },
-
-  onRender () {
-    const navigator = this.$('.search-navigator');
-    navigator.addClass('sticky search-navigator-extended-view');
-    const top = navigator.offset().top;
-    this.$('.search-navigator-workspace-header').css({ top });
-    this.$('.search-navigator-side').css({ top }).isolatedScroll();
-    this.renderIntro();
-  },
-
-  renderIntro () {
-    this.detailsRegion.show(new IntroView());
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile-changelog-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile-changelog-view.js
deleted file mode 100644 (file)
index 25fa859..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import Marionette from 'backbone.marionette';
-import Template from './templates/quality-profile-changelog.hbs';
-
-export default Marionette.ItemView.extend({
-  template: Template,
-
-  events: {
-    'submit #quality-profile-changelog-form': 'onFormSubmit',
-    'click .js-show-more-changelog': 'onShowMoreChangelogClick',
-    'click .js-hide-changelog': 'onHideChangelogClick'
-  },
-
-  onFormSubmit (e) {
-    e.preventDefault();
-    this.model.fetchChangelog(this.getSearchParameters());
-  },
-
-  onShowMoreChangelogClick (e) {
-    e.preventDefault();
-    this.model.fetchMoreChangelog();
-  },
-
-  onHideChangelogClick (e) {
-    e.preventDefault();
-    this.model.resetChangelog();
-  },
-
-  getSearchParameters () {
-    const form = this.$('#quality-profile-changelog-form');
-    return {
-      since: form.find('[name="since"]').val(),
-      to: form.find('[name="to"]').val()
-    };
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile-comparison-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile-comparison-view.js
deleted file mode 100644 (file)
index 79d79b7..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import Template from './templates/quality-profile-comparison.hbs';
-
-export default Marionette.ItemView.extend({
-  template: Template,
-
-  events: {
-    'submit #quality-profile-comparison-form': 'onFormSubmit',
-    'click .js-hide-comparison': 'onHideComparisonClick'
-  },
-
-  onRender () {
-    this.$('select').select2({
-      width: '250px',
-      minimumResultsForSearch: 50
-    });
-  },
-
-  onFormSubmit (e) {
-    e.preventDefault();
-    const withKey = this.$('#quality-profile-comparison-with-key').val();
-    this.model.compareWith(withKey);
-  },
-
-  onHideComparisonClick (e) {
-    e.preventDefault();
-    this.model.resetComparison();
-  },
-
-  getProfilesForComparison () {
-    const profiles = this.model.collection.toJSON();
-    const key = this.model.id;
-    const language = this.model.get('language');
-    return profiles.filter(function (profile) {
-      return profile.language === language && key !== profile.key;
-    });
-  },
-
-  serializeData () {
-    return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
-      profiles: this.getProfilesForComparison()
-    });
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile-details-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile-details-view.js
deleted file mode 100644 (file)
index a914b3a..0000000
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import ChangeProfileParentView from './change-profile-parent-view';
-import ProfileChangelogView from './profile-changelog-view';
-import ProfileComparisonView from './profile-comparison-view';
-import '../../components/SelectList';
-import Template from './templates/quality-profiles-profile-details.hbs';
-import { translate } from '../../helpers/l10n';
-
-export default Marionette.LayoutView.extend({
-  template: Template,
-
-  regions: {
-    changelogRegion: '#quality-profile-changelog',
-    comparisonRegion: '#quality-profile-comparison'
-  },
-
-  modelEvents: {
-    'change': 'onChange',
-    'flashChangelog': 'flashChangelog'
-  },
-
-  events: {
-    'click .js-profile': 'onProfileClick',
-    'click #quality-profile-change-parent': 'onChangeParentClick'
-  },
-
-  onRender () {
-    if (!this.model.get('isDefault')) {
-      this.initProjectsSelect();
-    }
-    this.changelogRegion.show(new ProfileChangelogView({ model: this.model }));
-    this.comparisonRegion.show(new ProfileComparisonView({ model: this.model }));
-    if (this.options.anchor === 'changelog') {
-      this.scrollToChangelog();
-      this.flashChangelog();
-    }
-    if (this.options.anchor === 'comparison') {
-      this.scrollToComparison();
-    }
-    this.$('#quality-profile-changelog-form input')
-        .datepicker({
-          dateFormat: 'yy-mm-dd',
-          changeMonth: true,
-          changeYear: true
-        });
-  },
-
-  onChange () {
-    const changed = Object.keys(this.model.changedAttributes());
-    if (!(changed.length === 1 && changed[0] === 'projectCount')) {
-      this.render();
-    }
-  },
-
-  initProjectsSelect () {
-    const key = this.model.get('key');
-    this.projectsSelectList = new window.SelectList({
-      el: this.$('#quality-profile-projects-list'),
-      width: '100%',
-      height: 200,
-      readOnly: !this.options.canWrite,
-      focusSearch: false,
-      format (item) {
-        return item.name;
-      },
-      searchUrl: window.baseUrl + '/api/qualityprofiles/projects?key=' + encodeURIComponent(key),
-      selectUrl: window.baseUrl + '/api/qualityprofiles/add_project',
-      deselectUrl: window.baseUrl + '/api/qualityprofiles/remove_project',
-      extra: {
-        profileKey: key
-      },
-      selectParameter: 'projectUuid',
-      selectParameterValue: 'uuid',
-      labels: {
-        selected: translate('quality_gates.projects.with'),
-        deselected: translate('quality_gates.projects.without'),
-        all: translate('quality_gates.projects.all'),
-        noResults: translate('quality_gates.projects.noResults')
-      },
-      tooltips: {
-        select: translate('quality_profiles.projects.select_hint'),
-        deselect: translate('quality_profiles.projects.deselect_hint')
-      }
-    });
-    this.listenTo(this.projectsSelectList.collection, 'change:selected', this.onProjectsChange);
-  },
-
-  onProfileClick (e) {
-    const key = $(e.currentTarget).data('key');
-    const profile = this.model.collection.get(key);
-    if (profile != null) {
-      e.preventDefault();
-      this.model.collection.trigger('select', profile);
-    }
-  },
-
-  onChangeParentClick (e) {
-    e.preventDefault();
-    this.changeParent();
-  },
-
-  onProjectsChange () {
-    this.model.collection.updateForLanguage(this.model.get('language'));
-  },
-
-  changeParent () {
-    new ChangeProfileParentView({
-      model: this.model
-    }).render();
-  },
-
-  scrollTo (selector) {
-    const el = this.$(selector);
-    const parent = el.scrollParent();
-    const elOffset = el.offset();
-    let parentOffset = parent.offset();
-    if (parent.is(document)) {
-      parentOffset = { top: 0 };
-    }
-    if (elOffset != null && parentOffset != null) {
-      const scrollTop = elOffset.top - parentOffset.top - 53;
-      parent.scrollTop(scrollTop);
-    }
-  },
-
-  scrollToChangelog () {
-    this.scrollTo('#quality-profile-changelog');
-  },
-
-  scrollToComparison () {
-    this.scrollTo('#quality-profile-comparison');
-  },
-
-  getExporters () {
-    const language = this.model.get('language');
-    return this.options.exporters.filter(function (exporter) {
-      return exporter.languages.indexOf(language) !== -1;
-    });
-  },
-
-  flashChangelog () {
-    const changelogEl = this.$(this.changelogRegion.el);
-    changelogEl.addClass('flash in');
-    setTimeout(function () {
-      changelogEl.removeClass('in');
-    }, 2000);
-  },
-
-  serializeData () {
-    const key = this.model.get('key');
-    const rulesSearchUrl = `/coding_rules#qprofile=${encodeURIComponent(key)}|activation=true`;
-    const activateRulesUrl = `/coding_rules#qprofile=${encodeURIComponent(key)}|activation=false`;
-    return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
-      rulesSearchUrl,
-      activateRulesUrl,
-      canWrite: this.options.canWrite,
-      exporters: this.getExporters()
-    });
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile-header-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile-header-view.js
deleted file mode 100644 (file)
index d1b29b0..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import ProfileCopyView from './copy-profile-view';
-import ProfileRenameView from './rename-profile-view';
-import ProfileDeleteView from './delete-profile-view';
-import Template from './templates/quality-profiles-profile-header.hbs';
-
-export default Marionette.ItemView.extend({
-  template: Template,
-
-  modelEvents: {
-    'change': 'render'
-  },
-
-  events: {
-    'click #quality-profile-backup': 'onBackupClick',
-    'click #quality-profile-copy': 'onCopyClick',
-    'click #quality-profile-rename': 'onRenameClick',
-    'click #quality-profile-set-as-default': 'onDefaultClick',
-    'click #quality-profile-delete': 'onDeleteClick'
-  },
-
-  onBackupClick (e) {
-    $(e.currentTarget).blur();
-  },
-
-  onCopyClick (e) {
-    e.preventDefault();
-    this.copy();
-  },
-
-  onRenameClick (e) {
-    e.preventDefault();
-    this.rename();
-  },
-
-  onDefaultClick (e) {
-    e.preventDefault();
-    this.setAsDefault();
-  },
-
-  onDeleteClick (e) {
-    e.preventDefault();
-    this.delete();
-  },
-
-  copy () {
-    new ProfileCopyView({ model: this.model }).render();
-  },
-
-  rename () {
-    new ProfileRenameView({ model: this.model }).render();
-  },
-
-  setAsDefault () {
-    this.model.trigger('setAsDefault', this.model);
-  },
-
-  delete () {
-    new ProfileDeleteView({ model: this.model }).render();
-  },
-
-  serializeData () {
-    const key = this.model.get('key');
-    return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
-      encodedKey: encodeURIComponent(key),
-      canWrite: this.options.canWrite
-    });
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile-view.js
deleted file mode 100644 (file)
index 09c5f75..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import Template from './templates/quality-profiles-profile.hbs';
-import { formatMeasure } from '../../helpers/measures';
-
-export default Marionette.ItemView.extend({
-  tagName: 'a',
-  className: 'list-group-item',
-  template: Template,
-
-  modelEvents: {
-    'change': 'render'
-  },
-
-  events: {
-    'click': 'onClick'
-  },
-
-  onRender () {
-    this.$el.toggleClass('active', this.options.highlighted);
-    this.$el.attr('data-key', this.model.id);
-    this.$el.attr('data-language', this.model.get('language'));
-    this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
-  },
-
-  onDestroy () {
-    this.$('[data-toggle="tooltip"]').tooltip('destroy');
-  },
-
-  onClick (e) {
-    e.preventDefault();
-    this.model.trigger('select', this.model);
-  },
-
-  serializeData () {
-    return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
-      projectCountFormatted: formatMeasure(this.model.get('projectCount'), 'INT')
-    });
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile.js
deleted file mode 100644 (file)
index 0093537..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import Backbone from 'backbone';
-
-export default Backbone.Model.extend({
-  idAttribute: 'key',
-
-  defaults: {
-    activeRuleCount: 0,
-    projectCount: 0
-  },
-
-  fetch () {
-    const that = this;
-    this.fetchChanged = {};
-    return $.when(
-        this.fetchProfileRules(),
-        this.fetchInheritance()
-    ).done(function () {
-      that.set(that.fetchChanged);
-    });
-  },
-
-  fetchProfileRules () {
-    const that = this;
-    const url = window.baseUrl + '/api/rules/search';
-    const key = this.id;
-    const options = {
-      ps: 1,
-      facets: 'types',
-      qprofile: key,
-      activation: 'true'
-    };
-    return $.get(url, options).done(function (r) {
-      const typesFacet = _.findWhere(r.facets, { property: 'types' });
-      if (typesFacet != null) {
-        const order = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
-        const types = typesFacet.values;
-        const typesComparator = function (t) {
-          return order.indexOf(t.val);
-        };
-        const sortedTypes = _.sortBy(types, typesComparator);
-        _.extend(that.fetchChanged, { rulesTypes: sortedTypes });
-      }
-    });
-  },
-
-  fetchInheritance () {
-    const that = this;
-    const url = window.baseUrl + '/api/qualityprofiles/inheritance';
-    const options = { profileKey: this.id };
-    return $.get(url, options).done(function (r) {
-      _.extend(that.fetchChanged, r.profile, {
-        ancestors: r.ancestors,
-        children: r.children
-      });
-    });
-  },
-
-  fetchChangelog (options) {
-    const that = this;
-    const url = window.baseUrl + '/api/qualityprofiles/changelog';
-    const opts = _.extend({}, options, { profileKey: this.id });
-    return $.get(url, opts).done(function (r) {
-      that.set({
-        events: r.events,
-        eventsPage: r.p,
-        totalEvents: r.total,
-        eventsParameters: _.clone(options)
-      });
-    });
-  },
-
-  fetchMoreChangelog () {
-    const that = this;
-    const url = window.baseUrl + '/api/qualityprofiles/changelog';
-    const page = this.get('eventsPage') || 0;
-    const parameters = this.get('eventsParameters') || {};
-    const opts = _.extend({}, parameters, { profileKey: this.id, p: page + 1 });
-    return $.get(url, opts).done(function (r) {
-      const events = that.get('events') || [];
-      that.set({
-        events: [].concat(events, r.events),
-        eventsPage: r.p,
-        totalEvents: r.total
-      });
-    });
-  },
-
-  resetChangelog () {
-    this.unset('events', { silent: true });
-    this.unset('eventsPage', { silent: true });
-    this.unset('totalEvents');
-  },
-
-  compareWith (withKey) {
-    const that = this;
-    const url = window.baseUrl + '/api/qualityprofiles/compare';
-    const options = { leftKey: this.id, rightKey: withKey };
-    return $.get(url, options).done(function (r) {
-      const comparison = _.extend(r, {
-        inLeftSize: _.size(r.inLeft),
-        inRightSize: _.size(r.inRight),
-        modifiedSize: _.size(r.modified)
-      });
-      that.set({
-        comparison,
-        comparedWith: withKey
-      });
-    });
-  },
-
-  resetComparison () {
-    this.unset('comparedWith', { silent: true });
-    this.unset('comparison');
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profiles-empty-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profiles-empty-view.js
deleted file mode 100644 (file)
index 9dbd11e..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import Marionette from 'backbone.marionette';
-import Template from './templates/quality-profiles-empty.hbs';
-
-export default Marionette.ItemView.extend({
-  className: 'list-group-item',
-  template: Template
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profiles-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profiles-view.js
deleted file mode 100644 (file)
index 99f2ee9..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import Marionette from 'backbone.marionette';
-import ProfileView from './profile-view';
-import ProfilesEmptyView from './profiles-empty-view';
-import Template from './templates/quality-profiles-profiles.hbs';
-import LanguageTemplate from './templates/quality-profiles-profiles-language.hbs';
-
-export default Marionette.CompositeView.extend({
-  className: 'list-group',
-  template: Template,
-  languageTemplate: LanguageTemplate,
-  childView: ProfileView,
-  childViewContainer: '.js-list',
-  emptyView: ProfilesEmptyView,
-
-  collectionEvents: {
-    'filter': 'filterByLanguage'
-  },
-
-  childViewOptions (model) {
-    return {
-      collectionView: this,
-      highlighted: model.get('key') === this.highlighted
-    };
-  },
-
-  highlight (key) {
-    this.highlighted = key;
-    this.render();
-  },
-
-  attachHtml (compositeView, childView, index) {
-    const $container = this.getChildViewContainer(compositeView);
-    const model = this.collection.at(index);
-    if (model != null) {
-      const prev = this.collection.at(index - 1);
-      let putLanguage = prev == null;
-      if (prev != null) {
-        const lang = model.get('language');
-        const prevLang = prev.get('language');
-        if (lang !== prevLang) {
-          putLanguage = true;
-        }
-      }
-      if (putLanguage) {
-        $container.append(this.languageTemplate(model.toJSON()));
-      }
-    }
-    compositeView._insertAfter(childView);
-  },
-
-  destroyChildren () {
-    Marionette.CompositeView.prototype.destroyChildren.apply(this, arguments);
-    this.$('.js-list-language').remove();
-  },
-
-  filterByLanguage (language) {
-    if (language) {
-      this.$('[data-language]').addClass('hidden');
-      this.$(`[data-language="${language}"]`).removeClass('hidden');
-    } else {
-      this.$('[data-language]').removeClass('hidden');
-    }
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profiles.js b/server/sonar-web/src/main/js/apps/quality-profiles/profiles.js
deleted file mode 100644 (file)
index 5be52cc..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import Backbone from 'backbone';
-import Profile from './profile';
-
-export default Backbone.Collection.extend({
-  model: Profile,
-  url: window.baseUrl + '/api/qualityprofiles/search',
-  comparator: 'key',
-
-  parse (r) {
-    return r.profiles;
-  },
-
-  updateForLanguage (language) {
-    this.fetch({
-      data: {
-        language
-      },
-      merge: true,
-      reset: false,
-      remove: false
-    });
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/propTypes.js b/server/sonar-web/src/main/js/apps/quality-profiles/propTypes.js
new file mode 100644 (file)
index 0000000..c8e4042
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { PropTypes } from 'react';
+
+const { shape, string, number, bool, arrayOf } = PropTypes;
+
+export const ProfileType = shape({
+  key: string.isRequired,
+  name: string.isRequired,
+  isDefault: bool.isRequired,
+  isInherited: bool.isRequired,
+  language: string.isRequired,
+  languageName: string.isRequired,
+  activeRuleCount: number.isRequired,
+  activeDeprecatedRuleCount: number.isRequired,
+  projectCount: number,
+  parentKey: string,
+  parentName: string
+});
+
+export const ProfilesListType = arrayOf(ProfileType);
+
+const LanguageType = shape({
+  key: string.isRequired,
+  name: string.isRequired
+});
+
+export const LanguagesListType = arrayOf(LanguageType);
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/rename-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/rename-profile-view.js
deleted file mode 100644 (file)
index 52c4e7e..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import ModalFormView from '../../components/common/modal-form';
-import Template from './templates/quality-profiles-rename-profile.hbs';
-
-export default ModalFormView.extend({
-  template: Template,
-
-  onFormSubmit () {
-    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
-    this.sendRequest();
-  },
-
-  sendRequest () {
-    const that = this;
-    const url = window.baseUrl + '/api/qualityprofiles/rename';
-    const name = this.$('#rename-profile-name').val();
-    const options = {
-      key: this.model.get('key'),
-      name
-    };
-    return $.ajax({
-      url,
-      type: 'POST',
-      data: options,
-      statusCode: {
-        // do not show global error
-        400: null
-      }
-    }).done(function () {
-      that.model.set({ name });
-      that.destroy();
-    }).fail(function (jqXHR) {
-      that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
-    });
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/restore-built-in-profiles-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/restore-built-in-profiles-view.js
deleted file mode 100644 (file)
index a46487a..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import ModalFormView from '../../components/common/modal-form';
-import Template from './templates/quality-profiles-restore-built-in-profiles.hbs';
-import TemplateSuccess from './templates/quality-profiles-restore-built-in-profiles-success.hbs';
-
-export default ModalFormView.extend({
-  template: Template,
-  successTemplate: TemplateSuccess,
-
-  getTemplate () {
-    return this.selectedLanguage ? this.successTemplate : this.template;
-  },
-
-  onFormSubmit () {
-    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
-    this.disableForm();
-    this.sendRequest();
-  },
-
-  onRender () {
-    ModalFormView.prototype.onRender.apply(this, arguments);
-    this.$('select').select2({
-      width: '250px',
-      minimumResultsForSearch: 50
-    });
-  },
-
-  sendRequest () {
-    const that = this;
-    const url = window.baseUrl + '/api/qualityprofiles/restore_built_in';
-    const lang = this.$('#restore-built-in-profiles-language').val();
-    const options = { language: lang };
-    this.selectedLanguage = _.findWhere(this.options.languages, { key: lang }).name;
-    return $.ajax({
-      url,
-      type: 'POST',
-      data: options,
-      statusCode: {
-        // do not show global error
-        400: null
-      }
-    }).done(function () {
-      that.collection.fetch({ reset: true });
-      that.collection.trigger('destroy');
-      that.render();
-    }).fail(function (jqXHR) {
-      that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
-      that.enableForm();
-    });
-  },
-
-  serializeData () {
-    return _.extend(ModalFormView.prototype.serializeData.apply(this, arguments), {
-      languages: this.options.languages,
-      selectedLanguage: this.selectedLanguage
-    });
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/restore-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/restore-profile-view.js
deleted file mode 100644 (file)
index 3c5bad5..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import ModalFormView from '../../components/common/modal-form';
-import Profile from './profile';
-import Template from './templates/quality-profiles-restore-profile.hbs';
-import { restoreQualityProfile } from '../../api/quality-profiles';
-
-export default ModalFormView.extend({
-  template: Template,
-
-  onFormSubmit (e) {
-    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
-    const data = new FormData(e.currentTarget);
-
-    this.disableForm();
-
-    restoreQualityProfile(data)
-        .then(r => {
-          this.profile = r.profile;
-          this.ruleSuccesses = r.ruleSuccesses;
-          this.ruleFailures = r.ruleFailures;
-          this.render();
-          this.addProfile(r.profile);
-        })
-        .catch(e => {
-          this.enableForm();
-          e.response.json().then(r => this.showErrors(r.errors, r.warnings));
-        });
-  },
-
-  addProfile (profileData) {
-    const profile = new Profile(profileData);
-    this.collection.add([profile], { merge: true });
-    const addedProfile = this.collection.get(profile.id);
-    if (addedProfile != null) {
-      addedProfile.trigger('select', addedProfile);
-    }
-  },
-
-  serializeData() {
-    return Object.assign({}, ModalFormView.prototype.serializeData.apply(this, arguments), {
-      profile: this.profile,
-      ruleSuccesses: this.ruleSuccesses,
-      ruleFailures: this.ruleFailures
-    });
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/router.js b/server/sonar-web/src/main/js/apps/quality-profiles/router.js
deleted file mode 100644 (file)
index 3888597..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import Backbone from 'backbone';
-
-export default Backbone.Router.extend({
-  routes: {
-    '': 'index',
-    'index': 'index',
-    'show?key=:key': 'show',
-    'changelog*': 'changelog',
-    'compare*': 'compare'
-  },
-
-  initialize (options) {
-    this.app = options.app;
-  },
-
-  index () {
-    this.app.controller.index();
-  },
-
-  show (key) {
-    this.app.controller.show(key);
-  },
-
-  changelog () {
-    const params = window.getQueryParams();
-    this.app.controller.changelog(params.key, params.since, params.to);
-  },
-
-  compare () {
-    const params = window.getQueryParams();
-    if (params.key && params.withKey) {
-      this.app.controller.compare(params.key, params.withKey);
-    }
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/styles.css b/server/sonar-web/src/main/js/apps/quality-profiles/styles.css
new file mode 100644 (file)
index 0000000..c71d00c
--- /dev/null
@@ -0,0 +1,149 @@
+.quality-profile-box {
+  padding: 20px;
+  border: 1px solid #e6e6e6;
+  border-radius: 2px;
+  background-color: #fff;
+}
+
+.quality-profiles-table {
+  margin-top: 30px;
+}
+
+.quality-profiles-table-name { }
+
+.quality-profiles-table-inheritance {
+  width: 280px;
+}
+
+.quality-profiles-table-projects,
+.quality-profiles-table-rules,
+.quality-profiles-table-date {
+  min-width: 120px;
+}
+
+.quality-profiles-list-header {
+  line-height: 24px;
+  margin-bottom: 15px;
+  padding: 5px 10px;
+  border-radius: 3px;
+  background-color: #f3f3f3;
+}
+
+.quality-profile-grid {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+}
+
+.quality-profile-grid-left {
+  width: 340px;
+  flex-shrink: 0;
+}
+
+.quality-profile-grid-right {
+  flex-grow: 1;
+  margin-left: 20px;
+}
+
+.quality-profile-rules,
+.quality-profile-projects,
+.quality-profile-inheritance,
+.quality-profile-evolution {
+  border: 1px solid #e6e6e6;
+  border-radius: 2px;
+  background-color: #fff;
+}
+
+.quality-profile-evolution {
+  padding: 20px;
+}
+
+.quality-profile-projects,
+.quality-profile-inheritance {
+  padding: 15px 20px 20px;
+}
+
+.quality-profile-rules {
+}
+
+.quality-profile-rules > header {
+  padding: 15px 20px;
+}
+
+.quality-profile-rules-distribution {
+  padding: 0 20px 20px;
+}
+
+.quality-profile-rules-deprecated {
+  margin-bottom: 20px;
+  padding: 15px 20px;
+  border-top: 1px solid #e6e6e6;
+  border-bottom: 1px solid #e6e6e6;
+  background-color: #fcf8e3;
+}
+
+.quality-profile-exporters {
+  margin-top: 20px;
+}
+
+.quality-profile-evolution {
+  display: flex;
+  margin-top: 20px;
+}
+
+.quality-profile-evolution > div {
+  width: 50%;
+  text-align: center;
+}
+
+.quality-profile-projects {
+  margin-top: 20px;
+}
+
+.quality-profile-inheritance {
+}
+
+.quality-profile-progress-bar {
+  transition: width 0.5s ease;
+  animation: appear 0.5s forwards;
+}
+
+@keyframes appear {
+  0% { transform: translateX(-100%); }
+  100% { transform: translateX(0); }
+}
+
+.quality-profile-not-found {
+  padding-top: 100px;
+  text-align: center;
+}
+
+.quality-profiles-evolution {
+  display: flex;
+  justify-content: flex-start;
+  align-items: stretch;
+  margin-bottom: 30px;
+}
+
+.quality-profiles-evolution-deprecated,
+.quality-profiles-evolution-stagnant,
+.quality-profiles-evolution-rules {
+  width: 325px;
+  padding: 15px 20px;
+  box-sizing: border-box;
+}
+
+.quality-profiles-evolution-deprecated,
+.quality-profiles-evolution-stagnant {
+  margin-right: 30px;
+  border: 1px solid #faebcc;
+  background-color: #fcf8e3;
+}
+
+.quality-profiles-evolution-rules {
+  border: 1px solid #e6e6e6;
+}
+
+.quality-profile-comparison-table {
+  table-layout: fixed;
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-changelog.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-changelog.hbs
deleted file mode 100644 (file)
index aa3bebc..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-<header class="page-header">
-  <div class="page-title">
-    <span class="h3">{{t 'changelog'}}</span>
-  </div>
-</header>
-
-<form class="spacer-bottom" id="quality-profile-changelog-form">
-  {{t 'quality_profiles.changelog_from'}}
-  <input name="since" type="text" value="{{eventsParameters.since}}" placeholder="{{t 'optional'}}">
-  {{t 'to'}}
-  <input name="to" type="text" value="{{eventsParameters.to}}" placeholder="{{t 'optional'}}">
-  <button id="quality-profile-changelog-form-submit">{{t 'search_verb'}}</button>
-</form>
-
-{{#notEmpty events}}
-  <table class="width-100 data zebra">
-    <thead>
-    <tr>
-      <th>{{t 'date'}}</th>
-      <th>{{t 'user'}}</th>
-      <th>{{t 'action'}}</th>
-      <th>{{t 'rule'}}</th>
-      <th>{{t 'parameters'}}</th>
-    </tr>
-    </thead>
-    <tbody>
-    {{#each events}}
-      <tr>
-        <td class="text-top nowrap thin">{{dt date}}</td>
-        <td class="text-top nowrap thin">{{default authorName 'System'}}</td>
-        <td class="text-top nowrap">{{t 'quality_profiles.changelog' action}}</td>
-        <td class="text-top"><a href="{{rulePermalink ruleKey}}">{{ruleName}}</a></td>
-        <td class="text-top thin">
-          <ul>
-            {{#each params}}
-              <li>
-                {{#eq @key 'severity'}}
-                  <span class="nowrap">{{severityChangelog this}}</span>
-                {{else}}
-                  {{parameterChangelog @key this}}
-                {{/eq}}
-              </li>
-            {{/each}}
-          </ul>
-        </td>
-      </tr>
-    {{/each}}
-    </tbody>
-  </table>
-
-
-  <p class="spacer-top text-center">
-    {{#unlessLength events totalEvents}}
-      <a class="js-show-more-changelog spacer-right" href="#">{{t 'show_more'}}</a>
-    {{/unlessLength}}
-    <a class="js-hide-changelog" href="#">{{t 'hide'}}</a>
-  </p>
-
-{{else}}
-  {{#notNull totalEvents}}
-    <div class="alert alert-info">
-      {{t 'quality_profiles.changelog.empty'}}
-      <a class="js-hide-changelog spacer-left" href="#">{{t 'hide'}}</a>
-    </div>
-  {{/notNull}}
-{{/notEmpty}}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-comparison.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-comparison.hbs
deleted file mode 100644 (file)
index e26259b..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-<header class="page-header">
-  <div class="page-title">
-    <span class="h3">{{t 'compare'}}</span>
-  </div>
-</header>
-
-{{#notEmpty profiles}}
-  <form class="spacer-bottom" id="quality-profile-comparison-form">
-    <label class="text-middle" for="quality-profile-comparison-with-key">{{t 'with'}}</label>
-    <select id="quality-profile-comparison-with-key">
-      {{#each profiles}}
-        <option value="{{key}}" {{#eq key ../comparedWith}}selected{{/eq}}>{{name}}</option>
-      {{/each}}
-    </select>
-    <button class="text-middle" id="quality-profile-comparison-form-submit">{{t 'compare'}}</button>
-  </form>
-{{else}}
-  <div class="alert alert-info">{{t 'quality_profiles.no_profiles_for_comparison'}}</div>
-{{/notEmpty}}
-
-{{#notNull comparison}}
-  <table class="width-100 data zebra">
-    {{#notEmpty comparison.inLeft}}
-      <tr>
-        <td class="width-50"><h6>{{tp 'quality_profiles.x_rules_only_in' comparison.inLeftSize}} {{comparison.left.name}}</h6></td>
-        <td class="width-50"></td>
-      </tr>
-      {{#each comparison.inLeft}}
-        <tr class="js-comparison-in-left">
-          <td class="width-50">{{severityIcon severity}}&nbsp;<a href="{{rulePermalink key}}">{{name}}</a></td>
-          <td class="width-50"></td>
-        </tr>
-      {{/each}}
-    {{/notEmpty}}
-
-    {{#notEmpty comparison.inRight}}
-      <tr>
-        <td class="width-50"></td>
-        <td class="width-50"><h6>{{tp 'quality_profiles.x_rules_only_in' comparison.inRightSize}} {{comparison.right.name}}</h6></td>
-      </tr>
-      {{#each comparison.inRight}}
-        <tr class="js-comparison-in-right">
-          <td class="width-50"></td>
-          <td class="width-50">{{severityIcon severity}}&nbsp;<a href="{{rulePermalink key}}">{{name}}</a></td>
-        </tr>
-      {{/each}}
-    {{/notEmpty}}
-
-    {{#notEmpty comparison.modified}}
-      <tr>
-        <td class="text-center width-50" colspan="2">
-          <h6>{{tp 'quality_profiles.x_rules_have_different_configuration' comparison.modifiedSize}}</h6>
-        </td>
-      </tr>
-      <tr>
-        <td class="width-50"><h6>{{comparison.left.name}}</h6></td>
-        <td class="width-50"><h6>{{comparison.right.name}}</h6></td>
-      </tr>
-      {{#each comparison.modified}}
-        <tr class="js-comparison-modified">
-          <td class="width-50">
-            <p>{{severityIcon left.severity}}&nbsp;<a href="{{rulePermalink key}}">{{name}}</a></p>
-            {{#notNull left.params}}
-              <ul>
-                {{#each left.params}}
-                  <li class="spacer-top"><code>{{@key}}: {{this}}</code></li>
-                {{/each}}
-              </ul>
-            {{/notNull}}
-          </td>
-          <td class="width-50">
-            <p>{{severityIcon right.severity}}&nbsp;<a href="{{rulePermalink key}}">{{name}}</a></p>
-            {{#notNull right.params}}
-              <ul>
-                {{#each right.params}}
-                  <li class="spacer-top"><code>{{@key}}: {{this}}</code></li>
-                {{/each}}
-              </ul>
-            {{/notNull}}
-          </td>
-        </tr>
-      {{/each}}
-    {{/notEmpty}}
-  </table>
-
-  <p class="spacer-top text-center">
-    <a class="js-hide-comparison" href="#">{{t 'hide'}}</a>
-  </p>
-{{/notNull}}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-actions.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-actions.hbs
deleted file mode 100644 (file)
index 672a7a6..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<header class="page-header">
-  <h1 class="page-title">{{t 'quality_profiles.page'}}</h1>
-
-  {{#if canWrite}}
-    <div class="page-actions">
-      <div class="button-group dropdown">
-        <button id="quality-profiles-create">{{t 'create'}}</button>
-        <a id="quality-profiles-actions" class="button dropdown-toggle" href="#"
-           data-toggle="dropdown"><i class="icon-dropdown"></i></a>
-        <ul class="dropdown-menu dropdown-menu-right">
-          <li>
-            <a id="quality-profiles-restore" href="#">{{t 'quality_profiles.restore_profile'}}</a>
-          </li>
-          <li>
-            <a id="quality-profiles-restore-built-in" href="#">{{t 'quality_profiles.restore_built_in_profiles'}}</a>
-          </li>
-        </ul>
-      </div>
-    </div>
-  {{/if}}
-</header>
-
-<div class="dropdown" id="quality-profiles-filter-by-language">
-  <span>Show:</span>
-  {{#if selectedLanguage}}
-    <a class="dropdown-toggle link-no-underline" href="#" data-toggle="dropdown">
-      {{tp 'quality_profiles.x_profiles' selectedLanguage.name}} <i class="icon-dropdown"></i>
-    </a>
-  {{else}}
-    <a class="dropdown-toggle link-no-underline" href="#" data-toggle="dropdown">
-      {{t 'quality_profiles.all_profiles'}} <i class="icon-dropdown"></i>
-    </a>
-  {{/if}}
-  <ul class="dropdown-menu">
-    <li><a class="js-filter-by-language" href="#">{{t 'quality_profiles.all_profiles'}}</a></li>
-    {{#each languages}}
-      <li><a class="js-filter-by-language" href="#" data-language="{{key}}">{{name}}</a></li>
-    {{/each}}
-  </ul>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-projects.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-projects.hbs
new file mode 100644 (file)
index 0000000..09e5913
--- /dev/null
@@ -0,0 +1,12 @@
+<div class="modal-head">
+  <h2>{{t 'projects'}}</h2>
+</div>
+
+<div class="modal-body">
+  <div class="js-modal-messages"></div>
+  <div id="profile-projects"></div>
+</div>
+
+<div class="modal-foot">
+  <a href="#" class="js-modal-close">{{t 'close'}}</a>
+</div>
index 50fbc195fffdaa66d496debaafc910234895084e..845e4843ef6b571f27083e71f1d5e9f92ba27b03 100644 (file)
@@ -6,9 +6,9 @@
   <div class="modal-body">
     <div class="js-modal-messages"></div>
     <div class="modal-field">
-      <label for="create-profile-name">{{t 'name'}}<em class="mandatory">*</em></label>
-      <input id="create-profile-name" name="name" type="text" size="50" maxlength="100" required>
-    </div>
+    <label for="create-profile-name">{{t 'name'}}<em class="mandatory">*</em></label>
+    <input id="create-profile-name" name="name" type="text" size="50" maxlength="100" required>
+  </div>
     <div class="modal-field spacer-bottom">
       <label for="create-profile-language">{{t 'language'}}<em class="mandatory">*</em></label>
       <select id="create-profile-language" name="language" required>
index 99ee29ce6113071153193767798946973da2ba94..17438526340abec0707f31f49610665f71f050b8 100644 (file)
@@ -4,12 +4,12 @@
   </div>
   <div class="modal-body">
     <div class="js-modal-messages"></div>
-    {{#notEmpty children}}
+    {{#if childrenCount}}
       <div class="alert alert-warning">{{t 'quality_profiles.this_profile_has_descendants'}}</div>
       <p>{{tp 'quality_profiles.are_you_sure_want_delete_profile_x_and_descendants' name languageName}}</p>
     {{else}}
       <p>{{tp 'quality_profiles.are_you_sure_want_delete_profile_x' name languageName}}</p>
-    {{/notEmpty}}
+    {{/if}}
   </div>
   <div class="modal-foot">
     <button id="delete-profile-submit">{{t 'delete'}}</button>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-empty.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-empty.hbs
deleted file mode 100644 (file)
index eb1e821..0000000
+++ /dev/null
@@ -1 +0,0 @@
-{{t 'quality_profiles.no_results'}}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-intro.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-intro.hbs
deleted file mode 100644 (file)
index add14e7..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<div class="search-navigator-intro markdown">
-  <p>{{t 'quality_profiles.intro1'}}</p>
-  <p>{{t 'quality_profiles.intro2'}}</p>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-layout.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-layout.hbs
deleted file mode 100644 (file)
index ac6f40b..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<div class="search-navigator">
-  <div class="search-navigator-side search-navigator-side-light">
-    <div class="search-navigator-filters"></div>
-    <div class="quality-profiles-results panel"></div>
-  </div>
-
-  <div class="search-navigator-workspace">
-    <div class="search-navigator-workspace-header"></div>
-    <div class="search-navigator-workspace-details"></div>
-  </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-details.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-details.hbs
deleted file mode 100644 (file)
index d7db852..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-<div class="panel panel-vertical">
-  <div class="columns">
-    <div class="column-two-thirds" id="quality-profile-rules">
-      <header class="page-header">
-        <h3 class="page-title">{{t 'coding_rules'}}</h3>
-      </header>
-
-      <div>
-        <div class="display-inline-block text-right little-spacer-right" style="width: 40px">
-          <strong>
-            <a href="{{link rulesSearchUrl}}">{{formatMeasure activeRuleCount 'INT'}}</a>
-          </strong>
-        </div>
-        <strong>
-          {{tp 'quality_profile.x_active_rules' ''}}
-        </strong>
-      </div>
-
-      {{#notEmpty rulesTypes}}
-        <div class="abs-width-400 spacer-top">
-          {{#each rulesTypes}}
-            <div class="spacer-top">
-              <div class="display-inline-block text-right little-spacer-right" style="width: 40px">
-                <a href="{{link ../rulesSearchUrl '|types=' val}}">{{formatMeasure count 'INT'}}</a>
-              </div>
-              {{issueType val}}
-            </div>
-          {{/each}}
-        </div>
-      {{/notEmpty}}
-
-      <div class="spacer-top">
-        <a class="button" href="{{link activateRulesUrl}}">
-          {{t 'quality_profiles.activate_more'}}
-        </a>
-      </div>
-    </div>
-
-    {{#notEmpty exporters}}
-      <div class="column-third" id="quality-profile-permalinks">
-        <header class="page-header">
-          <h3 class="page-title">{{t 'permalinks'}}</h3>
-        </header>
-        <ul>
-          {{#each exporters}}
-            <li class="spacer-bottom" style="line-height: 1.5">
-              <a class="link-with-icon" href="{{exporterUrl ../this key}}" target="_blank">
-                <i class="icon-detach"></i>
-                <span>{{name}}</span>
-              </a>
-            </li>
-          {{/each}}
-        </ul>
-      </div>
-    {{/notEmpty}}
-  </div>
-</div>
-
-<div class="panel panel-vertical" id="quality-profile-projects">
-  <header class="page-header">
-    <h3 class="page-title">{{t 'projects'}}</h3>
-  </header>
-  {{#if isDefault}}
-    {{#if canWrite}}
-      <p class="alert alert-info">{{t 'quality_profiles.projects_for_default.edit'}}</p>
-    {{else}}
-      <p class="alert alert-info">{{t 'quality_profiles.projects_for_default'}}</p>
-    {{/if}}
-  {{else}}
-    <div id="quality-profile-projects-list" class="select-list-on-full-width"></div>
-  {{/if}}
-</div>
-
-<div class="panel panel-vertical" id="quality-profile-inheritance">
-  <header class="page-header">
-    <h3 class="page-title">{{t 'quality_profiles.profile_inheritance'}}</h3>
-    {{#if canWrite}}
-      <div class="button-group big-spacer-left">
-        <button id="quality-profile-change-parent">{{t 'quality_profiles.change_parent'}}</button>
-      </div>
-    {{/if}}
-  </header>
-  <div class="text-center">
-    {{#notEmpty ancestors}}
-      <ul id="quality-profile-ancestors">
-        {{#eachReverse ancestors}}
-          <li>
-            <div class="alert alert-inline alert-info">
-              <h6><a class="js-profile" data-key="{{key}}" href="{{profileUrl key}}">{{name}}</a></h6>
-              <p class="note">{{tp 'quality_profile.x_active_rules' activeRuleCount}}</p>
-              {{#if overridingRuleCount}}
-                <p class="note">{{tp 'quality_profiles.x_overridden_rules' overridingRuleCount}}</p>
-              {{/if}}
-            </div>
-            <div class="spacer-top spacer-bottom">
-              <i class="icon-move-down"></i>
-            </div>
-          </li>
-        {{/eachReverse}}
-      </ul>
-    {{/notEmpty}}
-
-    <div id="quality-profile-inheritance-current" class="alert alert-inline alert-success">
-      <h6>{{name}}</h6>
-      <p class="note">{{tp 'quality_profile.x_active_rules' activeRuleCount}}</p>
-      {{#if overridingRuleCount}}
-        <p class="note">{{tp 'quality_profiles.x_overridden_rules' overridingRuleCount}}</p>
-      {{/if}}
-    </div>
-
-    {{#notEmpty children}}
-      <div class="spacer-top spacer-bottom">
-        <i class="icon-move-down"></i>
-      </div>
-      <ul id="quality-profile-children" class="list-inline">
-        {{#eachReverse children}}
-          <li>
-            <div class="alert alert-inline alert-info">
-              <h6><a class="js-profile" data-key="{{key}}" href="{{profileUrl key}}">{{name}}</a></h6>
-              <p class="note">{{tp 'quality_profile.x_active_rules' activeRuleCount}}</p>
-              {{#if overridingRuleCount}}
-                <p class="note">{{tp 'quality_profiles.x_overridden_rules' overridingRuleCount}}</p>
-              {{/if}}
-            </div>
-          </li>
-        {{/eachReverse}}
-      </ul>
-    {{/notEmpty}}
-  </div>
-</div>
-
-<div class="panel panel-vertical" id="quality-profile-changelog"></div>
-
-<div class="panel panel-vertical" id="quality-profile-comparison"></div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-header.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-header.hbs
deleted file mode 100644 (file)
index cd674f4..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<h2 class="search-navigator-header-component">
-  {{name}}
-  <span class="note">{{languageName}}</span>
-</h2>
-
-<div class="search-navigator-header-actions">
-  <div class="button-group">
-    <a class="button" href="{{link '/api/qualityprofiles/backup?profileKey=' encodedKey}}"
-       id="quality-profile-backup">{{t 'backup_verb'}}</a>
-    {{#if canWrite}}
-      <button id="quality-profile-rename">{{t 'rename'}}</button>
-      <button id="quality-profile-copy">{{t 'copy'}}</button>
-      {{#unless isDefault}}
-        <button id="quality-profile-set-as-default">{{t 'set_as_default'}}</button>
-        <button id="quality-profile-delete" class="button-red">{{t 'delete'}}</button>
-      {{/unless}}
-    {{/if}}
-  </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile.hbs
deleted file mode 100644 (file)
index f6cc630..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<table>
-  <tr>
-    <td class="text-top">{{name}}</td>
-    {{#if isInherited}}
-      <td class="text-top thin spacer-left">
-        <i class="icon-inheritance" title="{{tp 'quality_profiles.inherits' parentName}}"
-           data-toggle="tooltip" data-placement="bottom"></i>
-      </td>
-    {{/if}}
-    <td class="text-top thin nowrap spacer-left">
-      {{#if isDefault}}
-        <span class="badge pull-right">{{t 'default'}}</span>
-      {{else}}
-        <span class="note pull-right">{{tp 'quality_profiles.x_projects' projectCountFormatted}}</span>
-      {{/if}}
-    </td>
-  </tr>
-</table>
-
-
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles-language.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles-language.hbs
deleted file mode 100644 (file)
index f3ec16f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<h6 class="spacer-top js-list-language" data-language="{{language}}">{{languageName}}</h6>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles.hbs
deleted file mode 100644 (file)
index 8022059..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<div class="js-list"></div>
index 41c449e88d4a1b7b30084c352da7b52a587d46d2..01ccc52871841fa8f86e678b592318746d4b3e12 100644 (file)
@@ -4,7 +4,7 @@
   </div>
   <div class="modal-body">
     <div class="alert alert-success">
-      {{tp 'quality_profiles.restore_built_in_profiles_success_message' selectedLanguage}}
+      {{tp 'quality_profiles.restore_built_in_profiles_success_message' language.name}}
     </div>
   </div>
   <div class="modal-foot">
index 9df1cc04587679f7768a68ffaebf065b1a8cd3b5..f7473ea8e2611e17ae75c6e19b2292d9210e01b2 100644 (file)
@@ -4,18 +4,10 @@
   </div>
   <div class="modal-body">
     <div class="js-modal-messages"></div>
-    <div id="restore-built-in-profiles-form-success" class="alert alert-success hidden"></div>
-    <div class="modal-field">
-      <label for="restore-built-in-profiles-language">{{t 'language'}}<em class="mandatory">*</em></label>
-      <select id="restore-built-in-profiles-language" name="language">
-        {{#each languages}}
-          <option value="{{key}}">{{name}}</option>
-        {{/each}}
-      </select>
-    </div>
+    {{tp 'quality_profiles.restore_built_in_profiles_confirmation' language.name}}
   </div>
   <div class="modal-foot">
     <button id="restore-built-in-profiles-submit">{{t 'restore'}}</button>
-    <a href="#" class="js-modal-close" id="restore-built-in-profiles-cancel">{{t 'cancel'}}</a>
+    <a href="#" class="js-modal-close">{{t 'close'}}</a>
   </div>
 </form>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/utils.js b/server/sonar-web/src/main/js/apps/quality-profiles/utils.js
new file mode 100644 (file)
index 0000000..cba412b
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * 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 sortBy from 'lodash/sortBy';
+
+export function sortProfiles (profiles) {
+  const result = [];
+  const sorted = sortBy(profiles, 'name');
+
+  function retrieveChildren (parent) {
+    return sorted.filter(p => (
+        (parent == null && p.parentKey == null) ||
+        (parent != null && p.parentKey === parent.key)
+    ));
+  }
+
+  function putProfile (profile = null, depth = 0) {
+    const children = retrieveChildren(profile);
+
+    if (profile != null) {
+      result.push({ ...profile, childrenCount: children.length, depth });
+    }
+
+    children.forEach(child => putProfile(child, depth + 1));
+  }
+
+  putProfile();
+
+  return result;
+}
+
+export function createFakeProfile (overrides) {
+  return {
+    key: 'key',
+    name: 'name',
+    isDefault: false,
+    isInherited: false,
+    language: 'js',
+    languageName: 'JavaScript',
+    activeRuleCount: 10,
+    activeDeprecatedRuleCount: 2,
+    projectCount: 3,
+    ...overrides
+  };
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeParentView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeParentView.js
new file mode 100644 (file)
index 0000000..6b40517
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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 ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-change-profile-parent.hbs';
+import { changeProfileParent } from '../../../api/quality-profiles';
+
+export default ModalFormView.extend({
+  template: Template,
+
+  onRender () {
+    ModalFormView.prototype.onRender.apply(this, arguments);
+    this.$('select').select2({
+      width: '250px',
+      minimumResultsForSearch: 50
+    });
+  },
+
+  onFormSubmit () {
+    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
+    this.disableForm();
+    this.sendRequest();
+  },
+
+  sendRequest () {
+    const parent = this.$('#change-profile-parent').val();
+    changeProfileParent(this.options.profile.key, parent)
+        .then(() => {
+          this.destroy();
+          this.trigger('done');
+        })
+        .catch(e => {
+          if (e.response.status === 400) {
+            this.enableForm();
+            e.response.json().then(r => this.showErrors(r.errors, r.warnings));
+          }
+        });
+  },
+
+  serializeData () {
+    const { profile } = this.options;
+    const profiles = this.options.profiles
+        .filter(p => p !== profile && p.language === profile.language);
+    return { ...profile, profiles };
+  }
+});
+
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeProjectsView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeProjectsView.js
new file mode 100644 (file)
index 0000000..5292bfa
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * 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 ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-change-projects.hbs';
+import { translate } from '../../../helpers/l10n';
+import '../../../components/SelectList';
+
+export default ModalFormView.extend({
+  template: Template,
+
+  onRender () {
+    ModalFormView.prototype.onRender.apply(this, arguments);
+
+    const { key } = this.options.profile;
+
+    const searchUrl = window.baseUrl + '/api/qualityprofiles/projects?key=' +
+        encodeURIComponent(key);
+
+    new window.SelectList({
+      searchUrl,
+      el: this.$('#profile-projects'),
+      width: '100%',
+      readOnly: false,
+      focusSearch: false,
+      format (item) {
+        return item.name;
+      },
+      selectUrl: window.baseUrl + '/api/qualityprofiles/add_project',
+      deselectUrl: window.baseUrl + '/api/qualityprofiles/remove_project',
+      extra: {
+        profileKey: key
+      },
+      selectParameter: 'projectUuid',
+      selectParameterValue: 'uuid',
+      labels: {
+        selected: translate('quality_gates.projects.with'),
+        deselected: translate('quality_gates.projects.without'),
+        all: translate('quality_gates.projects.all'),
+        noResults: translate('quality_gates.projects.noResults')
+      },
+      tooltips: {
+        select: translate('quality_profiles.projects.select_hint'),
+        deselect: translate('quality_profiles.projects.deselect_hint')
+      }
+    });
+  },
+
+  onDestroy() {
+    this.options.loadProjects();
+    ModalFormView.prototype.onDestroy.apply(this, arguments);
+  }
+});
+
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/CopyProfileView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/CopyProfileView.js
new file mode 100644 (file)
index 0000000..5809894
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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 ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-copy-profile.hbs';
+import { copyProfile } from '../../../api/quality-profiles';
+
+export default ModalFormView.extend({
+  template: Template,
+
+  onFormSubmit () {
+    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
+    this.disableForm();
+    this.sendRequest();
+  },
+
+  sendRequest () {
+    const name = this.$('#copy-profile-name').val();
+    copyProfile(this.options.profile.key, name)
+        .then(profile => {
+          this.destroy();
+          this.trigger('done', profile);
+        })
+        .catch(e => {
+          if (e.response.status === 400) {
+            this.enableForm();
+            e.response.json().then(r => this.showErrors(r.errors, r.warnings));
+          }
+        });
+  },
+
+  serializeData () {
+    return this.options.profile;
+  }
+});
+
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/CreateProfileView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/CreateProfileView.js
new file mode 100644 (file)
index 0000000..60d800b
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * 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 $ from 'jquery';
+import _ from 'underscore';
+import ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-create-profile.hbs';
+import { createQualityProfile } from '../../../api/quality-profiles';
+
+export default ModalFormView.extend({
+  template: Template,
+
+  events () {
+    return _.extend(ModalFormView.prototype.events.apply(this, arguments), {
+      'change #create-profile-language': 'onLanguageChange'
+    });
+  },
+
+  onFormSubmit () {
+    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
+
+    const form = this.$('form')[0];
+    const data = new FormData(form);
+
+    createQualityProfile(data)
+        .then(r => {
+          this.trigger('done', r.profile);
+          this.destroy();
+        })
+        .catch(e => {
+          e.response.json().then(r => this.showErrors(r.errors, r.warnings));
+        });
+  },
+
+  onRender () {
+    ModalFormView.prototype.onRender.apply(this, arguments);
+    this.$('select').select2({
+      width: '250px',
+      minimumResultsForSearch: 50
+    });
+    this.onLanguageChange();
+  },
+
+  onLanguageChange () {
+    const that = this;
+    const language = this.$('#create-profile-language').val();
+    const importers = this.getImportersForLanguages(language);
+    this.$('.js-importer').each(function () {
+      that.emptyInput($(this));
+      $(this).addClass('hidden');
+    });
+    importers.forEach(function (importer) {
+      that.$(`.js-importer[data-key="${importer.key}"]`).removeClass('hidden');
+    });
+  },
+
+  emptyInput (e) {
+    e.wrap('<form>').closest('form').get(0).reset();
+    e.unwrap();
+  },
+
+  getImportersForLanguages (language) {
+    if (language != null) {
+      return this.options.importers.filter(function (importer) {
+        return importer.languages.indexOf(language) !== -1;
+      });
+    } else {
+      return [];
+    }
+  },
+
+  serializeData () {
+    return _.extend(ModalFormView.prototype.serializeData.apply(this, arguments), {
+      languages: this.options.languages,
+      importers: this.options.importers
+    });
+  }
+});
+
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/DeleteProfileView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/DeleteProfileView.js
new file mode 100644 (file)
index 0000000..b9bb602
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-delete-profile.hbs';
+import { deleteProfile } from '../../../api/quality-profiles';
+
+export default ModalFormView.extend({
+  template: Template,
+
+  modelEvents: {
+    'destroy': 'destroy'
+  },
+
+  onFormSubmit () {
+    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
+    this.disableForm();
+    this.sendRequest();
+  },
+
+  sendRequest () {
+    deleteProfile(this.options.profile.key)
+        .then(() => {
+          this.destroy();
+          this.trigger('done');
+        })
+        .catch(e => {
+          if (e.response.status === 400) {
+            this.enableForm();
+            e.response.json().then(r => this.showErrors(r.errors, r.warnings));
+          }
+        });
+  },
+
+  serializeData () {
+    return this.options.profile;
+  }
+});
+
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/RenameProfileView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/RenameProfileView.js
new file mode 100644 (file)
index 0000000..964a4b5
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-rename-profile.hbs';
+import { renameProfile } from '../../../api/quality-profiles';
+
+export default ModalFormView.extend({
+  template: Template,
+
+  onFormSubmit () {
+    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
+    this.sendRequest();
+  },
+
+  sendRequest () {
+    const name = this.$('#rename-profile-name').val();
+    renameProfile(this.options.profile.key, name)
+        .then(profile => {
+          this.destroy();
+          this.trigger('done', profile);
+        })
+        .catch(e => {
+          if (e.response.status === 400) {
+            this.enableForm();
+            e.response.json().then(r => this.showErrors(r.errors, r.warnings));
+          }
+        });
+  },
+
+  serializeData () {
+    return this.options.profile;
+  }
+});
+
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreBuiltInProfilesView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreBuiltInProfilesView.js
new file mode 100644 (file)
index 0000000..f71f85c
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-restore-built-in-profiles.hbs';
+import TemplateSuccess from '../templates/quality-profiles-restore-built-in-profiles-success.hbs';
+import { restoreBuiltInProfiles } from '../../../api/quality-profiles';
+
+export default ModalFormView.extend({
+  template: Template,
+  successTemplate: TemplateSuccess,
+
+  getTemplate () {
+    return this.done ? this.successTemplate : this.template;
+  },
+
+  onFormSubmit () {
+    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
+    this.disableForm();
+    this.sendRequest();
+  },
+
+  sendRequest () {
+    restoreBuiltInProfiles(this.options.language.key)
+        .then(() => {
+          this.done = true;
+          this.render();
+          this.trigger('done');
+        })
+        .catch(e => {
+          this.enableForm();
+          e.response.json().then(r => this.showErrors(r.errors, r.warnings));
+        });
+  },
+
+  serializeData () {
+    return { language: this.options.language };
+  }
+});
+
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreProfileView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreProfileView.js
new file mode 100644 (file)
index 0000000..4fceffd
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-restore-profile.hbs';
+import { restoreQualityProfile } from '../../../api/quality-profiles';
+
+export default ModalFormView.extend({
+  template: Template,
+
+  onFormSubmit (e) {
+    ModalFormView.prototype.onFormSubmit.apply(this, arguments);
+    const data = new FormData(e.currentTarget);
+
+    this.disableForm();
+
+    restoreQualityProfile(data)
+        .then(r => {
+          this.profile = r.profile;
+          this.ruleSuccesses = r.ruleSuccesses;
+          this.ruleFailures = r.ruleFailures;
+          this.render();
+          this.trigger('done');
+        })
+        .catch(e => {
+          this.enableForm();
+          e.response.json().then(r => this.showErrors(r.errors, r.warnings));
+        });
+  },
+
+  serializeData() {
+    return {
+      ...ModalFormView.prototype.serializeData.apply(this, arguments),
+      profile: this.profile,
+      ruleSuccesses: this.ruleSuccesses,
+      ruleFailures: this.ruleFailures
+    };
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.js b/server/sonar-web/src/main/js/components/controls/DateInput.js
new file mode 100644 (file)
index 0000000..5958152
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * 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 $ from 'jquery';
+import React from 'react';
+import pick from 'lodash/pick';
+import './styles.css';
+
+export default class DateInput extends React.Component {
+  static propTypes = {
+    value: React.PropTypes.string,
+    format: React.PropTypes.string,
+    name: React.PropTypes.string,
+    placeholder: React.PropTypes.string,
+    onChange: React.PropTypes.func.isRequired
+  };
+
+  static defaultProps = {
+    value: '',
+    format: 'yy-mm-dd'
+  };
+
+  componentDidMount () {
+    this.attachDatePicker();
+  }
+
+  componentWillReceiveProps (nextProps) {
+    this.refs.input.value = nextProps.value;
+  }
+
+  handleChange () {
+    const { value } = this.refs.input;
+    this.props.onChange(value);
+  }
+
+  attachDatePicker () {
+    const opts = {
+      dateFormat: this.props.format,
+      changeMonth: true,
+      changeYear: true,
+      onSelect: this.handleChange.bind(this)
+    };
+
+    if ($.fn && $.fn.datepicker) {
+      $(this.refs.input).datepicker(opts);
+    }
+  }
+
+  render () {
+    const inputProps = pick(this.props, ['placeholder', 'name']);
+
+    return (
+        <span className="date-input-control">
+          <input
+              className="date-input-control-input"
+              ref="input"
+              type="text"
+              initialValue={this.props.value}
+              readOnly={true}
+              {...inputProps}/>
+          <span className="date-input-control-icon">
+            <svg width="14" height="14" viewBox="0 0 16 16">
+              <path
+                  d="M5.5 6h2v2h-2V6zm3 0h2v2h-2V6zm3 0h2v2h-2V6zm-9 6h2v2h-2v-2zm3 0h2v2h-2v-2zm3 0h2v2h-2v-2zm-3-3h2v2h-2V9zm3 0h2v2h-2V9zm3 0h2v2h-2V9zm-9 0h2v2h-2V9zm11-9v1h-2V0h-7v1h-2V0h-2v16h15V0h-2zm1 15h-13V4h13v11z"/>
+            </svg>
+          </span>
+        </span>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/controls/styles.css b/server/sonar-web/src/main/js/components/controls/styles.css
new file mode 100644 (file)
index 0000000..1ed5c1f
--- /dev/null
@@ -0,0 +1,26 @@
+.date-input-control {
+  position: relative;
+  display: inline-block;
+  cursor: pointer;
+}
+
+.date-input-control-input {
+  width: 105px;
+  padding-left: 24px !important;
+  cursor: pointer;
+}
+
+.date-input-control-icon {
+  position: absolute;
+  top: 5px;
+  left: 5px;
+}
+
+.date-input-control-icon path {
+  fill: #cdcdcd;
+  transition: fill 0.3s ease;
+}
+
+.date-input-control-input:focus + .date-input-control-icon path {
+  fill: #4b9fd5;
+}
index 4536c61dbb3d2dd099c875090bdbc1d12b661feb..13f679014471e4146bfb6278a53b29aa555863ac 100644 (file)
@@ -66,7 +66,7 @@ export const TooltipsContainer = React.createClass({
   },
 
   componentWillUpdate() {
-    this.hideTooltips();
+    this.destroyTooltips();
   },
 
   componentDidUpdate () {
index 6bbbbec9efa852e1bbb4cda2de302ceefc32d1ab..818d8068e77f05eca8fe324bf016c99cc8c5e012 100644 (file)
@@ -26,11 +26,12 @@ export default React.createClass({
     if (!this.props.severity) {
       return null;
     }
-    return <span>
-      <span className="spacer-right">
-        <SeverityIcon severity={this.props.severity}/>
-      </span>
-      {translate('severity', this.props.severity)}
-    </span>;
+    return (
+        <span>
+          <SeverityIcon severity={this.props.severity}/>
+          {' '}
+          {translate('severity', this.props.severity)}
+        </span>
+    );
   }
 });
index cf1a0fe57a4e281bd5925032c62f59a9f3fb12d2..17c424ab9e7f7d3e267cc0e514c09fd1e3d3faf4 100644 (file)
@@ -101,3 +101,29 @@ export function getQualityProfileUrl (key) {
 export function getQualityGateUrl (key) {
   return window.baseUrl + '/quality_gates/show/' + encodeURIComponent(key);
 }
+
+/**
+ * Generate URL for the rules page
+ * @param {object} query
+ * @returns {string}
+ */
+export function getRulesUrl (query) {
+  if (query) {
+    const serializedQuery = Object.keys(query).map(criterion => (
+        `${encodeURIComponent(criterion)}=${encodeURIComponent(
+            query[criterion])}`
+    )).join('|');
+    return window.baseUrl + '/coding_rules#' + serializedQuery;
+  }
+  return window.baseUrl + '/coding_rules';
+}
+
+/**
+ * Generate URL for the rules page filtering only active deprecated rules
+ * @param {object} query
+ * @returns {string}
+ */
+export function getDeprecatedActiveRulesUrl (query = {}) {
+  const baseQuery = { activation: 'true', statuses: 'DEPRECATED' };
+  return getRulesUrl({ ...query, ...baseQuery });
+}
index 84f496dea95c41f6472085b6150e04c0c2db59c2..e6a828a05428005cb0128da12884bb08486f8cff 100644 (file)
@@ -42,6 +42,12 @@ body {
   padding-bottom: 20px;
 }
 
+.page-limited-small {
+  .page-limited;
+  width: 1080px;
+  box-sizing: border-box;
+}
+
 .page-container {
   min-width: 1080px;
 }
index aa8d7613397bfcbf7daadc7d1d3b9241fe4a2fc3..9483155f10ca452348358f2d8074b8071f4996bf 100644 (file)
@@ -36,6 +36,10 @@ class ProfilesController < ApplicationController
     render :action => 'index'
   end
 
+  def create
+    render :action => 'index'
+  end
+
   # GET /profiles/export?name=<profile name>&language=<language>&format=<exporter key>
   def export
     language = params[:language]
index 083265fe8a56ad3017916f4a12048beb41d18ee3..d3dbd50844ced670df13c735a4cbd8c0d4e74f7b 100644 (file)
@@ -1,3 +1,3 @@
 --recursive
---compilers js:babel-register,css:tests/null-compiler.js
+--compilers js:babel-register,css:tests/null-compiler.js,hbs:tests/null-compiler.js
 --require tests/jsdom-setup.js
index f0167414d7a5e1d792e24dffbf8a1ea585bbb0f4..22595ed633752fd66cb097fa52856d9499a07691 100644 (file)
@@ -22,3 +22,4 @@ function nothing () {
 }
 
 require.extensions['.css'] = nothing;
+require.extensions['.hbs'] = nothing;
index 2f7820ffcf4da9a8bcff2ceac2aceeaacee8d388..c4854f06fe38404219be26f8cfed7d5e38485472 100644 (file)
@@ -102,6 +102,7 @@ moreCriteria=+ More Criteria
 name=Name
 name_too_long_x=Name is too long (maximum is {0} characters)
 navigation=Navigation
+never=Never
 none=None
 unassigned=Not assigned
 off=Off
@@ -688,6 +689,10 @@ issue.set_type=Change Type
 issue.type.CODE_SMELL=Code Smell
 issue.type.BUG=Bug
 issue.type.VULNERABILITY=Vulnerability
+issue.type.CODE_SMELL.plural=Code Smells
+issue.type.BUG.plural=Bugs
+issue.type.VULNERABILITY.plural=Vulnerabilities
+
 
 issue.status.REOPENED=Reopened
 issue.status.REOPENED.description=Transitioned to and then back from some other status.
@@ -1670,7 +1675,8 @@ cloud.top_risk=Top risk
 quality_profiles.quality_profiles=Quality Profiles
 quality_profiles.new_profile=New Profile
 quality_profiles.compare_profiles=Compare Profiles
-quality_profiles.restore_profile=Restore Profile
+quality_profiles.compare_with=Compare with
+quality_profiles.restore_profile=Restore
 quality_profiles.restore_submit=Restore
 quality_profiles.restore_profile.success={1} rule(s) restored in profile "{0}"
 quality_profiles.restore_profile.warning={1} rule(s) restored, {2} rule(s) ignored in profile "{0}"
@@ -1698,7 +1704,7 @@ quality_profiles.editing_profile=Editing profile
 quality_profiles.profile_inheritance=Inheritance
 quality_profiles.available_projects=Available projects
 quality_profiles.associated_projects=Associated projects
-quality_profiles.no_projects_associated_to_profile_x=No projects are explicitly associated to the profile "{0}".
+quality_profiles.no_projects_associated_to_profile=No projects are explicitly associated to the profile.
 quality_profiles.projects_warning=List of projects explicitly associated to this Quality profile :
 quality_profiles.including_x_overriding.suffix=, incl. {0} overriding
 quality_profiles.set_parent=Set parent
@@ -1707,7 +1713,7 @@ quality_profiles.no_version=no version
 quality_profiles.last_version_x_with_date=last version {0} ({1})
 quality_profiles.version_x_with_date=version {0} ({1})
 quality_profiles.version_x=version {0}
-quality_profiles.parameter_set_to_x=Parameter <strong>{0}</strong> set to <strong>{1}</strong>
+quality_profiles.parameter_set_to=Parameter {0} set to {1}
 quality_profiles.only_in_profile_x=Only in {0}
 quality_profiles.with_different_configuration=With different configuration
 quality_profiles.with_same_configuration=With same configuration
@@ -1727,8 +1733,8 @@ quality_profiles.copy_new_name=New name
 quality_profiles.copy_overwrite_x=You are about to copy this quality profile into the existing "{0}" profile, which will fully overwrite it. Please confirm the name if you want to overwrite, or choose another name.
 quality_profiles.copy_x_overwritten=Profile '{0}' has been overwritten.
 quality_profiles.rename_x_title=Rename Profile {0} - {1}
-quality_profiles.restore_built_in_profiles=Restore Built-in Profiles
-quality_profiles.restore_built_in_profiles_confirmation=Are you sure you want to restore "{0}" profile(s) for {1}?
+quality_profiles.restore_built_in_profiles=Restore Built-in
+quality_profiles.restore_built_in_profiles_confirmation=Are you sure you want to restore {0} built-in profiles?
 quality_profiles.restore_built_in_profiles_success_message={0} built-in profiles have been restored.
 quality_profiles.including=including
 quality_profiles.deprecated=deprecated
@@ -1736,30 +1742,50 @@ quality_profiles.manage_rules_tooltip=Manage rules of this profile
 quality_profiles.manage_rules_tooltip_x_profile=Manage rules of profile '{0}'
 quality_profiles.see_rules_tooltip=See rules of this profile
 quality_profiles.see_rules_tooltip_x_profile=See rules of profile '{0}'
-quality_profiles.severity_set_to_x=Severity set to <strong>{0}</strong>
+quality_profiles.severity_set_to=Severity set to
 quality_profiles.changelog_from=Changelog from
 quality_profiles.changelog.empty=No changes have been done.
 quality_profiles.changelog.ACTIVATED=Activated
 quality_profiles.changelog.DEACTIVATED=Deactivated
 quality_profiles.changelog.UPDATED=Updated
-quality_profiles.changelog.parameter_reset_to_default_value_x=Parameter <strong>{0}</strong> reset to default value
+quality_profiles.changelog.parameter_reset_to_default_value=Parameter {0} reset to default value
 quality_profiles.deleted_profile=The profile {0} doesn't exist anymore
 quality_profiles.projects_for_default=Every project not specifically associated to a quality profile will be associated to this one by default.
 quality_profiles.projects_for_default.edit=You must not select specific projects for the default quality profile.
 quality_profiles.inherits=Inherits "{0}"
+quality_profile.x_rules={0} rules
 quality_profile.x_active_rules={0} active rules
+quality_profile.total_active_rules=Total Active Rules
 quality_profiles.x_overridden_rules={0} overridden rules
 quality_profiles.change_parent=Change Parent
 quality_profiles.all_profiles=All Profiles
 quality_profiles.x_profiles={0} Profiles
+quality_profiles._quality_profiles=quality profiles
 quality_profiles.x_projects={0} projects
 quality_profiles.no_results=No profiles found. Try installing a language plugin.
 quality_profiles.projects.select_hint=Click to associate this project with the quality profile
 quality_profiles.projects.deselect_hint=Click to remove association between this project and the quality profile
 quality_profiles.no_profiles_for_comparison=There are no profiles for comparison
+quality_profile.empty_comparison=The quality profiles are equal.
 quality_profiles.activate_more=Activate More
 quality_profiles.intro1=Quality Profiles are collections of rules to apply during an analysis.
 quality_profiles.intro2=For each language there is a default profile. All projects not explicitly assigned to some other profile will be analyzed with the default.
+quality_profiles.list.profile=Profile
+quality_profiles.list.inheritance=Inheritance
+quality_profiles.list.projects=Projects
+quality_profiles.list.rules=Rules
+quality_profiles.list.updated=Updated
+quality_profiles.list.used=Used
+quality_profiles.x_activated_out_of_y={0} rules activated out of {1} available
+quality_profiles.change_projects=Change Projects
+quality_profiles.not_found=The requested quality profile was not found.
+quality_profiles.latest_new_rules=Latest New Rules
+quality_profiles.latest_new_rules.activated={0}, activated on {1} profiles
+quality_profiles.latest_new_rules.not_activated={0}, not yet activated
+quality_profiles.deprecated_rules=Deprecated Rules
+quality_profiles.x_deprecated_rules_are_still_activated={0} deprecated rules are still activated on {1} quality profiles:
+quality_profiles.stagnant_profiles=Stagnant Profiles
+quality_profiles.not_updated_more_than_year=The following profiles haven't been updated for more than 1 year: